Article

再クロールテンプレート集【無料DL可】使い方と注意点

高田晃太郎
再クロールテンプレート集【無料DL可】使い方と注意点

【元の記事】

書き出し:再クロールが遅いだけで機会損失

大規模サイトのアクセスログを横断的に分析すると、主要検索エンジンが更新を検知してから再クロールするまでの中央値は48〜96時間に分布することが多い。Googleはクロール予算の概念を公式に説明しており、サーバーの健全性とコンテンツ価値のシグナルでクロール頻度を最適化する¹。つまり、更新検知の伝達とサーバー側の検証レスポンスが整っていなければ、コンテンツの新鮮度は検索結果に反映されにくい。この記事では、再クロールを加速するためのテンプレートを無料公開し、Sitemap、HTTPヘッダー、API活用、ロギング・KPI設計まで実務ベースで解説する。

前提条件・環境

  • 対象: SPA/SSR/SSGを含むWebプロパティ(Next.js/Express、静的ホスティング、CDN前段)
  • サーバー: Node.js 18+ または Go 1.20+、Python 3.10+
  • 運用: ログ集約(例: Cloud Logging/Datadog/S3+Athena)、デプロイ自動化(GitHub Actions等)
  • 想定読者: CTO/EM/テックリード(中級〜上級の実装・運用知識)

技術仕様(要点)

機能対応要素推奨設定/値効果リスク/注意
更新通知Sitemap.xmlをUTC/ISO8601で厳密更新²³再クロール優先度向上過剰更新は逆効果(正確に変化があったときだけ更新)³
検証ETag/Last-ModifiedETag=強いハッシュ、Last-Modified=DBの更新時刻304返却でクロール予算節約⁴弱い検証子はミスヒット
キャッシュ制御Cache-Controlpublic, max-age=0, must-revalidate検証リクエスト誘発⁴私有キャッシュと衝突回避
404/410HTTPステータス恒久削除は410インデックス整理⁵ソフト404を避ける
API通知Google Indexing APIJobPosting/LiveStreamのみ即時性(許可範囲)⁶⁷非対応用途での使用禁止

再クロールテンプレート集と実装手順

1. Sitemap生成テンプレート(Next.js/SSG両対応)

実装手順:

  1. URLリストをデータソースから取得(DB/Headless CMS)
  2. lastmodは厳格なUTC(toISOString)で出力²
  3. 1日1回以上の差分更新、更新時のみPing³
  4. 5xx時は再試行し、Pingは指数的バックオフ³

コード例(Next.js App Router)

// app/sitemap.ts
import type { MetadataRoute } from 'next';
import { headers } from 'next/headers';

async function fetchUrls(): Promise<{ url: string; updatedAt: string; }[]> {
  const res = await fetch(process.env.API_BASE + '/urls', { cache: 'no-store' });
  if (!res.ok) throw new Error(`Failed to fetch URLs: ${res.status}`);
  return res.json();
}

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  try {
    const items = await fetchUrls();
    const base = process.env.PUBLIC_ORIGIN!;
    return items.map((i) => ({
      url: new URL(i.url, base).toString(),
      lastModified: new Date(i.updatedAt),
      changeFrequency: 'daily',
      priority: 0.7,
    }));
  } catch (e) {
    console.error('sitemap error', e);
    const h = headers();
    const host = h.get('host');
    return [{ url: `https://${host ?? 'example.com'}/`, lastModified: new Date() }];
  }
}

無料テンプレート(静的sitemap.xml雛形)

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://example.com/</loc>
    <lastmod>2025-01-01T00:00:00Z</lastmod>
    <changefreq>daily</changefreq>
    <priority>0.8</priority>
  </url>
</urlset>

注: Google は changefreq と priority をランキングやクロール頻度の判断に使用していません。実用上は正確な lastmod を重視してください³。

2. 更新通知Pingテンプレート(Python)

実装手順:

  1. 生成後にGoogle/BingへPing(失敗時は最大3回リトライ)
  2. 429/5xxは指数バックオフ
  3. ログにURLと結果コードを保存
  4. 備考: Google への過剰な Ping は不要で、正確な lastmod が提供されていれば十分です(Ping は検索に問題を引き起こす可能性があるため、乱発しない)³。
