双语站 canonical 错配:3 个原因 + 修复路径

双语 / 多 locale 页 canonical 指错方向。常见原因:模板写死一个 canonical;translationKey 逻辑误作 canonical;默认 canonical fall back 到默认语言 URL。先做:每篇:canonical 等于自己 URL。

每篇文章都有 EN 和 ZH 版本。Search Console 把 /en/your-article/ 标成 “Duplicate without user-selected canonical”,或更糟,“Alternate page with proper canonical tag”——结果只收录了 ZH 版。原因几乎都是:EN 模板里 canonical 指向了 ZH URL(或反过来),或者两个版本都把同一种语言当 canonical。多语言站的规则很简单:每个语言版本自指 canonical,hreflang 声明互为 alternate。这块的模板 bug 大多来自混淆这两个机制。

常见原因

按命中率从高到低。

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 默认 fall back 到默认语言 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> 应该是:

<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。

6. Astro site 配置的 base URL 不对

astro.config.mjssite 不对,所有从它推导出来的 canonical 都不对。

怎么判断

// astro.config.mjs
export default defineConfig({
  site: 'https://site.com', // 必须跟生产一致
});

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 示例):

---
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}/`} />

canonical 从页面真实 URL 推导,不从任何硬编码前缀。

第 3 步:构建期校验

prebuild 脚本:

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 步:Search Console 验证

部署后,Search Console → URL Inspection 提交几个之前配错的 ZH URL。等 1-2 周。页面状态应从 “Duplicate without user-selected canonical” 变为 “Indexed”。

第 5 步:重交 sitemap

如果之前的 sitemap 是用错的 canonical 生成的,重新生成并提交。sitemap 每个 URL 一条(EN 和 ZH 都各自一条)。

第 6 步:看 hreflang 错误

Search Console → 旧版工具 → International Targeting → Hreflang。修后 “no return tags” 警告应在 1-2 周内消失。

预防建议

  • 模板原则:canonical = 页面自己的完整 URL,永远。没例外。
  • canonical 和 hreflang 当两个独立机制——canonical 表达”这个 URL 的这个版本是正版”;hreflang 表达”这个 URL 还有其它语言版本”。
  • CI 校验每个渲染出来的 HTML 的 canonical 等于它的真实路径。
  • 不要从 translationKey 或任何共享标识符推 canonical——只从 URL 推。
  • 加新 locale 时,先在样本文章上验证 canonical / hreflang 输出再批量上线。

相关阅读

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