IT技術ブログ

IndexNowをCloudflare Pagesのデプロイに組み込み、Bingに新しいURLを知らせる実装。鍵の置き方、送るURLの選び方、送るタイミング、応答コードの意味と、このブログでの送信記録

IndexNowを静的サイトのデプロイ後に組み込む実装です。鍵ファイルの置き方、サイトマップの差分で送るURLを選ぶ方法、反映を待ってから送る理由、応答コードの意味を、このブログの送信記録と送らずに中身を確かめた結果で説明します。

この記事の結論:IndexNowは、鍵のファイル・送るURLの選び方・送る時機の3つを決めて組み込みます。①鍵のファイルをサイトのルートに/<鍵>.txtとして置き、②前回送ったときのサイトマップと今のサイトマップを比べて「増えた・変わった・消えた」URLだけを選び、③デプロイが終わって公開側に反映されたことを確かめてから送る、という形です。応答の200と202は「受け付けた」という意味で、Bingに載ったかどうかは、Bing Webmaster Toolsで別に確かめます。

このブログ(Astroで作った静的サイトを、Cloudflare Pagesで公開しています)では、2026年10月2日にIndexNowの鍵と送信スクリプトを入れ、これまでに4回送りました。この記事では、その送信記録をそのまま載せたうえで、サイトマップの差分だけを送るスクリプトを新しく書き、IndexNowには送らずに、送る中身だけを表示するモードで動きを確かめました。IndexNowのAPIへの送信は、この記事のためには行っていません。

IndexNowは、URLの追加・更新・削除をサイトの側から検索エンジンへ伝える仕組み

IndexNowを使うと、「このURLが増えた・変わった・消えた」を、サイトの側から検索エンジンへ直接伝えられます。検索エンジンがクロールして変化に気づくのを待たずに済みます。

仕組みの要点は次の3つです。

  • 鍵でサイトの持ち主であることを示す。 送信には鍵を付け、同じ鍵をサイトに置いたファイルで公開します。検索エンジンはそのファイルを読んで、送ってきたのがサイトの持ち主かを確かめます。
  • 1か所に送れば、参加している検索エンジンに共有される。 IndexNowのドキュメントでは、参加している検索エンジンのどれか1つに送れば、ほかの参加しているエンジンにも共有されると説明されています。このブログでは、api.indexnow.orgに送っています。
  • 送っても、載ることは約束されない。 IndexNowのFAQでもBingの案内でも、送ったURLがすぐにクロールやインデックスの対象になるとは限らないと書かれています。クロールするかどうかは、検索エンジンが決めます。

IndexNowのFAQに載っている送り先の一覧(Amazon、Bing、Naver、Seznam.cz、Yandex、Yep など)に、Googleは入っていません。Googleには、これまでどおりサイトマップとSearch Consoleで知らせます。サイトマップを送ったあとに「検出 - インデックス未登録」が続いたときの切り分けは、Search Consoleのインデックス未登録の記事にまとめています。

鍵は、サイトのルートに「鍵.txt」として置く

鍵のファイルは、サイトのルートに<鍵>.txtという名前で置き、中身にも同じ鍵を書きます。IndexNowのドキュメントでは、鍵は8〜128文字で、使える文字は英字の大文字・小文字、数字、ハイフンと決められています。ファイルはUTF-8で保存します。

このブログでは、32文字の16進数の鍵を作り、Astroのpublic/フォルダに<鍵>.txtとして置きました。public/の中身は、ビルドのときにサイトのルートへそのまま出力されます。2026年10月4日に公開側のURLを開くと、状態コードは200、Content-Typeはtext/plain; charset=utf-8でした。

$ curl -s -o /dev/null -w "%{http_code} %{content_type}\n" https://www.yamayamabloglink.com/<鍵>.txt
200 text/plain; charset=utf-8

鍵はルート以外の場所にも置けます。その場合は、送るたびにkeyLocationでファイルの場所を伝えます。ただし、ドキュメントでは、ルート以外に置いた鍵で送れるのは、そのファイルがあるパスで始まるURLだけだと説明されています。サイト全体のURLを送るなら、ルートに置くのが簡単です。

この鍵は、仕様上サイトで公開する値なので、秘密の情報ではありません。ただ、鍵を知っていれば、ほかの人もこのサイトのURLを送れることになります。このブログでは、記事やログに載せるときは先頭の4文字だけを見せるようにしています。

このブログでの送信記録:移行直後は全URL、その後は1件ずつ