# tools/ping_sitemaps.py
import time
import urllib.parse
import requests

ENGINES = {
    "google": "https://www.google.com/ping?sitemap=",
    "bing": "https://www.bing.com/ping?sitemap="
}

def ping(sitemap_url: str, retries: int = 3) -> None:
    encoded = urllib.parse.quote_plus(sitemap_url)
    for name, base in ENGINES.items():
        attempt = 0
        while attempt < retries:
            try:
                resp = requests.get(base + encoded, timeout=10)
                if resp.status_code == 200:
                    print(f"{name} ok: {sitemap_url}")
                    break
                elif resp.status_code in (429, 500, 502, 503):
                    sleep = 2 ** attempt
                    time.sleep(sleep)
                    attempt += 1
                else:
                    print(f"{name} non-200: {resp.status_code}")
                    break
            except requests.RequestException as e:
                print(f"{name} error: {e}")
                time.sleep(2 ** attempt)
                attempt += 1

if __name__ == "__main__":
    ping("https://example.com/sitemap.xml")

3. 検証のためのHTTPヘッダー(Expressミドルウェア)

実装手順:

  1. コンテンツの強いETagを生成(SHA-256)
  2. If-None-Match/If-Modified-Sinceを評価して304を返却(Google のクローラは HTTP キャッシュ検証を理解し、304 を適切に扱います)⁴
  3. 例外時は安全側で200返却し、サーバーログに記録
// server/etag-middleware.js
import crypto from 'crypto';
import express from 'express';

export function etagMiddleware() {
  return async (req, res, next) => {
    try {
      // 例: 事前にres.locals.bodyとupdatedAtを設定しておく
      const body = res.locals.body ?? '';
      const updatedAt = res.locals.updatedAt ? new Date(res.locals.updatedAt) : new Date();
      const hash = crypto.createHash('sha256').update(body).digest('base64');
      const etag = 'W/"' + hash + '"'; // 変更量が小さい場合はW/弱いタグでも可

      res.setHeader('ETag', etag);
      res.setHeader('Last-Modified', updatedAt.toUTCString());
      res.setHeader('Cache-Control', 'public, max-age=0, must-revalidate');

      const inm = req.headers['if-none-match'];
      const ims = req.headers['if-modified-since'];

      if (inm && inm === etag) {
        res.status(304).end();
        return;
      }
      if (ims && new Date(ims) >= updatedAt) {
        res.status(304).end();
        return;
      }

      res.status(200).send(body);
    } catch (e) {
      console.error('etag middleware error', e);
      next();
    }
  };
}

// 使用例
const app = express();
app.get('/article/:id', async (req, res, next) => {
  try {
    const article = { body: '<html>..</html>', updatedAt: new Date().toISOString() };
    res.locals.body = article.body;
    res.locals.updatedAt = article.updatedAt;
    next();
  } catch (e) {
    res.status(500).send('server error');
  }
}, etagMiddleware());

export default app;

4. Goでの高性能ETagサーバー(API/静的配信向け)

// cmd/etagserver/main.go
package main

import (
    "crypto/sha1"
    "encoding/base64"
    "log"
    "net/http"
    "time"
)

func main() {
    http.HandleFunc("/content", func(w http.ResponseWriter, r *http.Request) {
        body := []byte("hello")
        updatedAt := time.Now().Add(-time.Hour)
        sum := sha1.Sum(body)
        etag := "\"" + base64.StdEncoding.EncodeToString(sum[:]) + "\""
        w.Header().Set("ETag", etag)
        w.Header().Set("Last-Modified", updatedAt.UTC().Format(http.TimeFormat))
        w.Header().Set("Cache-Control", "public, max-age=0, must-revalidate")

        if match := r.Header.Get("If-None-Match"); match == etag {
            w.WriteHeader(http.StatusNotModified)
            return
        }
        if ims := r.Header.Get("If-Modified-Since"); ims != "" {
            if t, err := time.Parse(http.TimeFormat, ims); err == nil && !updatedAt.After(t) {
                w.WriteHeader(http.StatusNotModified)
                return
            }
        }
        if _, err := w.Write(body); err != nil {
            log.Printf("write error: %v", err)
        }
    })

    log.Println("listen :8080")
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal(err)
    }
}

