IndexNow 推送返回 200,Bing 却还是搜不到:一次完整排查记录

 

这是一篇踩坑记录,不是教程。所有命令和脚本都在我们自己的站点上跑过,能复现。

起因

公司官网是 Next.js,部署完新页面后我希望搜索引擎能尽快发现。以前的做法是等 sitemap 被定期抓取,慢的时候一两周。查资料时看到 IndexNow,官方说法是"主动通知搜索引擎你的页面变了",接入成本看起来只有一个 HTTP 请求,就接了。

接完当天,推送接口返回 200,我以为是搞定了。

一周后我在 Bing 里搜 site:我们的域名,0 条结果。

推送全成功,收录全没有。这篇记录的就是中间那一周我在排查什么。

先把 IndexNow 是什么讲清楚

很多文章把 IndexNow 说成"让搜索引擎立刻收录",这个说法不准确。它做的事只有一件:

你告诉参与方:"这几个 URL 内容变了,你们可以重新抓一次。"

它不保证抓取,更不保证收录。返回 200 的语义是"我收到通知了",不是"我收录了"。

另外两件容易被忽略的事:

  1. Google 明确不参与 IndexNow。 这个协议是微软和 Yandex 主导的,参与方主要是 Bing、Yandex、Seznam、Naver。如果你指望靠它让 Google 加速,方向是错的。
  2. 它和 llms.txt 没有任何关系。 我见过不少地方把这两件事混在一起说。IndexNow 面向的是传统搜索引擎的索引流程,llms.txt 面向的是大模型抓取器,两套机制、两拨客户端,互不感知。后面会单独讲。

第一步:接入(这部分很简单)

生成一个 key,要求是 8–128 位的 [a-zA-Z0-9-]:

openssl rand -hex 16

把 key 原文(不含换行以外的任何字符)写成一个文件,放在网站根目录:

https://www.example.com/7f3a...yourkey...c91.txt

文件内容就是 key 本身。两个细节:

  • Content-Type 必须是 text/plain。Next.js 放在 public/ 下的 .txt 通常自动就是这个类型,但如果你的 Nginx 或者 CDN 改了默认行为,要单独确认。
  • 文件末尾不要有多余换行。有些编辑器会自动加一个,Bing 校验时会认为 key 不匹配。

推送请求:

curl -X POST https://api.indexnow.org/indexnow \
  -H 'Content-Type: application/json; charset=utf-8' \
  -d '{
    "host": "www.example.com",
    "key": "7f3a...yourkey...c91",
    "keyLocation": "https://www.example.com/7f3a...yourkey...c91.txt",
    "urlList": [
      "https://www.example.com/pricing",
      "https://www.example.com/services/geo-managed-service"
    ]
  }'

单次最多 10000 个 URL。返回码:

状态码含义
200 已接收(不等于收录)
400 / 422 请求格式或参数有问题
403 key 无效,通常是 key 文件校验不过
429 限流

到这里都很顺利,我的推送全部 200。

第二步:一周后 site 查询 0 结果

site: 语法各家口径不一样,Bing 的结果尤其保守,所以我先在 Bing Webmaster Tools 里用 URL Inspection 单独查了一个 URL —— 返回 "URL not found in index"。

推送成功、页面存在、HTTP 200,但没进索引。下面是我逐条排查的顺序,也是这篇文章真正的部分。

1. 推送的 URL 和页面的 canonical 是不是完全一致

这是最常见的原因,也最容易忽略。IndexNow 通知的是一个字符串,如果它和页面自己声明的 canonical 不一致,引擎会认为"这是两个页面",通知就作废了。

逐字比对这三件事:

推送的 URL      https://www.example.com/pricing
页面 canonical  https://www.example.com/pricing
实际响应        200 且 Location 为空

容易出问题的地方:www 有无、结尾有没有 /、http 和 https、大小写、以及 Next.js 的 trailingSlash 配置改过之后 sitemap 和 canonical 没同步。

我的排查脚本:

#!/usr/bin/env bash
set -euo pipefail
url="$1"
canonical=$(curl -sSL "$url" | grep -o '<link[^>]*rel="canonical"[^>]*>' | grep -o 'href="[^"]*"' | sed 's/href="//;s/"$//')
echo "pushed:    $url"
echo "canonical: ${canonical:-<none>}"
[ "$url" = "$canonical" ] && echo "OK" || echo "MISMATCH"

2. robots.txt 有没有挡住

这一步我差点漏掉。sitemap 里新增的页面正好落在一条比较宽的 Disallow 规则下面,人类访问完全正常,爬虫直接被拒。

curl -s https://www.example.com/robots.txt

别只看有没有 Disallow: /,要看具体路径规则。另外 Bing 有个特点:robots.txt 不允许抓取的 URL,IndexNow 推送了也不会抓。

顺手把 AI 抓取器的名单也一并检查了。这些 user-agent 各家会调整,以官方文档为准,我当时核对的是:

抓取器user-agent用途
Bingbot bingbot Bing 索引
GPTBot GPTBot OpenAI 训练
OAI-SearchBot OAI-SearchBot ChatGPT 搜索
ChatGPT-User ChatGPT-User 用户触发的即时抓取
ClaudeBot ClaudeBot Anthropic 抓取
PerplexityBot PerplexityBot Perplexity 索引
Bytespider Bytespider 字节系
Applebot-Extended Applebot-Extended Apple 训练开关
CCBot CCBot Common Crawl

