IndexNow 推送返回 200,Bing 却还是搜不到:一次完整排查记录
这是一篇踩坑记录,不是教程。所有命令和脚本都在我们自己的站点上跑过,能复现。
起因
公司官网是 Next.js,部署完新页面后我希望搜索引擎能尽快发现。以前的做法是等 sitemap 被定期抓取,慢的时候一两周。查资料时看到 IndexNow,官方说法是"主动通知搜索引擎你的页面变了",接入成本看起来只有一个 HTTP 请求,就接了。
接完当天,推送接口返回 200,我以为是搞定了。
一周后我在 Bing 里搜 site:我们的域名,0 条结果。
推送全成功,收录全没有。这篇记录的就是中间那一周我在排查什么。
先把 IndexNow 是什么讲清楚
很多文章把 IndexNow 说成"让搜索引擎立刻收录",这个说法不准确。它做的事只有一件:
你告诉参与方:"这几个 URL 内容变了,你们可以重新抓一次。"
它不保证抓取,更不保证收录。返回 200 的语义是"我收到通知了",不是"我收录了"。
另外两件容易被忽略的事:
- Google 明确不参与 IndexNow。 这个协议是微软和 Yandex 主导的,参与方主要是 Bing、Yandex、Seznam、Naver。如果你指望靠它让 Google 加速,方向是错的。
- 它和
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 混为一谈
排查过程中我一度想"既然都要给机器读,是不是一起处理",后来发现这两件事的目标客户端完全不一样:
| IndexNow | llms.txt | |
|---|---|---|
| 面向 | 传统搜索引擎的索引系统 | 大模型及其抓取/检索流程 |
| 机制 | 你主动 POST 通知 | 对方主动来取 |
| 结果 | 可能被重新抓取 | 可能被读取并作为素材 |
| 是否有标准 | 有明确协议 | 非官方约定,各家实现不一致 |
| 能否验证 | 可看索引状态 | 目前没有可靠的验证手段 |
最后一行是重点:llms.txt 到现在也没有任何一家公开承诺"我读了它并且会因此改变输出"。所以它的正确定位是降低解析成本的辅助文件,不是"让 AI 推荐我"的开关。任何把它说成后者的说法,都值得怀疑。
我把站内 llms.txt 从 3000 多字符补到 5000 多字符,是因为它能更清楚地交代主体、服务范围和联系方式,这件事本身有意义;但我不会把它和"AI 会不会引用我"挂钩,那是另一套机制。
小结
- 返回 200 只代表收到通知,不代表抓取,更不代表收录
- Google 不参与 IndexNow
- 推送 URL 必须和页面 canonical 逐字一致
- robots.txt 挡住的 URL,推送了也不会抓
- 去日志里确认 bingbot 是否真的来过,这比改推送代码有用
- key 文件会被 CDN 缓存失效,纳入发布后检查
- 别指望当天生效,也别把 llms.txt 当成同一件事
如果你也在做类似的接入,欢迎在评论区说说你遇到的情况,尤其是我没覆盖到的失败模式。
浙公网安备 33010602011771号