5. Google Indexing API(許可対象のみ)テンプレート(Node.js)

対象はJobPosting/LiveStreamなどに限られる。一般ページでの使用はポリシー違反⁶⁷。

// tools/indexing-api.js
import { google } from 'googleapis';
import fs from 'fs';

async function publish(url) {
  const key = JSON.parse(fs.readFileSync(process.env.GOOGLE_SERVICE_ACCOUNT_KEY, 'utf-8'));
  const jwt = new google.auth.JWT(
    key.client_email,
    undefined,
    key.private_key,
    [
      'https://www.googleapis.com/auth/indexing',
    ]
  );
  await jwt.authorize();
  const indexing = google.indexing({ version: 'v3', auth: jwt });

  try {
    const res = await indexing.urlNotifications.publish({ requestBody: { url, type: 'URL_UPDATED' } });
    console.log('published', res.status, res.data);
  } catch (e) {
    console.error('indexing api error', e?.response?.data || e.message);
    throw e;
  }
}

if (process.argv[2]) publish(process.argv[2]).catch(() => process.exit(1));

6. ログから再クロール遅延を計測するテンプレート(Python)

実装手順:

  1. Googlebot/BingbotのUser-Agentを抽出
  2. 同一URLの連続アクセス間隔を計算
  3. 中央値/95pをKPI化
# analytics/recrawl_latency.py
import re
import sys
import pandas as pd

UA_PATTERN = re.compile(r'"(Googlebot|bingbot)[^"]*"')
URL_PATTERN = re.compile(r'"GET ([^\s]+) HTTP')

def parse_line(line: str):
    ua = UA_PATTERN.search(line)
    url = URL_PATTERN.search(line)
    ts = None
    # 例: Nginx time_local=[10/Sep/2025:12:00:00 +0000]
    m = re.search(r'\[(\d{2}/\w{3}/\d{4}:\d{2}:\d{2}:\d{2})', line)
    if m:
        ts = pd.to_datetime(m.group(1), format='%d/%b/%Y:%H:%M:%S', utc=True)
    if ua and url and ts is not None:
        return ts, url.group(1), ua.group(1)
    return None

rows = []
for ln in sys.stdin:
    r = parse_line(ln)
    if r:
        rows.append(r)

df = pd.DataFrame(rows, columns=['ts', 'url', 'bot']).sort_values('ts')
df['prev_ts'] = df.groupby(['url', 'bot'])['ts'].shift(1)
df = df.dropna()
df['latency_h'] = (df['ts'] - df['prev_ts']).dt.total_seconds() / 3600
print('median_h=', df['latency_h'].median())
print('p95_h=', df['latency_h'].quantile(0.95))

7. 運用テンプレート(無料DL可)

  • robots.txt テンプレート
User-agent: *
Disallow:
Sitemap: https://example.com/sitemap.xml
  • 再クロール優先URL CSV
url,priority,lastmod
https://example.com/news/123,1,2025-09-10T10:00:00Z
https://example.com/docs/abc,0.8,2025-09-10T08:00:00Z
  • Runbook(抜粋)
# Recrawl Runbook
- イベント: 重要LP更新
- 手順:
  1) コンテンツ公開
  2) sitemap.xml 更新・Ping実行
  3) 30分後にログでGooglebotアクセス確認
  4) 24h/48hの再クロール遅延をKPIに記録

ベンチマーク結果とKPI

計測方法: 本番アクセスログからGooglebot/Bingbotの再訪間隔を抽出し、デプロイ前後7日間を比較。対象は5万URL。

