Google 说我的页面'被 robots.txt 屏蔽'?——别急着改代码,先用 URL Inspection API 查官方判定
我们的站点是中英双语架构:URL 带 /zh/、/en/ 前缀,每篇文章只有一种语言,中英对版是两条独立记录、两个独立 slug。前几天收到反馈:
https://example.com/en/articles/guide-cron-20260304会自动跳转,Google 说没有收录,已被 robots.txt 屏蔽。
紧接着一个架构级的提议:既然英文前缀会触发跳转,是不是应该升级路由——让 zh/en 前缀只控制界面语言,文章不再自动跳转?
如果当时直接照做,我们就会为一个不存在的问题推翻一套教科书级正确的 SEO 架构。这篇文章复盘完整排查过程:三个小坑、一个关键方法论、一次架构取舍讨论。
第一步:核对 robots.txt——矛盾出现
"被 robots.txt 屏蔽"是个可以立刻证伪的判定。我们的 robots 规则由 Next.js 的 robots.ts 生成,屏蔽列表只有这些:
const disallow = [
"/api/",
// 同时列出带/不带尾斜杠两种形态:"/*/dashboard/" 拦不住 "/en/dashboard" 本身
"/*/dashboard",
"/*/dashboard/",
"/*/auth/",
"/*/tweets",
"/*/tweets/*",
];
/en/articles/guide-cron-20260304 不匹配任何一条。拉取线上 robots.txt 逐行核对,结论一致:robots.txt 不可能拦这个 URL。
到这里就出现了排查中最有价值的信号——转述的结论和一手证据对不上。这时候不要试图"调和"两边(比如猜"也许 Google 缓存了旧版 robots.txt"),而是去找更权威的一手数据源。
第二步:curl 探测——先踩了自己埋的坑
想看这个 URL 到底怎么跳转,最直接的是 curl:
$ curl -sI "https://example.com/en/articles/guide-cron-20260304"
HTTP/2 403
403?页面浏览器里明明能打开。
原因是我们自己几个月前部署的反爬 middleware:非浏览器 User-Agent(curl、python-requests、HeadlessChrome 等)会被直接拒绝。测自己的站,先想想自己给自己设过什么门禁。 带上浏览器 UA 再测:
$ curl -sI -A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ... Chrome/127.0" \
"https://example.com/en/articles/guide-cron-20260304"
HTTP/2 308
location: https://example.com/en/articles/guide-cron-20260304-en
跳转链路清楚了:这是一篇中文文章的 slug,用 /en/ 前缀访问时,服务端发现它有已发布的英文对版 guide-cron-20260304-en,于是 308 永久重定向过去。这正是设计行为,对应的代码:
// 跨 locale 访问 → 301 永久重定向
// 1. 文章有对版且对版已发布 → 重定向到对版 URL(用户看到自己语言的对应内容)
// 2. 否则 → 重定向到原文真实 locale 的 URL(用户看到原文)
if (requestLocale !== articleLocale) {
if (article.translationSlug) {
const statusResult = await getArticleStatusBySlug(article.translationSlug);
if (statusResult.code === 0 && statusResult.data?.status === "published") {
permanentRedirect(
buildLocalizedUrl(requestLocale, `/articles/${article.translationSlug}`),
);
}
}
permanentRedirect(
buildLocalizedUrl(articleLocale, `/articles/${article.slug}`),
);
}
行为符合预期。那 Google 那边到底怎么判定的?
第三步(关键):别看界面转述,直接问 Google——URL Inspection API
Search Console 的界面报告有两个天然的失真源:
- 分组名相近。"未编入索引"下面有十几种原因分组:'网页会自动重定向'、'已被 robots.txt 屏蔽'、'已抓取但尚未编入索引'……人工转述时很容易串行。
- 数据是快照。界面报告更新有延迟,看到的可能是几周前的状态。
而 URL Inspection API 返回的是 Google 索引库里对这个 URL 的当前官方判定,字段是结构化的,没有翻译损耗。一个可复用的最小脚本(Node.js + Service Account):
// gsc-inspect-url.ts —— 查询任意 URL 的 Google 官方收录判定
// 用法:npx tsx gsc-inspect-url.ts <url>
import { GoogleAuth } from "google-auth-library";
const url = process.argv[2];
async function main() {
const auth = new GoogleAuth({
keyFile: ".secrets/gsc-sa-key.json", // Service Account 密钥
scopes: ["https://www.googleapis.com/auth/webmasters.readonly"],
});
const client = await auth.getClient();
// 注意:用 client.request()(gaxios),不要用 Node 原生 fetch——
// 原生 fetch 不遵守 https_proxy 环境变量,代理环境下会 ConnectTimeout
const res = await client.request({
url: "https://searchconsole.googleapis.com/v1/urlInspection/index:inspect",
method: "POST",
data: {
inspectionUrl: url,
// 必须与 GSC 属性类型严格一致:
// URL-prefix 属性 → "https://example.com/"(带尾斜杠)
// Domain 属性 → "sc-domain:example.com"
// 填错会报 "You do not own this site"
siteUrl: "https://example.com/",
},
});
console.log(JSON.stringify(res.data, null, 2));
}
main().catch((e) => { console.error(e); process.exit(1); });
配置只有三步:GSC 后台把 Service Account 邮箱加为属性用户(完整权限)、下载 SA 密钥、装 google-auth-library。脚本里埋着两个我们实际踩过的坑,都写在注释里了:
- 坑 A:Node 原生 fetch 不认代理。
fetch底层是 undici,不读https_proxy环境变量;在需要代理访问 Google API 的网络环境下会一直ConnectTimeout。换成 google-auth-library 自带的client.request()(底层 gaxios)即可。 - 坑 B:
siteUrl必须和属性类型精确匹配。 我们第一次填了sc-domain:example.com,报错 "You do not own this site"——实际属性是 URL-prefix 类型,必须写成https://example.com/,尾斜杠都不能少。不确定时先调GET /webmasters/v3/sites列出 SA 有权限的属性。
判定结果:不是 robots.txt,是"网页会自动重定向"——而这是正常态
对出问题的 URL 跑一遍:
{
"indexStatusResult": {
"verdict": "NEUTRAL",
"coverageState": "Page with redirect",
"robotsTxtState": "ALLOWED",
"indexingState": "INDEXING_ALLOWED",
"pageFetchState": "SUCCESSFUL",
"googleCanonical": "https://example.com/en/articles/guide-cron-20260304-en",
"referringUrls": [
"https://example.com/en/articles/guide-cron-20260304-en",
"https://example.com/en/articles"
]
}
}
三个字段直接终结了"robots.txt 屏蔽"的假设:
robotsTxtState: "ALLOWED"—— robots.txt 没有拦它,Google 亲口说的;coverageState: "Page with redirect"—— 真实分组是"网页会自动重定向";googleCanonical指向-en英文对版 —— Google 完全理解了我们的意图。
再查跳转目标 guide-cron-20260304-en:
{
"verdict": "PASS",
"coverageState": "Submitted and indexed",
"sitemap": ["https://example.com/sitemap.xml"]
}
内容根本没有丢收录。 英文文章在它自己的 URL 上"已提交并编入索引",富结果校验也通过。GSC 把重定向源 URL 列在"未编入索引"里,只是如实陈述"这个 URL 跳到了别处,我收录了目标页"——这不是故障告警,是系统按设计工作的日志。
那要不要改成"不跳转"?——多语言架构的 SEO 取舍
回到最初的提议:zh/en 前缀只控制界面语言,/en/ 前缀访问中文文章时直接渲染、不再 301。从 SEO 角度这是个负优化,三个理由:
1. 制造重复内容。 同一篇中文文章会在 /zh/articles/x 和 /en/articles/x 两个 URL 下都返回 200。即使 canonical 都指向 /zh/,也是在让 Google 抓双倍页面、再靠推断合并——canonical 是"建议",301 是"指令",信号强度差一个量级。
2. 污染语言信号。 URL 前缀声明 en,html lang 和正文却是中文,hreflang 体系里出现自相矛盾的数据。多语言 SEO 的全部前提是"每个 URL 的语言身份唯一且自洽"。
3. 现状就是 Google 官方推荐的架构。 一语言一 URL、自引用 canonical、双向 hreflang、跨语言访问 301 到对版——我们逐项核对了两个页面的实际输出:
<!-- 中文页 /zh/articles/guide-cron-20260304 -->
<link rel="canonical" href="https://example.com/zh/articles/guide-cron-20260304"/>
<link rel="alternate" hreflang="zh-CN" href="https://example.com/zh/articles/guide-cron-20260304"/>
<link rel="alternate" hreflang="en" href="https://example.com/en/articles/guide-cron-20260304-en"/>
<!-- 英文页 /en/articles/guide-cron-20260304-en -->
<link rel="canonical" href="https://example.com/en/articles/guide-cron-20260304-en"/>
<link rel="alternate" hreflang="en" href="https://example.com/en/articles/guide-cron-20260304-en"/>
<link rel="alternate" hreflang="zh-CN" href="https://example.com/zh/articles/guide-cron-20260304"/>
canonical 有个容易做错的细节:它必须指向文章真实语言的 URL,与请求路径无关。用户拿错误前缀访问时,metadata 里的 canonical 和 301 的目标保持一致,Google 收到的两路信号互相印证:
// canonical 必须指向文章真实 locale 的 URL,与 request locale 无关。
// 即使用户访问了错误 locale 的 URL,页面也会 301 到真实 locale;
// 此时 metadata 的 canonical 和 301 目标保持一致,信号最干净。
const canonicalUrl = buildLocalizedUrl(articleLocale, `/articles/${article.slug}`);
还有一条防御性规则:hreflang 只在对版"已发布"时才声明(sitemap 和页面 metadata 双重校验)。对版还是草稿时就声明 alternate,等于给 Google 递一个会 301 回源或 404 的 URL,是主动送出去的错误信号。
结论:架构不动。 唯一值得做的是下一节。
意外收获:referringUrls 暴露了一个真问题
API 返回的 referringUrls 字段告诉你 Google 是从哪里发现这个 URL 的。上面的结果显示,它是从 /en/articles 列表页链过来的——说明历史上某个版本的英文列表页,曾经输出过"英文前缀 + 中文 slug"的内部链接。
内部链接指向一个注定要 301 的 URL,虽然不致命(Google 会跟随),但每个这样的链接都在浪费一次跳转、稀释一点权重。正确姿势是内链永远直指最终 URL。我们实测当前线上列表页已经不再输出这种链接(文章列表和相关文章推荐都按 locale 过滤),所以这些 redirect 条目会随 Google 重抓自然从报告里淡出,无需额外处理。
顺带一提,referringUrls 也是排查"Google 为什么会抓到这个奇怪 URL"类问题的最短路径——比在全站代码里 grep 链接快得多。
方法论沉淀
- "Google 说"要拿到 Google 的原话。 GSC 界面分组名相近、数据有延迟、人工转述会失真。收录疑问一律走 URL Inspection API,看
coverageState/robotsTxtState/googleCanonical三个结构化字段,五分钟拿到官方判定。 - 转述与一手证据矛盾时,升级数据源而不是调和矛盾。 本例中 robots.txt 核对结果与反馈直接冲突,正确动作是去找更权威的判定(API),而不是给"也许缓存了旧 robots.txt"这类猜想打补丁。
- "未编入索引"不全是坏消息。 "Page with redirect"、"Alternate page with proper canonical tag" 这类分组是系统按设计工作的日志。改架构之前先确认:内容本体(canonical 目标)是否已收录?是,就什么都不用做。
- 测自己的站点时,记住自己设过的门禁。 反爬 UA 黑名单、地域封锁、登录墙——curl 返回的 403 不一定是新问题,可能是几个月前的自己。
- 多语言站的铁律:一语言一 URL。 跨语言访问用 301 收敛,canonical 指向真实语言版本,hreflang 只声明已发布的对版,内链直指最终 URL。任何"同一内容多 URL 返回 200"的设计,无论动机多合理,都是在给搜索引擎出难题。