Bing Webmaster Toolsが「IndexNow 未設定」と言ってくる。検索流入のほぼ全部がBingなので、Bingが推奨するものは素直に入れることにした。

IndexNowは、検索エンジンに「このURLが新しくなった」とこちらから伝えるための仕組みだ。クロールを待たずに知らせられる。Bing、Yandex、Naverなどが共有していて、1つのエンドポイントに送れば参加エンジン全部に伝わる。

仕組み自体は簡単だ。難しいのは「何を、いつ送るか」のほうだった。

設置: キーを置いてPOSTする

  1. 任意の英数字のキーを決める(32文字程度)
  2. https://<ホスト>/<キー>.txt に、キーそのものを中身とするファイルを置く
  3. https://api.indexnow.org/indexnow にJSONをPOSTする
{
  "host": "tools.omikuji.dev",
  "key": "<キー>",
  "keyLocation": "https://tools.omikuji.dev/<キー>.txt",
  "urlList": ["https://tools.omikuji.dev/newtool/", "..."]
}

応答は 200 か 202。1リクエストで最大10,000件まで送れる。

キーは「所有者であることの証明」なので、公開されているファイルの中身がキーそのものだ。秘密ではない。ただし他人のホストに対しては使えないので、漏れて困るものでもない。

問題: 同じURLを毎日送ってはいけない

素朴に考えると「毎朝、サイトマップの全URLを送る」になる。これは駄目だ。

IndexNowのガイドラインには、変更のないURLを繰り返し送らないことが明記されている。乱用すると送信元ごと無視される可能性がある。「とりあえず全部送っておけ」は、一番やってはいけない使い方だった。

送るべきなのは、前回から増えた(または変わった)URLだけだ。となると「前回何があったか」を覚えておく必要がある。

前回のサイトマップをどこに置くか

覚えておく場所の候補は3つあった。

  • リポジトリにコミットする: 毎朝スナップショットが更新されてコミットが積まれる。履歴が汚れるし、pushがまたデプロイを起こす
  • 外部ストレージ: KVでもR2でもいいが、これだけのために口を1つ増やしたくない
  • GitHub Actionsのキャッシュ: 無料で、リポジトリを汚さず、消えても困らない

3番にした。消えても困らないのが決め手だ。スナップショットが消えたら、その日はベースラインを取り直すだけで、送信をスキップする。次の日からまた差分が取れる。

- name: Restore previous sitemap snapshot
  uses: actions/cache/restore@v4
  with:
    path: .indexnow-sitemap.txt
    key: indexnow-sitemap-${{ github.run_id }}
    restore-keys: |
      indexnow-sitemap-
- name: Submit new URLs to IndexNow
  run: node scripts/indexnow-submit.mjs --diff .indexnow-sitemap.txt
- name: Save sitemap snapshot
  if: always()
  uses: actions/cache/save@v4
  with:
    path: .indexnow-sitemap.txt
    key: indexnow-sitemap-${{ github.run_id }}

キーに run_id を入れて毎回新しいエントリを作り、restore-keys の前方一致で直近のものを拾う。Actionsのキャッシュはキーの完全一致が無いと前方一致で最新を返すので、これで「前回の」が取れる。

if: always() を付けているのは、送信に失敗した回でもスナップショットは更新したいからだ。更新しないと、次の日に「前回」が2日前になって、昨日の差分が二重に送られる。

スクリプト: 差分を取って送る

if (args[0] === "--diff") {
  const snapshotPath = args[1] ?? ".indexnow-sitemap.txt";
  const current = await fetchSitemapUrls();       // 本番の sitemap-index を読む

  if (!existsSync(snapshotPath)) {
    writeFileSync(snapshotPath, current.join("\n") + "\n");
    console.log("初回実行: ベースラインを保存しました。送信はしません");
    process.exit(0);
  }

  const prev = new Set(readFileSync(snapshotPath, "utf8").split("\n").filter(Boolean));
  const added = current.filter((u) => !prev.has(u));
  writeFileSync(snapshotPath, current.join("\n") + "\n");

  if (added.length === 0) {
    console.log("新規 URL なし。送信はしません");
    process.exit(0);
  }
  await submit(added);
}

やっていることは集合の差だけだ。サイトマップは本番のものを読む。ローカルでビルドしたものではなく、実際に配信されているsitemap-indexを取りに行く。デプロイに失敗して古いままの日に、新しいURLを送ってしまわないためだ。

送信側では、ホストが自分のものでないURLを弾く。サイトマップに他所のURLが混ざることは無いはずだが、誤って他人のホストに送ると、そのホストのキーで検証されて失敗するだけでなく、行儀の悪い送信元として記録される。

const invalid = urlList.filter((u) => !u.startsWith(`https://${HOST}/`));
if (invalid.length > 0) { console.error("..."); process.exit(1); }

途中で変えたこと: 日次リビルドをやめた

このワークフローは、最初 daily-deploy.yml という名前だった。IndexNowを送る前に、サイトの再ビルドを毎朝起こしていた。

毎朝の再ビルドは「念のため」で入っていた。でも調べたら、このサイトは全ページSSRで、ビルド時刻に依存する出力が無い。新しいURLは main へのpushでしか増えない。毎朝ビルドしても、何も変わらないものを作り直しているだけだった。

再ビルドの段を落として、差分送信だけ残した。ファイル名も daily-indexnow.yml に変えた。ビルドは減り、Actionsの消費分も減った。 「念のため」で入れていたものは、たいてい念のためにもなっていない。

効いたのか

正直に書く。順位や表示回数がIndexNowのおかげで上がった、とは言えない。

そもそもIndexNowは順位を上げる仕組みではない。「新しいURLに気づくのを早くする」仕組みだ。効果があるとすれば「公開からクロールまでの時間」だが、IndexNow無しの対照を取っていないので、それも言えない。

同じ時期にtitleとmeta descriptionも直したが、そちらは2か月後にBingのAPIで測ったら順位は動いていなかった。改善2・悪化2・不変1・消失1で、統計的にはノイズだ。IndexNowもtitleも「やって損は無いが、これで数字が動くわけではない」に落ち着いた。

それでも残している理由は、コストがほぼゼロだからだ。毎朝1回、数秒のジョブが走るだけ。ガイドラインに沿って差分だけ送っている限り、害も無い。

まとめ

  • IndexNowは「新しいURLを知らせる」仕組み。順位を上げる仕組みではない
  • 同じURLを繰り返し送らない。 差分だけ送る。「毎朝全部送る」は乱用にあたる
  • 差分を取るには前回のサイトマップが要る。GitHub Actionsのキャッシュに置けば、リポジトリを汚さず、消えてもベースラインを取り直すだけで済む
  • キャッシュのキーに run_id を入れて、restore-keys の前方一致で直近を拾う
  • if: always() でスナップショットは失敗した回も更新する。しないと翌日に二重送信になる
  • サイトマップは本番のものを読む。 ローカルビルドではなく、実際に配信されているものを
  • 自分のホスト以外のURLは送信前に弾く
  • 「念のため」の日次リビルドは、SSRサイトでは何も生まなかった。落として問題は起きていない
  • 効果は「クロールが早くなる」以上のものではなく、それを測る対照も持っていない。コストがゼロだから残している

「入れると順位が上がる」と思って入れると、がっかりする。「入れておいて損は無い」ものとして入れると、ちょうどいい。