注意 Applebot-Extended 和 Google-Extended 这类不是爬虫,是训练数据的开关 token,写在 robots.txt 里控制的是"能不能用于训练",和"能不能抓页面"是两件事。

3. 页面是不是真的 200

有几种情况 curl 看起来正常,但引擎不认:

  • 返回 200,但内容是 JS 渲染后才有正文,且引擎没执行脚本
  • 软 404:页面存在、状态码 200,但内容是"页面不存在"
  • 被 CDN 按地区返回了不同内容(我们踩过一次,CDN 缓存了一份旧版错误页)

用不执行 JS 的方式看一眼真实返回体:

curl -sSL -A "Mozilla/5.0 (compatible; bingbot/2.0)" https://www.example.com/pricing | head -c 2000

4. 有没有登录墙、地域限制或频率限制

服务器如果对方爬虫的 IP 段有限流(比如按 UA 或者按 ASN 拦过一波恶意流量),很容易误伤 bingbot。查一下 Nginx 日志里到底有没有 bingbot 来过:

grep -i bingbot /var/log/nginx/access.log | tail -20
grep -i bingbot /var/log/nginx/access.log | awk '{print $1}' | sort -u | head

没有 bingbot 的访问记录,就说明前面的推送根本没转化成抓取,这时候改推送代码是没用的。

5. Bing Webmaster Tools 里的提交配额

IndexNow 在 Bing 侧走的是 URL submission 配额。配额用完之后推送依然返回 200,但不会真的排队。这个坑比较隐蔽:接口不报错,只是静默不处理。

在 Bing Webmaster Tools 的 URL Submission 页面能看到剩余配额。

6. 时间

这个我只能给单次观察,不能当结论:修正完 canonical 和 robots 之后,第一批 URL 大约在第 4 天出现在 URL Inspection 里,site: 能查到又晚了两三天。

不同站点差异很大,别把这个数字当成预期。能确定的是:当天推送、当天收录这个预期是不现实的。

第三步:把校验做成发布流程的一部分

排查完之后我做了两件事,避免下次再靠人肉发现。

一是在部署脚本里加推送 + 校验。 推完之后立刻回读一次,不一致就报错:

// scripts/indexnow-push.mjs
const HOST = process.env.SITE_HOST;
const KEY = process.env.INDEXNOW_KEY;

async function push(urls) {
  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: KEY,
      keyLocation: `https://${HOST}/${KEY}.txt`,
      urlList: urls,
    }),
  });
  return { status: res.status, body: await res.text() };
}

async function canonicalOf(url) {
  const html = await (await fetch(url)).text();
  const m = html.match(/<link[^>]*rel="canonical"[^>]*href="([^"]+)"/);
  return m?.[1] ?? null;
}

for (const url of process.argv.slice(2)) {
  const { status } = await push([url]);
  const canonical = await canonicalOf(url);
  const ok = status === 200 && canonical === url;
  console.log(`${ok ? "OK  " : "FAIL"} ${status} ${url}${canonical === url ? "" : ` -> canonical=${canonical}`}`);
  if (!ok) process.exitCode = 1;
}

二是把 key 文件本身也纳入发布后检查。 key 文件被 CDN 缓存成 404、或者被某次发布覆盖掉,是很容易发生的事:

curl -sS -o /dev/null -w '%{http_code} %{content_type}\n' "https://www.example.com/$KEY.txt"
curl -sS "https://www.example.com/$KEY.txt" | tr -d '\n' | wc -c   # 应等于 key 长度

最后:别把 IndexNow 和 llms.txt 混为一谈

排查过程中我一度想"既然都要给机器读,是不是一起处理",后来发现这两件事的目标客户端完全不一样:

 IndexNowllms.txt
面向 传统搜索引擎的索引系统 大模型及其抓取/检索流程
机制 你主动 POST 通知 对方主动来取
结果 可能被重新抓取 可能被读取并作为素材
是否有标准 有明确协议 非官方约定,各家实现不一致
能否验证 可看索引状态 目前没有可靠的验证手段

最后一行是重点:llms.txt 到现在也没有任何一家公开承诺"我读了它并且会因此改变输出"。所以它的正确定位是降低解析成本的辅助文件,不是"让 AI 推荐我"的开关。任何把它说成后者的说法,都值得怀疑。

我把站内 llms.txt 从 3000 多字符补到 5000 多字符,是因为它能更清楚地交代主体、服务范围和联系方式,这件事本身有意义;但我不会把它和"AI 会不会引用我"挂钩,那是另一套机制。

小结

  • 返回 200 只代表收到通知,不代表抓取,更不代表收录
  • Google 不参与 IndexNow
  • 推送 URL 必须和页面 canonical 逐字一致
  • robots.txt 挡住的 URL,推送了也不会抓
  • 去日志里确认 bingbot 是否真的来过,这比改推送代码有用
  • key 文件会被 CDN 缓存失效,纳入发布后检查
  • 别指望当天生效,也别把 llms.txt 当成同一件事

如果你也在做类似的接入,欢迎在评论区说说你遇到的情况,尤其是我没覆盖到的失败模式。

posted @ 2026-10-13 18:30  yangyuyu  阅读(4)  评论(0)    收藏  举报