このブログでの送信は、ドメインの移行の直後と、記事をまとめて公開した直後が全URL、その後は公開した記事1件だけ、という流れでした。応答コードは、記録の仕組みを入れる前の送信だったため、4回とも残っていません。

送信の記録(docs/indexnow-log.csv)は、2026年10月4日15時台に記録の仕組みを入れたときに、それ以前の作業メモから書き起こしたものです。

日時(日本時間・目安) 件数 送ったもの 応答
2026-10-02 夕方 98 サイトマップの全URL(WordPressからの移行の直後) 記録なし
2026-10-02 23時ごろ 記録なし サイトマップの全URL(記事13本を公開した直後) 記録なし
2026-10-03 朝 169 サイトマップの全URL(記事30本を公開した直後) 記録なし
2026-10-04 14時 1 公開した記事1本のURL 記録なし

送信に使ったのは、リポジトリにある次のスクリプトです。引数でURLを渡せばそのURLだけを、引数がなければ公開中のサイトマップの全URLを送ります。応答コードは画面に出すだけだったので、10月4日に、実行のたびにCSVへ1行ずつ書き足す処理を加えました。

scripts/indexnow.mjsjs
#!/usr/bin/env node
// IndexNow:指定した URL(または sitemap の全URL)を Bing など参加している検索エンジンへ知らせる。
// 使い方: node scripts/indexnow.mjs https://www.yamayamabloglink.com/xxx/ ...   (引数なしなら sitemap の全URL)
// 鍵は public/<32桁>.txt(IndexNow の仕様で公開してよい値)
// 実行ごとに docs/indexnow-log.csv へ1行追記する(時刻,件数,応答,送り方,URL)。記録しないときは INDEXNOW_LOG=0
import { appendFileSync, existsSync, readdirSync, readFileSync } from 'node:fs';
const HOST = 'www.yamayamabloglink.com';
const keyFile = readdirSync('public').find((f) => /^[0-9a-f]{32}\.txt$/.test(f));
const key = readFileSync(`public/${keyFile}`, 'utf8').trim();
let urls = process.argv.slice(2);
const source = urls.length ? 'args' : 'sitemap';
if (!urls.length) {
  const sm = await (await fetch(`https://${HOST}/sitemap-0.xml`)).text();
  urls = [...sm.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]);
}
const res = await fetch('https://api.indexnow.org/indexnow', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json; charset=utf-8' },
  body: JSON.stringify({ host: HOST, key, keyLocation: `https://${HOST}/${keyFile}`, urlList: urls }),
});
console.log(`IndexNow: ${urls.length}件 → ${res.status}`);
if (process.env.INDEXNOW_LOG !== '0') {
  const LOG = 'docs/indexnow-log.csv';
  try {
    if (!existsSync(LOG)) appendFileSync(LOG, 'timestamp,count,status,source,urls\n');
    // sitemap 全件のときは URL を並べず、件数だけ(全件=その時点の sitemap-0.xml)
    const list = source === 'args' ? urls.map((u) => u.replace(`https://${HOST}`, '')).join(' ') : '';
    appendFileSync(LOG, `${new Date().toISOString()},${urls.length},${res.status},${source},"${list.replaceAll('"', '""')}"\n`);
  } catch (e) {
    console.warn(`記録できませんでした: ${e.message}`);
  }
}

振り返ると、移行の直後の全URLの送信は、IndexNowのFAQに沿っています。FAQでは、移転やリニューアルでサイト全体が変わったときは全URLを送ってかまわないとしつつ、ふだんは追加・更新・削除したURLだけを送るものだと説明されています。記事をまとめて公開した直後の2回は、本来は新しい記事のURLだけでよかったはずです。この反省から、次の章の「差分だけを送る」作りにしました。

WordPressからAstroとCloudflare Pagesへの移行そのものの手順は、WordPressから静的サイトへの移行の記事に書いています。

送るURLは、前回のサイトマップとの差分で選ぶ

送るURLは、前回送ったときのサイトマップを保存しておき、今のサイトマップと比べて「増えたURL」「lastmodが変わったURL」「消えたURL」の3つに絞ります。人が送るURLを選ぶと、更新した記事を送り忘れたり、消したURLを送らなかったりします。

