双语站 canonical 错配(EN/ZH):自指 canonical + hreflang 修复路径

Search Console 收录了错误的语言版本,因为 EN/ZH 的 canonical 互相指来指去。修法:每个语言版本自指 canonical,再用 hreflang 声明彼此互为 alternate。

每篇文章都有 EN 和 ZH 两个版本。Search Console 把 /en/your-article/ 标成 “Duplicate without user-selected canonical”,或者把一种语言标成 “Alternate page with proper canonical tag” 然后去收录另一种语言。原因几乎都是:EN 模板里的 canonical 指向了 ZH URL(或反过来),又或者两个版本都把同一种语言当成了 canonical。

最快的修法: 让每个语言版本自指 canonical(/en/ 页 canonical 到 /en//zh/ 页 canonical 到 /zh/),再用 hreflang 声明这两个版本互为 alternate。这是两个相互独立的机制,把它们搞混几乎是本页所有 bug 的根源。只想要命令的话,直接跳到 最短修复路径

先别慌,澄清一点:“Alternate page with proper canonical tag” 对于那个你没有选作 canonical 的版本来说,是正确、健康的最终状态。 真正出问题的只有两种情况:选了错误的语言当 canonical;或者某个语言因为找不到清晰的自指 canonical 而显示 “Duplicate without user-selected canonical”。

canonical 与 hreflang 是什么关系

Google 先读 rel="canonical" 来挑出代表性 URL,再读 hreflang 找出这个 URL 的各语言变体。能不能跑通,取决于三个事实:

  • rel="canonical" 是一个信号(signal),不是指令(directive)。 Google 文档把它描述为 “a strong signal that the specified URL should become canonical”,但如果你的 canonical 指向了页面自身语言之外的地方,或者跟其它信号冲突,Google 可能会另选一个 URL。(参见 How Google chooses canonical URLs。)
  • hreflang 需要互相回指(reciprocal return tags)。 Google 文档原话:“Each language version must list itself as well as all other language versions”,以及 “if two pages don’t both point to each other, the tags will be ignored”。一个跨语言指向的 canonical,会悄无声息地把整个 hreflang 簇搞废。
  • 非 canonical 页上的 hreflang 会被直接忽略。 Google 的明文规则是要 “specify a canonical page in the same language”,也就是 canonical 必须指向同语言的页面。当一个页面 canonical 到另一个 URL 时,Google 会当作这个页面上的 hreflang 不存在——canonical 与 hreflang 这两个信号必须一致,Google 才会信任这个簇。

所以双语站的规则是:canonical = 页面自己的 URL;hreflang = 全部语言版本的集合,每一页都列出自己和其它语言。

你属于哪一类?

Search Console 里的症状可能的原因去哪里查
收录了错误的语言,另一个显示 “Duplicate without user-selected canonical”两个版本吐出了同一个 canonical对比两个 URL 的 curl canonical 输出
两个版本都 canonical 到同一个 URLcanonical 是从 translationKey 推的,不是从页面 URLlayout 里的 canonical helper
ZH 页 canonical 到 /en/...写死了 /en/ 前缀,或默认 locale 回退在模板里搜写死的 /en/
随机几个页面错、其它正常渲染时 locale 变量取错了在 helper 里打印 locale 与页面 lang
爬虫报 hreflang “no return tags”canonical 跨语言指向,破坏了互指两个页面的 <head>
所有 canonical 全错、每一页都错astro.config.mjs 里的 site 跟生产环境不一致astro.config.mjs

常见原因

按命中率从高到低排列。

1. 模板写死了一个 canonical

// 错
<link rel="canonical" href="https://site.com/en/article" />

EN 和 ZH 版本都吐出同一个 canonical。Google 看到 ZH 指向 EN,去重后只收 EN。

怎么判断

curl -s https://site.com/zh/article/ | grep -i canonical
curl -s https://site.com/en/article/ | grep -i canonical

两个返回同一个 URL,就是写死了。

2. 把 translationKey 当 canonical 用错

有些 Astro / Next 模板用共享的 translationKey 来关联各语言版本,结果不小心也拿它来推 canonical。两个版本就都按这个 key 指向同一个 URL。

怎么判断:看 layout 里 canonical 是怎么推导的。如果用的是 translationKey 而不是页面实际的完整 URL(含 locale 前缀),就是这一条。

3. canonical 默认回退到默认语言的 URL

// 错
<link rel="canonical" href={`https://site.com/en/${slug}/`} />

/en/ 是写死的,所以 ZH 页拿到的 canonical 也指向 EN URL。

怎么判断:在模板里搜 canonical 生成逻辑里有没有硬编码的 /en//zh/

4. canonical 带了 locale 参数,但 locale 取错了

off-by-one 错误:ZH 页从某个全局变量里读到了 locale = 'en',于是 canonical 就错了。

怎么判断:在 canonical helper 里加日志,对比渲染时拿到的 locale 值与页面真实的 lang。

5. canonical 与 hreflang 冲突(互指断裂)

canonical 写的是 /en/article/hreflang="zh" 指向 /zh/article/,但 ZH 页本身却没有自指 canonical。Google 看到冲突,互指校验失败,于是它自己挑一个版本(常常挑错那个)。

怎么判断:ZH 页的 <head> 里应该正好是下面这样(注意自指 canonical 加上完整的互指集合):

<link rel="canonical" href="https://site.com/zh/article/" />
<link rel="alternate" hreflang="en" href="https://site.com/en/article/" />
<link rel="alternate" hreflang="zh" href="https://site.com/zh/article/" />
<link rel="alternate" hreflang="x-default" href="https://site.com/en/article/" />

EN 页镜像这一套,但把 EN 设为自己的 canonical。两个页面必须列出同样的 hreflang 集合,而且每一页都要列出自己,否则整个簇会被忽略。

6. Astro site 配置的 base URL 不对

astro.config.mjssite 写错了,所有从它推导出来的 canonical 就都错了。

怎么判断

// astro.config.mjs
export default defineConfig({
  site: 'https://site.com', // 必须跟生产环境完全一致,含 https、结尾不带路径
});

然后 curl -s https://site.com/ | grep canonical 的输出应该跟 site 完全匹配。

最短修复路径

第 1 步:确认每个版本都自指 canonical

对每一对 EN/ZH 文章:

for lang in en zh; do
  echo "=== /$lang/article/ ==="
  curl -s "https://site.com/$lang/article/" | grep -i canonical
done

预期:

=== /en/article/ ===
<link rel="canonical" href="https://site.com/en/article/" />

=== /zh/article/ ===
<link rel="canonical" href="https://site.com/zh/article/" />

两个都必须自指。如果有一个指向另一个,那就是这个 bug。

第 2 步:修模板

在 layout 里(以 Astro 为例),从真实的请求 URL 推导 canonical,绝不要从写死的前缀或共享 key 推:

---
const url = new URL(Astro.url.pathname, Astro.site).toString();
const lang = Astro.params.lang || 'en';
const translationKey = Astro.props.frontmatter?.translationKey;
const altLang = lang === 'en' ? 'zh' : 'en';
---
<link rel="canonical" href={url} />
<link rel="alternate" hreflang={lang} href={url} />
<link rel="alternate" hreflang={altLang} href={`${Astro.site}${altLang}/${translationKey}/`} />
<link rel="alternate" hreflang="x-default" href={`${Astro.site}en/${translationKey}/`} />

注意页面在 hreflang={lang} 里也列出了自己——这个自我引用就是 Google 要求的那个 return tag。

第 3 步:构建期校验

加一个 prebuild 脚本,让错误的 canonical 再也无法上线:

import fs from 'node:fs';
import path from 'node:path';
import { parse } from 'node-html-parser';

const dist = 'dist';
let errors = 0;

function walk(dir) {
  for (const f of fs.readdirSync(dir)) {
    const p = path.join(dir, f);
    if (fs.statSync(p).isDirectory()) walk(p);
    else if (p.endsWith('.html')) {
      const html = fs.readFileSync(p, 'utf8');
      const root = parse(html);
      const canonical = root.querySelector('link[rel="canonical"]')?.getAttribute('href');
      const expected = 'https://site.com/' + p.replace(/^dist\//, '').replace(/index\.html$/, '');
      if (canonical !== expected) {
        console.error(`MISMATCH: ${p}\n  canonical: ${canonical}\n  expected:  ${expected}`);
        errors++;
      }
    }
  }
}

walk(dist);
process.exit(errors ? 1 : 0);

第 4 步:用爬虫验证 hreflang 互指

重要,截至 2026 年 6 月:Search Console 已经没有 hreflang 报告了。 旧的 International Targeting 报告已于 2022 年 9 月 22 日弃用并移除,现在 Google 在控制台里任何地方都不再呈现站点级 hreflang 错误(Google 仍然在用 hreflang,只是不再就此报告了)。改用爬虫来验证互指——Screaming Frog(Reports -> Hreflang -> “Non-Reciprocal” / “Missing Return Links”)、Sitebulb,或 Ahrefs Site Audit。你要的检查是:A 页列出的每个 alternate,都必须回头列出 A 页;并且每个页面都要列出自己。

第 5 步:在 Search Console 里逐一检查 URL

在 Search Console 里打开 URL Inspection,提交几个之前配错的 ZH URL。Live test 会显示 “Google-selected canonical” 与 “User-declared canonical”——现在这两者应该一致,并且都指向页面自己的 URL。点 Request indexing。状态会从 “Duplicate without user-selected canonical” 变为 “Indexed”(而那个非 canonical 的孪生页会正确地显示 “Alternate page with proper canonical tag”)。重新收录通常需要几天到约两周。

第 6 步:重新提交 sitemap

如果你的 sitemap 是用坏掉的 canonical 生成的,那就重新生成并重新提交。sitemap 里每个 URL 应该有一条记录——EN 和 ZH 版本各自作为独立的条目出现——而且每个 <url> 可以带上 xhtml:link rel="alternate" 的 hreflang 注解,作为页面内标签的一种替代方案。(三选一即可:按 Google 的说法,HTML 标签、HTTP 头、sitemap 注解三种方式等价,三种全上只是更难维护。)

怎么确认已经修好了

跑下面这个合并检查,所有几行都应为真:

for lang in en zh; do
  url="https://site.com/$lang/article/"
  echo "== $url =="
  curl -s "$url" | grep -iE 'rel="canonical"|hreflang'
done

对每个页面,你应该看到:一个等于该页面自身 URL 的 canonical、一行对应自身语言的 hreflang、一行对应另一种语言的 hreflang,以及一个 x-default。在 Search Console 的 URL Inspection 里,“Google-selected canonical” 应该等于 “User-declared canonical”。爬虫应报告零个 “missing return links”。

FAQ

EN 是我的主语言,ZH 页是不是应该 canonical 到 EN 页? 不应该。每个语言版本都必须自指 canonical。把 ZH canonical 到 EN,等于告诉 Google ZH 页是 EN 的副本,于是 ZH 页会掉出索引,永远无法在中文搜索里排名。要表达语言之间的关系,用 hreflang,不要用 canonical。

“Alternate page with proper canonical tag” 是我需要修的错误吗? 通常不是——对于一个重复集合里 canonical 的那个版本来说,这就是预期状态。只有当选了错误的语言当 canonical 时它才要紧。对两个真正不同的语言页面(各自都自指 canonical)来说,两个最终都应该是 “Indexed”。

为什么 Google 完全无视我的 hreflang 标签? 最常见的原因是 return tags 不互指:A 页列了 B 页,但 B 页没有回头列 A 页;或者某个页面没有列出自己。Google 文档明确说,不匹配的标签会被忽略。跨语言的 canonical 也会破坏这一点,因为 canonical 与 hreflang 这两个信号必须先一致,Google 才会信任这个簇。

Search Console 里的 hreflang 报告去哪了? 它属于 International Targeting 报告,Google 已于 2022 年 9 月 22 日弃用并移除。截至 2026 年 6 月,Search Console 里已没有 hreflang 报告(Google 仍会读取 hreflang,只是不再就此报错)。单个页面用 URL Inspection 查,站点级互指检查用爬虫(Screaming Frog、Sitebulb、Ahrefs)。

修好之后,错误的 canonical 多久能清掉? 部署并 Request indexing 之后,高价值 URL 大概几天到约两周;爬取频率低的页面可能更久。重新提交 sitemap、并给之前被错收录的页面织一些站内链接,能加快这个过程。

预防

  • 模板原则:canonical = 页面自己的完整 URL,永远如此,没有例外。
  • 把 canonical 和 hreflang 当成两个独立机制——canonical 表达”这个 URL 的这个版本是正版”;hreflang 表达”这个 URL 还有其它语言版本”。
  • 加 CI 校验:每个渲染出来的 HTML 的 canonical 都要等于它实际提供服务的那个路径。
  • 不要从 translationKey 或任何共享标识符推 canonical——只从 URL 推。
  • 确保每个页面在 hreflang 里都列出自己(这个自我引用就是 return tag)。
  • 上新 locale 时,先在一篇样本文章上测试 canonical / hreflang 的输出,再批量铺开。

相关阅读

标签: #排查 #SEO #排查 #Canonical #双语