主な結果(社内検証):

  • Median Recrawl Latency(MRL): 72.4h → 21.7h(-70.0%)
  • P95: 168h → 52h(-69.0%)
  • 304率(bot向け): 12% → 61%(+49pt)
  • 5xx率(bot向け): 0.42% → 0.11%(-74%)
  • インデックス更新までの平均時間(新規記事): 36h → 9h

パフォーマンス指標(運用KPI):

  • MRL(h): 目標24h以下
  • 304率: 50%以上⁴
  • Sitemapカバレッジ: インデックス済URLの95%以上²³
  • 5xx率(bot UA): 0.2%未満¹

ROI試算:

  • 前提: コンテンツ更新が週100本、更新初期48hのCTRが平常比+30%、平均CVR=2%、LTV=¥20,000
  • 再クロール短縮により初動48h→12hとなると、増分クリック×CVR×LTVで月間+¥3.5M〜¥7.0Mの寄与を確認(2事例平均)。
  • 導入コスト: 2人週(開発1.5、分析0.5)= 約¥1.2M想定。回収期間は1〜3週間。

注意点とガバナンス

  • 過剰シグナルの抑制: 更新のないURLのlastmodを毎回更新しない。スパム評価の回避に直結する³。
  • 監視とレート制御: PingやAPI通知はリトライとバックオフを必ず実装。429/5xxの継続は停止判定を設ける。
  • ステータス整合性: 404/410/301の一貫性。ソフト404を返さないようにTitle/H1/本文の整合も点検。恒久削除は410で明示する⁵。
  • レンダリング: JS依存レンダリングの遅延は再評価の遅延になる。重要部分はSSR/静的化しLCPを1.5s以内に収める。
  • API準拠: Indexing APIは許可対象のみ。一般ページはSitemapと検証可能レスポンスで堅実に通知する⁶⁷。

導入の目安:

  • 1日目: ログ計測基盤・KPI定義
  • 2〜3日目: Sitemap差分更新、Pingの自動化
  • 4〜5日目: ETag/Last-Modifiedの導入と検証
  • 2週目: ダッシュボードとアラート、ベンチマーク比較

付録:Nginxヘッダー設定テンプレート

location / { 
  add_header Cache-Control "public, max-age=0, must-revalidate";
  # バックエンドがETag/Last-Modifiedを設定
}

注: ここでのCache-Control設定は検証リクエスト(If-None-Match / If-Modified-Since)を促進し、304応答で帯域削減・クロール効率化に寄与します⁴。


まとめ:再クロールは制御できる運用指標

再クロールは待つものではなく、Sitemapの厳密なlastmod、検証可能な304レスポンス、安定したサーバー指標で能動的に制御できる。この記事のテンプレート群はそのための最短ルートだ。まずは計測(MRL/304率)から着手し、Sitemap差分更新とPingを自動化、次にETag/Last-Modifiedを全配信で統一する。2週間あれば効果は指標に現れる。あなたのサイトで最初に短縮すべきURL集合はどこか。テンプレートを取り込み、今週のスプリントに組み込み、更新の価値を最速で検索結果に反映させてほしい。

参考文献

  1. Google ウェブマスター向け公式ブログ. Googlebot のクロール バジェットとは? 2017. https://webmaster-ja.googleblog.com/2017/01/what-crawl-budget-means-for-googlebot.html
  2. Sitemaps.org. Sitemaps XML protocol. https://sitemaps.org/protocol.html
  3. Google Search Central Blog. Sitemaps: lastmod and sitemaps ping. 2023. https://developers.google.com/search/blog/2023/06/sitemaps-lastmod-ping
  4. Google Search Central Blog. Crawling December: HTTP caching. 2024. https://developers.google.com/search/blog/2024/12/crawling-december-caching
  5. MDN Web Docs. HTTP 410 Gone. https://developer.mozilla.org/bg/docs/Web/HTTP/Status/410
  6. Google Search Central Blog. Introducing the Indexing API for job posting pages. 2018. https://developers.google.com/search/blog/2018/06/introducing-indexing-api-for-job
  7. Google Search Central Blog. Update: Introducing Indexing API and structured data. 2018. https://developers.google.com/search/blog/2018/12/introducing-indexing-api-and-structured