2026年10月4日に公開中のサイトマップを数えると、URLは242件で、そのうちlastmodが付いているのは167件でした。このブログのサイトマップでlastmodを持つのは記事のURLだけで、トップやカテゴリーの一覧は日付なしです。一覧は記事を足すたびに中身が動くため、1つの日付で正しく表しにくいからです。そのため、差分の判定は次のようにしました。

  • 増えた:今のサイトマップにあり、前回には無いURL。一覧のページも含む
  • 変わった:両方にあり、lastmodが変わったURL。lastmodの無い一覧のページは対象にならない
  • 消えた:前回にあり、今のサイトマップには無いURL

使い始めの1回は、送らずに今の状態を記録するだけにしています。Bingの案内のFAQに、IndexNowを使い始める前に変わったURLは送らず、使い始めてから変わったURLだけを送るよう書かれているためです。

送るのは、デプロイが終わり、公開側に反映されたのを確かめてから

送信は、Cloudflare Pagesのデプロイが終わり、送るURLが公開側で新しい状態になってから行います。ビルドコマンドの中で送ると、その時点ではまだ新しいページが公開されていないので、検索エンジンが取りに来たときに404や古いページを返すおそれがあります。

そこでスクリプトは、送る前に各URLをHEADで開き、増えた・変わったURLは200、消えたURLは404・410・301・308になっていることを確かめます。条件に合わなければ30秒おきに10回まで待ち、それでも反映されなければ送らずに止まります。こうしておくと、デプロイの完了の知らせを受けて手で実行する場合でも、CIの中でデプロイのコマンドの後ろにつなぐ場合でも、早すぎる送信を防げます。

消えたURLを送るのは、検索エンジンに「このURLは無くなった」と早く知らせるためです。IndexNowのドキュメントでも、追加と更新に加えて削除も知らせる対象になっています。消したつもりのURLがまだ200を返していたら、送る前にそちらを直します。

Cloudflareには、キャッシュの状態から変更を推測してIndexNowで知らせる「Crawler Hints」という機能もあります。Cloudflareの説明では無料のプランから使えるとされていますが、Pagesのサイトでどう動くかは書かれていません。このブログでは、Crawler Hintsの設定は確かめておらず、送信は自分のスクリプトで行っています。

差分だけを送るスクリプト

次のスクリプトは、公開中のサイトマップを読んで前回の記録と比べ、反映を確かめてから、増えた・変わった・消えたURLを1回のリクエストで送ります。DRY_RUN=1を付けると、送らずに送る中身だけを表示します。

indexnow-deploy.mjsjs
#!/usr/bin/env node
// デプロイが終わったあとに実行し、前回から「増えた・変わった・消えた」URLだけを IndexNow で送る。
//   node indexnow-deploy.mjs            … 差分を送る
//   node indexnow-deploy.mjs --init     … 今の状態を記録するだけ(使い始めの1回。過去のURLは送らない)
//   DRY_RUN=1 node indexnow-deploy.mjs  … 送らずに、送る中身だけを表示する
import { existsSync, readFileSync, readdirSync, writeFileSync, appendFileSync } from 'node:fs';

const HOST = process.env.INDEXNOW_HOST ?? 'www.yamayamabloglink.com';
const PUBLIC_DIR = process.env.PUBLIC_DIR ?? 'public';            // 鍵のファイルがあるフォルダ
const SNAPSHOT = process.env.SNAPSHOT ?? '.indexnow-snapshot.json'; // 前回送ったときのサイトマップ
const LOG = process.env.INDEXNOW_LOG_FILE ?? 'indexnow-log.csv';
const DRY_RUN = process.env.DRY_RUN === '1';
const INIT = process.argv.includes('--init');

// 1. 鍵:public/<鍵>.txt を探す。ファイル名と中身が同じであることも確かめる
const keyFile = readdirSync(PUBLIC_DIR).find((f) => /^[0-9a-f]{32}\.txt$/.test(f));
if (!keyFile) throw new Error(`${PUBLIC_DIR} に鍵のファイルがありません`);
const key = readFileSync(`${PUBLIC_DIR}/${keyFile}`, 'utf8').trim();
if (`${key}.txt` !== keyFile) throw new Error('鍵のファイル名と中身が一致しません');

// 2. 公開中のサイトマップを読む(loc と lastmod)
async function readSitemap() {
  const xml = await (await fetch(`https://${HOST}/sitemap-0.xml`)).text();
  const map = {};
  for (const [, body] of xml.matchAll(/<url>(.*?)<\/url>/gs)) {
    const loc = body.match(/<loc>([^<]+)<\/loc>/)?.[1];
    const lastmod = body.match(/<lastmod>([^<]+)<\/lastmod>/)?.[1] ?? null;
    if (loc) map[loc] = lastmod;
  }
  return map;
}

