一句话先看: 每个页面的 canonical 必须指向自己的 URL。如果 ZH 文章的 <link rel="canonical"> 指向 EN 版,Google 就把 ZH 和 EN 当成同一个 URL,保留 EN、把 ZH 从索引里丢掉。最快的修法:在 layout 里用当前页自己的 lang 和 slug 算出 canonical,锁死末尾斜杠策略,再用 curl + view-source 验证。只要某页的 canonical 指向别处,Google 就会完全忽略它的 hreflang——这一个 bug 会悄无声息地毁掉整个双语簇。
打开 Search Console,看 Page indexing(页面索引)报告,你的 ZH 页都归在 “Alternate page with proper canonical tag” 下——Google 决定不索引它们,因为它们的 canonical 指向别处。对一篇 ZH 文章 view-source,你会看到:
<link rel="canonical" href="https://site.com/en/articles/foo/">
ZH 页把自己 canonical 到了 EN 版。对 Google 来说 ZH 和 EN 现在是同一个 URL,于是 ZH 版从索引里消失,你双语投入的一半就此隐形。
它比看上去更糟,原因在于 canonical 和 hreflang 的相互作用。按 Google 的 canonicalization 文档,rel="canonical" 是一个强信号(是提示 hint,不是硬性指令),而 hreflang 只有在每个变体都带自指 canonical 时才生效。当 Googlebot 抓到一个 canonical 指向别的 URL 的页面时,这个页面上的 hreflang 会被直接丢弃,就像它不存在一样。2026 年 LinkGraph 对多语言站点的一次审计发现,大约三分之二的站点至少有一处 canonical/hreflang 冲突,使整个簇失效——所以这是双语 SEO 最常见的崩坏方式。
原因几乎总是其一:SEO 插件被设成”用主语言版本”,或者 layout 写死了 EN URL。修法概念上很简单(每页 canonical 就是它自己的 URL),但执行需要细心改模板、加一个 build 时检查、再抽样 view-source 验证一次。
你属于哪一类?
对一篇 ZH 文章跑下面这条命令,从 canonical href 直接读出诊断:
curl -s https://site.com/zh/articles/foo/ | grep -i 'rel="canonical"'
| 你看到的 canonical href | 类别 | 根因 |
|---|---|---|
.../en/articles/foo/(语言错) | 1 或 2 | 插件设成主语言,或写死了 EN URL |
.../zh/articles/foo(无末尾 /) | 3 | canonical 与实发的末尾斜杠策略不一致 |
.../zh/articles/foo/?utm_source=... | 4 | canonical 里夹了查询串 / 片段 |
https://medium.com/...(外部主机) | 5 | 跨域 canonical 指向了联合发布的副本 |
| 没有任何输出 | 6 | canonical 标签完全缺失 |
.../zh/articles/foo/(正确) | — | canonical 没问题,接着看 hreflang |
常见原因
1. SEO 插件配成把所有翻译 canonical 到主语言
有些插件(和一些自定义 layout)有”在主语言上集中权威”选项——听起来合理,实际是错的。hreflang 已经表达了 locale 关系,canonical 应当自指。正如 Google 文档所说,非 canonical 页上的 hreflang 根本不会被采用,所以这个设置既把翻译去索引、又破坏了 hreflang。
如何判断: 对一篇翻译页 view-source,查 canonical href。
curl -s https://site.com/zh/articles/foo/ | grep -i 'rel="canonical"'
href 指向 /en/ 就是插件配错了。
2. Layout 里写死 canonical
有人在 article layout 里写了一个总是发 EN URL 的 canonical,比如 https://site.com/en/articles/<slug>/——EN 页凑合能用,ZH 页悄无声息地坏了。
如何判断: grep layout 和组件里的 canonical。
grep -rn 'rel="canonical"' src/layouts/ src/components/ src/pages/
如果 URL 不是从 Astro.url / Astro.site 或当前页 locale 推导出来的,就是写死的。
3. Canonical 末尾斜杠与实发 URL 不一致
服务器发的是 /zh/articles/foo/,但 canonical 声明 /zh/articles/foo(无 /)。Google 把它们当成两个不同 URL,跟随 canonical、索引无斜杠那个,你真实的 URL 被降权。注意:对 Google 来说,主机名之后的部分也区分大小写——/ZH/Articles/Foo/ 和 /zh/articles/foo/ 是两个不同 URL,所以大小写也必须完全一致。
如何判断: 把 canonical href 和实际发出的 URL 逐字符比对,末尾 / 和大小写都必须匹配。
4. Canonical 含查询串或片段
作者分享了一个带 ?utm_source=twitter 的 URL,那串被缓存或写死进了模板。canonical 现在带着查询串,使索引信号碎片化。Google 明确要求 canonical 是干净、绝对、无追踪参数的版本。
如何判断: canonical href 含 ? 或 #。
5. 跨域 canonical 指向再发布版
你把文章联合发布到 Medium 或 Substack。有人正确地把联合副本的 canonical 指回你站,但同时把你站自己的 canonical 指向了 Medium URL。于是你自家的页面告诉 Google”我是 Medium 的副本”,Google 可能转而索引 Medium 而不是你。
如何判断: 任何与本页主机名不符的 canonical 主机名。
6. Canonical 完全缺失
完全没有 canonical 标签。Google 会自己挑(通常会正确地自指),但一旦有 URL 变体(带/不带末尾 /、带 utm 参数、www 与非 www、http 与 https),你就失去了确定性索引,Google 可能挑错变体。
如何判断: view-source 找 rel="canonical",结果为空就是缺失。
最短修复路径
Step 1:让 canonical 每页自指
在 article layout 里,从当前页自己的 lang 和 slug 算 canonical,绝不要用固定 locale。用 new URL(...) 配合 Astro.site,让 origin 和末尾斜杠直接来自 config:
---
const { article } = Astro.props;
// Astro.site 携带 astro.config.mjs 里的 origin + base
const canonical = new URL(
`${article.data.lang}/articles/${article.data.urlSlug}/`,
Astro.site,
).href;
---
<link rel="canonical" href={canonical} />
这样就保证 ZH canonical 到 ZH、EN 到 EN。hreflang 单独告诉 Google 这些页互为 alternate——而因为现在每个变体都自指 canonical,Google 才会真正采用这些 hreflang。
Google 文档里的一个细节:带 hreflang、lang、media 或 type 属性的 rel="canonical" 在 canonicalization 时会被忽略。所以 canonical 链接保持干净(不加额外属性),把 hreflang 放进单独的 <link rel="alternate" hreflang="..."> 标签里。
Step 2:锁定末尾斜杠和大小写
在 astro.config.mjs:
import { defineConfig } from "astro/config";
export default defineConfig({
site: "https://site.com",
trailingSlash: "always",
build: { format: "directory" },
});
设 trailingSlash: "always" 加 build.format: "directory" 后,Astro 输出 foo/index.html、把 /foo/ 当作规范路径,按需路由会把无斜杠形式重定向到带斜杠形式。因为 Step 1 是用 Astro.site 构造 canonical,它会自动继承这个末尾斜杠,于是 canonical 永远和服务器实发一致。再定一套大小写策略(slug 全小写),并在各处都保持 slug 小写。
Step 3:curl + view-source 抽样验证
每种语言抽一篇、每个大类抽一篇:
for url in \
https://site.com/en/articles/foo/ \
https://site.com/zh/articles/foo/ \
https://site.com/en/articles/bar/ \
https://site.com/zh/articles/bar/
do
echo "=== $url ==="
curl -s "$url" | grep -iE 'rel="(canonical|alternate)"'
done
每页的 canonical 都应精确匹配自己的 URL。每一对的 hreflang alternate 都应互相声明(EN 列出 ZH、ZH 列出 EN,且两者都列出自己)。如果 curl 显示 canonical 正确但 Search Console 仍报错,多半是抓取数据过期——等重新抓取后再核对(Step 5)。
Step 4:加一个 postbuild 断言
在上线前抓住回退。它遍历构建出的 dist/,只要有页面 canonical 和自己路径不符就让构建失败:
# scripts/audit-canonical.mjs
import fs from "node:fs";
import path from "node:path";
const SITE = "https://site.com";
const distRoot = "dist";
let problems = 0;
function walk(dir) {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, e.name);
if (e.isDirectory()) { walk(p); continue; }
if (!p.endsWith("index.html")) continue;
const html = fs.readFileSync(p, "utf8");
const m = html.match(/<link\s+rel="canonical"\s+href="([^"]+)"/i);
if (!m) { console.error(`MISSING canonical: ${p}`); problems++; continue; }
// 从文件路径重构期望 URL
const rel = p.replace(/^dist/, "").replace(/index\.html$/, "");
const expected = `${SITE}${rel}`;
if (m[1] !== expected) {
console.error(`WRONG canonical: ${p} -> ${m[1]} (expected ${expected})`);
problems++;
}
}
}
walk(distRoot);
process.exit(problems > 0 ? 1 : 0);
接到 postbuild 步骤(例如 "postbuild": "node scripts/audit-canonical.mjs")。
Step 5:给之前被去索引的页请求重新索引
在 Search Console 里,打开 Page indexing 报告,点进 “Alternate page with proper canonical tag” 分组,canonical 修好后点 Validate Fix 让 Google 重新核查整组。对几个高优先级 URL,再用 URL Inspection 工具点 Request Indexing,催一次单页重抓。
对时间有合理预期(截至 2026 年 6 月):
- Validate Fix 验证通常要跑到大约两周,有时更久。
- Request Indexing 重抓一般在 2 到 10 天内完成,它是提示、不是保证。
- URL Inspection 工具每个资源每天约只能提交 10 到 12 个 URL,所以别想把整站都从这里推过去——批量重抓请提交 sitemap。
修法只在 Google 下次抓取时生效,所以在重抓发生前报告里不会有变化。
如何确认已修好
curl -s <zh-url> | grep -i 'rel="canonical"'返回该页自己的 ZH URL,且末尾 / 和大小写都正确。- 在 Search Console 的 URL Inspection 里,“User-declared canonical”(用户声明的 canonical)和 “Google-selected canonical”(Google 选定的 canonical)都显示该页自己的 URL(而不是 EN 孪生页)。
- EN/ZH 这一对的 hreflang alternate 互相声明(各自都列出两个变体外加自己)。
- 在接下来的一到两周里,受影响的 ZH 页从 “Alternate page with proper canonical tag” 移出,进入 Indexed(已索引)。
常见问答
“Alternate page with proper canonical tag” 一定是问题吗?
不一定。在单语言站点上它通常是良性的,只是表示 Google 找到了一个重复项(一个 ?utm 变体、一个分页、一个 AMP 副本)并正确地做了合并。只有当你想索引的页面——比如你的 ZH 翻译——被当成了另一语言页面的 alternate 时,它才是 bug。看 canonical href:如果它指向另一个 locale,那就是 bug。
为了”集中权威”,我该不该把 ZH 页 canonical 到 EN 页? 不该。这正是把翻译去索引的那个错误。翻译不是重复,而是 alternate。每个语言版本用自指 canonical,让 hreflang 去表达关系。跨语言做 canonical 还会让 Google 完全丢弃你的 hreflang。
每页都自指 canonical 后,还需要 hreflang 吗? 需要。自指 canonical 保证每页可被索引;hreflang 告诉 Google 该把哪个版本给哪类受众。两者职责不同,都要有。hreflang 必须双向——EN 指向 ZH、ZH 指回 EN,且各自包含自指。
curl 看 canonical 是对的,但 Search Console 还在报,为什么?
Search Console 显示的是 Google 上次抓取时的状态,可能比线上 HTML 滞后几天到几周。先用 curl 确认线上 HTML,再用 Validate Fix 和 Request Indexing,然后等重抓。如果 curl 已显示正确的 canonical,就别再反复改模板了。
末尾斜杠真的有这么重要?
有。对 Google 来说 /zh/articles/foo 和 /zh/articles/foo/ 是两个不同 URL,主机名之后的任何大小写差异也一样。canonical 必须和实发 URL 逐字节匹配:同协议、同主机(www 与非 www)、同大小写、同末尾斜杠策略。
像 ?utm_source 这样的查询串该怎么处理?
它们绝不属于 canonical。canonical 应当是干净、绝对、无参数的 URL。追踪参数会制造重复 URL、稀释你的索引信号——让 canonical 把它们全部指向那一个干净版本。
预防
- canonical 通过
Astro.site从当前页自己的 URL 算出,绝不写死成某一 locale。 - 末尾斜杠和大小写策略在
astro.config.mjs里锁死;canonical 与实发 URL 精确匹配。 - postbuild 审计:每页 canonical 都匹配自己的 URL,否则构建失败。
- SEO 插件(如有)配置成不集中翻译。
- canonical 里无查询串、无片段、无额外的
<link rel="canonical">属性(hreflang/lang/media/type)。 - 跨域联合发布:原站 canonical 自指;只让联合副本 canonical 回原站。
- 任何重大模板改动后,做一次抽样 view-source 检查。
相关
- Content Site Hreflang Tags Misconfigured
- Bilingual Pages Drift Apart Over Time
- Content Site Translation Pages Mismatched
- Content Site Sitemap Not Resubmitted After Big Changes
- Search Console Low Value URLs
- Content Site FAQ Schema Not Extracted
标签: #内容运营 #站点质量 #站点审计 #排查 #Canonical