每篇文章都有 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.mjs 里 site 不对,所有从它推导出来的 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 #双语