// 3. 公開側が新しい状態になっているかを確かめる(送ったURLを開いたら古い・無い、を防ぐ)
async function waitUntil(url, ok, { tries = 10, intervalMs = 30_000 } = {}) {
  for (let i = 1; i <= tries; i++) {
    const res = await fetch(url, { method: 'HEAD', redirect: 'manual' });
    if (ok(res.status)) return res.status;
    if (i < tries) await new Promise((r) => setTimeout(r, intervalMs));
  }
  throw new Error(`${url} がまだ反映されていません`);
}

const now = await readSitemap();
const prev = existsSync(SNAPSHOT) ? JSON.parse(readFileSync(SNAPSHOT, 'utf8')) : null;

if (INIT || !prev) {
  writeFileSync(SNAPSHOT, JSON.stringify(now, null, 1));
  console.log(`今の ${Object.keys(now).length} 件を記録しました(送信はしていません)`);
  process.exit(0);
}

const added = Object.keys(now).filter((u) => !(u in prev));
const changed = Object.keys(now).filter((u) => u in prev && now[u] && now[u] !== prev[u]);
const removed = Object.keys(prev).filter((u) => !(u in now));
console.log(`増えた ${added.length} / 変わった ${changed.length} / 消えた ${removed.length}`);

// 増えた・変わったURLは 200、消えたURLは 404・410・301・308 になっていることを確かめる
for (const u of [...added, ...changed]) console.log(`  ${await waitUntil(u, (s) => s === 200)} ${u}`);
for (const u of removed) console.log(`  ${await waitUntil(u, (s) => [404, 410, 301, 308].includes(s))} ${u}`);

const urlList = [...added, ...changed, ...removed];
if (!urlList.length) {
  console.log('送るURLはありません');
  process.exit(0);
}
if (urlList.length > 10_000) throw new Error('1回に送れるのは 10,000 件までです');

const payload = { host: HOST, key, keyLocation: `https://${HOST}/${keyFile}`, urlList };
if (DRY_RUN) {
  // 鍵は公開してよい値だが、記事やログに貼るときのために先頭だけ見せる
  const shown = { ...payload, key: `${key.slice(0, 4)}…`, keyLocation: payload.keyLocation.replace(key, `${key.slice(0, 4)}…`) };
  console.log('DRY_RUN:送らずに終わります。送る中身:');
  console.log(JSON.stringify(shown, null, 2));
  process.exit(0);
}

const res = await fetch('https://api.indexnow.org/indexnow', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json; charset=utf-8' },
  body: JSON.stringify(payload),
});
const line = `${new Date().toISOString()},${urlList.length},${res.status},diff,"${urlList.map((u) => u.replace(`https://${HOST}`, '')).join(' ')}"\n`;
appendFileSync(LOG, line);

if (res.status === 200 || res.status === 202) {
  writeFileSync(SNAPSHOT, JSON.stringify(now, null, 1)); // 受け付けられたときだけ「前回」を更新する
  console.log(`IndexNow: ${urlList.length} 件 → ${res.status}`);
} else if (res.status === 429) {
  console.warn('429:送りすぎ。前回の記録は更新せず、次の実行でもう一度送ります');
} else {
  // 400・403・422 は、送り方か鍵の置き方が間違っている。CI を止めて直す
  console.error(`IndexNow: ${res.status} ${await res.text()}`);
  process.exit(1);
}

前回の記録(.indexnow-snapshot.json)は、送信が受け付けられたとき(200か202)だけ更新します。429で断られたときや、途中で止まったときは更新しないので、次の実行で同じURLをもう一度送れます。

送らずに中身を確かめた結果

2026年10月4日に、公開中のこのブログのサイトマップを相手に、DRY_RUN=1で動かしました。IndexNowのAPIには送っていません。鍵は、リポジトリのpublic/にある本物のファイルを読ませています。

まず、使い始めの記録と、何も変えずにもう一度動かしたときの結果です。

$ DRY_RUN=1 node indexnow-deploy.mjs --init
今の 242 件を記録しました(送信はしていません)
$ DRY_RUN=1 node indexnow-deploy.mjs
増えた 0 / 変わった 0 / 消えた 0
送るURLはありません

次に、差分の判定を確かめるため、保存した記録をテスト用に書き換えました。実在する記事1本を記録から消し(増えたことになる)、別の記事1本のlastmodを古い日付にし(変わったことになる)、実在しないURLを1つ足しました(消えたことになる)。サイトのほうは何も変えていません。

simulate.mjs(テスト用)js
// テスト用:前回の記録を書き換えて「増えた・変わった・消えた」を1件ずつ作る
import { readFileSync, writeFileSync } from 'node:fs';
const F = '.indexnow-snapshot.json';
const s = JSON.parse(readFileSync(F, 'utf8'));
const H = 'https://www.yamayamabloglink.com';
delete s[`${H}/wedding-venue-web-staff-weekly-checklist/`];        // → 増えた
s[`${H}/ai-crawler-robots-txt-cloudflare/`] = '2026-10-01T00:00:00.000Z'; // → 変わった
s[`${H}/indexnow-dry-run-removed-test/`] = '2026-10-01T00:00:00.000Z';    // → 消えた(実在しないURL)
writeFileSync(F, JSON.stringify(s, null, 1));
$ DRY_RUN=1 node indexnow-deploy.mjs
増えた 1 / 変わった 1 / 消えた 1
  200 https://www.yamayamabloglink.com/wedding-venue-web-staff-weekly-checklist/
  200 https://www.yamayamabloglink.com/ai-crawler-robots-txt-cloudflare/
  404 https://www.yamayamabloglink.com/indexnow-dry-run-removed-test/
DRY_RUN:送らずに終わります。送る中身:
{
  "host": "www.yamayamabloglink.com",
  "key": "49d7…",
  "keyLocation": "https://www.yamayamabloglink.com/49d7….txt",
  "urlList": [
    "https://www.yamayamabloglink.com/wedding-venue-web-staff-weekly-checklist/",
    "https://www.yamayamabloglink.com/ai-crawler-robots-txt-cloudflare/",
    "https://www.yamayamabloglink.com/indexnow-dry-run-removed-test/"
  ]
}

増えた・変わったURLは公開側で200、消えたことにしたURLは404になっていることを確かめたうえで、3件を1回で送る中身が組み立てられました。

応答コードの意味と、受け取ったあとの扱い

IndexNowの応答は、200と202なら受け付けられた、400・403・422なら送り方か鍵に問題がある、429なら送りすぎです。IndexNowのドキュメントとBingの案内に載っている意味と、このスクリプトでの扱いを並べます。

応答 公式の説明(要約) このスクリプトでの扱い
200 受け付けた 前回の記録を更新する
202 受け取った。鍵の確認はこれから 前回の記録を更新する。鍵の確認が済めば、以降は200になるとFAQに書かれている
400 形式が正しくない 止める。JSONの形とContent-Typeを見直す
403 鍵が無効(鍵のファイルが無い、ファイルの中身が鍵と違う) 止める。公開側で鍵のファイルが開けるかを確かめる
422 URLがホストに属していない、または鍵が仕様に合っていない 止める。hostとurlListのドメイン、wwwの有無をそろえる
429 送りすぎ(スパムの疑い) 記録を更新せず、時間をおいて次の実行で送り直す

200について、IndexNowのドキュメントでは、検索エンジンがURLの一覧を受け取ったことだけを表すと書かれています。IndexNowのFAQには、送ったURLはサイトのクロールの割り当て(クロールの量の上限)に数えられること、同じURLを送り直すときは間をあけることも書かれています(FAQの中に「5分以上」と「10分以上」の両方の記述があるため、迷ったら長いほうに合わせます)。中身が変わっていないURLを何度も送らないことが、差分で送る理由の1つです。

1回に送れるURLは10,000件までです。このブログの規模では1回で収まりますが、スクリプトでは超えたら止まるようにしています。

Bingに届いたかは、Bing Webmaster Toolsで確かめる

送ったURLが届いたか、Bingに載ったかは、IndexNowの応答では分からないので、Bing Webmaster Toolsで確かめます。Bingの案内の手順でも、最後の4つ目が「Bing Webmaster Toolsで、URLが検索エンジンに受け取られたかを確かめる」になっています。

このブログでは、2026年10月4日の時点で、このドメインをBing Webmaster Toolsに登録済みかを確かめられていません。そのため、ここに書くのは、これから行う手順です。

  1. Bing Webmaster Toolsにサイトを追加し、持ち主の確認を済ませる。
  2. 送ったURLを、Bing Webmaster ToolsのURLの検査で1件ずつ調べ、Bingがそのページを知っているか、インデックスに載っているかを見る。
  3. docs/indexnow-log.csvの送った日時と並べて、「送った日」「Bingがクロールした日」「載った日」を記録する。

site:での検索結果の件数は目安にしかならないので、数字としては使いません。このブログでは、ボット(検索エンジンやAIのクローラー)の訪問を記録する仕組みも作ったので、本番で記録が始まったら、Bingのクローラーが送ったURLに来た日時とも照らし合わせる予定です。結果は、記録がたまってから別の記事で書きます。

会社のサイトでのIndexNowの導入や、検索とAIへの取り組みの記録は、会社のコラムbundlyzeのLLMO・SEO実践記録に載っています。株式会社bundlyzeでは、自社のサイトでIndexNowを含むLLMOの取り組みを実践しています。サイト制作の際に、サイトマップやIndexNowの送信をデプロイの流れに組み込むところから相談できます。相談の窓口はSEO・LLMO対策のページです。

動作確認した環境

確認日は2026年10月4日、使ったのは手元のMacです。

項目 バージョン・内容
OS macOS 26(Darwin 25.6.0)
Node.js 22.23.2(標準のfetchを使用。追加のパッケージなし)
相手にしたサイト https://www.yamayamabloglink.com (公開中のサイトマップsitemap-0.xmlと各URLへのHEAD)
確かめたこと --initでの記録、差分なしのとき送らないこと、増えた・変わった・消えたの判定、反映の確認(200と404)、送る中身の組み立て

DRY_RUNを付けずに動かしてIndexNowのAPIへ送ることは、この記事のためには行っていません。そのため、差分のスクリプトでの実際の応答コード、429のときの動き、前回の記録の更新は確かめていません。反映を待つ処理(30秒おきに10回)も、今回はすべて1回目で条件に合ったため、待つ場面は試していません。Bing Webmaster Toolsでの確認と、CIやデプロイの完了の知らせとのつなぎ込みも、まだ行っていません。

参照した公式ドキュメント(確認日:2026年10月4日)

  • IndexNow「Documentation」:鍵の長さと使える文字、ルートの鍵ファイルとkeyLocation、まとめて送るときのJSON、1回10,000件まで、応答コードの一覧、200は受け取っただけであること
  • IndexNow「FAQ」:送っても載るとは限らないこと、202の意味、全URLを一度に送るものではないこと(移転の直後は可)、クロールの割り当てに数えられること、送り直しは間をあけること(5分・10分の両方の記述あり)、サイトマップと併用すること
  • Bing Webmaster Tools「How to add IndexNow to your website」:4つの手順(鍵を作る・置く・送る・Bing Webmaster Toolsで確かめる)、応答コード、使い始める前に変わったURLは送らないこと、Cloudflareの Crawler Hints
  • Cloudflare Docs「Crawler Hints」:キャッシュの状態から変更を推測してIndexNowで知らせる機能、使えるプラン

よくある質問

IndexNowの鍵のファイルは、どこに置けばよいですか?

サイトのいちばん上(ルート)に「鍵の文字列.txt」という名前で置き、中身にも同じ鍵を書きます。AstroやViteのような静的サイトなら、public/ フォルダに置けば、ビルド後にサイトのルートへそのまま出ます。ルート以外に置くこともできますが、その場合は送信のたびにkeyLocationで場所を伝える必要があり、送れるURLもそのファイルがあるパスの下に限られます。

IndexNowでは、サイトマップの全URLを毎回送ってもよいですか?

IndexNowの公式のFAQでは、追加・更新・削除したURLを知らせる仕組みで、サイトの全URLを一度に送るためのものではないと説明されています。サイトの移転やリニューアルの直後に全URLを送るのはかまわないとされています。ふだんの公開では、前回送ったときのサイトマップと比べて、増えた・変わった・消えたURLだけを送ります。

IndexNowで200が返ってきたら、Bingに載ったということですか?

違います。IndexNowのドキュメントでは、200は検索エンジンがURLの一覧を受け取ったことだけを表すと書かれています。クロールするか、インデックスに載せるかは、検索エンジンがその後に決めます。載ったかどうかは、Bing Webmaster Toolsで別に確かめます。

IndexNowの送信は、Cloudflare Pagesのビルドコマンドの中で行ってもよいですか?

ビルドの途中では送らないでください。ビルドコマンドが動いている時点では、新しいページはまだ公開されていません。そのタイミングで送ると、検索エンジンが取りに来たときに古いページや404を返すおそれがあります。デプロイが終わってから、送るURLが公開側で200(消したURLなら404など)になっていることを確かめて送ります。