你打开 EN 文章,再打开对应的 ZH,两边几乎不像同一篇的翻译。EN 有 5 个 ## 章节、3 个代码块;ZH 只有 3 个章节、1 个代码块。EN 六周前加了 FAQ 段,ZH 根本没收到。EN 把一步从 Step 1: Audit 改名成 Step 1: Inventory,ZH 还是旧措辞。这一对共享同一个 translationKey,但内容已经从结构上走散了。
最快修复: 跑一次结构 diff(按对统计 ## 标题数和 ``` 代码围栏数,而不是只看文件时间戳),按差距大小排序,给每一对漂移的页面选三条出路之一——补齐落后那侧、拆成两篇独立文章、或正式标为单语页并去掉它的 hreflang alternate。然后加一个 CI 检查,让这个差距不会每次有人改一侧就重新裂开。不要把生机翻直接贴进缺失的章节:截至 2026 年 6 月,Google 的「大规模内容滥用」(scaled content abuse)政策明确把自动翻译列为「在价值很低时可被当作垃圾内容处理」的一类转换手法(2026 年 3 月 24 日的 spam update 进一步收紧了这方面的执行)。经过人工编辑、质量过关的 AI 翻译是可以的;未经审阅的直接倾倒才是风险。
这和单纯的字数漂移(中文本来就更紧凑)不是一回事。这是结构漂移:章节数不同、代码块数不同、链接指向不同、小标题在翻译一个对面已经不存在的概念。通过 hreflang alternate 落地的读者会觉得被骗,而 SEO 损失是实打实的——hreflang 标注必须互相呼应(reciprocal),所以一对断裂或只发了一半的 alternate 会让 Google 把两端都忽略掉。
你属于哪一类
| 症状 | 可能原因 | 对应修法 |
|---|---|---|
ZH 整段缺了 EN 有的 ## 章节 | EN 单边改完没回流 | Step 1 → Step 2(同步) |
| ZH 永远落后 EN 几周,从来追不上 | 单向流水线 | Step 3(CI 闸门) |
| hreflang alternate 报 404 或指向错误页 | 改名 slug / 移动文件 | 原因 3 + Step 5 |
| ZH 散文引用了页面上没有的片段 | 代码块只加到 EN | 原因 4 |
| ZH 明显更薄、没有 FAQ | FAQ 只加在一侧 | 原因 5 |
| ZH 短,但覆盖了相同要点 | 正常的语言精简,不是 bug | 无需处理 |
最后一行很关键:中文经常用更少的字、更少的行表达同样的意思。不要因为行数不同就去「修」一对其实已经对等的页面。下面的结构审计以章节数和代码块数为锚,这两个指标不受啰嗦程度影响,正好绕开字节长度。
常见原因
1. 单边编辑从不触发翻译工单
你编辑 en/foo.mdx 加了一个新章节,提交。没有任何东西提醒你 zh/foo.mdx 现在缺了这段。在 200 篇文章上重复半年,结构鸿沟就拉得很大。
如何判断:按文件统计 ## 标题数,再按对 diff。
for f in src/content/articles/en/troubleshooting/*.mdx; do
key=$(basename "$f")
zh="src/content/articles/zh/troubleshooting/$key"
[ -f "$zh" ] || continue
en_sec=$(grep -c '^## ' "$f")
zh_sec=$(grep -c '^## ' "$zh")
if [ "$en_sec" != "$zh_sec" ]; then
echo "$key: en=$en_sec zh=$zh_sec"
fi
done
凡是 en 和 zh 相差 2 个以上的对,就是结构漂移,而不是语言精简。
2. 编辑只往一个方向走——通常是 EN -> ZH 卡住
多数内容站有一个主作者先写 EN,ZH 几周后才翻(如果翻的话)。之后的 EN 修订基本不会反向回到 ZH。这一对一开始是镜像的,然后每个 PR 都漂一点。
如何判断:列出 EN 文件最后一次提交比 ZH 文件新 30 天以上的对。
for f in src/content/articles/en/troubleshooting/*.mdx; do
zh="${f/\/en\//\/zh\/}"
[ -f "$zh" ] || continue
en_t=$(git log -1 --format=%ct -- "$f")
zh_t=$(git log -1 --format=%ct -- "$zh")
diff_days=$(( (en_t - zh_t) / 86400 ))
[ "$diff_days" -gt 30 ] && echo "$(basename "$f"): EN newer by ${diff_days}d"
done
用提交时间(git log),不要用文件系统的 mtime——git checkout 或 git clone 会把每个文件的 mtime 重置到检出那一刻,所以在新克隆的仓库或 CI 里,mtime 比较是毫无意义的。
3. 改名 translationKey 或移动文件,悄悄断对
你在 EN 里改了一个 slug。translationKey 现在指向一个缺失的 ZH 文件(或者一个还带旧 key 的过期文件)。hreflang 标注发出一对悬空的配对,而 build 不会报错。这是最贵的一类,因为断裂的 hreflang 不是「中性」的:Google 要求互相呼应的 return tag,且集合里每个 URL 都必须返回 HTTP 200。如果页面 A 指向一个 404 或 301 跳转的 B,Google 会对整个集合忽略这条标注——两个方向都丢掉 alternate 信号。
如何判断:从两端导出 translationKey 值后 diff。
diff \
<(grep -h "^translationKey:" src/content/articles/en/**/*.mdx | sort -u) \
<(grep -h "^translationKey:" src/content/articles/zh/**/*.mdx | sort -u)
任何只在一侧出现的 key,都是一篇未配对的文章——要么它的孪生缺失,要么有一侧用了错误的 key。
4. EN 新加的代码示例从没复制到 ZH
你在 EN 里加了一个代码块,里面是一段全新的 shell 脚本。ZH 还是旧版(或者根本没有代码块)。代码块本身与语言无关,但它前后的散文不是——所以 ZH 页现在引用了一段不在页面上的片段。
如何判断:按对统计三反引号围栏的行数并 diff。一个正确的文件围栏行数应当是偶数(开 + 闭);奇数意味着有一个未闭合的围栏,单这一点就能让 MDX build 挂掉。
5. FAQ 段只加在一侧
你在 EN 加了一个 ## FAQ 章节,带 3 个 ### Question? 条目。ZH 没收到。页面级的 FAQPage JSON-LD 只在 EN 发出,而 ZH 页读起来明显更薄。
把 2026 年 6 月这件事说清楚。Google 弃用了 FAQ rich results:它们从 2026 年 5 月 7 日起不再出现在搜索结果里;FAQ 的搜索展示筛选器、rich-result 报告、以及 Rich Results Test 的支持在 2026 年 6 月陆续移除(Search Console API 的支持延续到 2026 年 8 月)。所以 ZH 缺这个 FAQ,已经不再让你损失一次 rich result——那个视觉展示对几乎所有人来说都没了。它仍然让你损失的,是实打实的东西:一个更薄、更不顶用、回答了更少读者真实疑问的 ZH 页,再加上与 EN 孪生之间的结构漂移。FAQPage 依然是有效的 Schema.org 类型,而 Google 说留着没用上的结构化数据并无害处,所以正确的修法是把 ZH 页的内容补到对等,而不是去追一个已经不存在的 SERP 功能。
最短修复路径
Step 1:跑一次全量结构 diff
写一个比结构(不只比时间戳)的脚本:
// scripts/audit-pair-structure.mjs
import fs from "node:fs";
import path from "node:path";
const EN_DIR = "src/content/articles/en/troubleshooting";
const ZH_DIR = "src/content/articles/zh/troubleshooting";
function metrics(file) {
const txt = fs.readFileSync(file, "utf8");
return {
h2: (txt.match(/^## /gm) || []).length,
h3: (txt.match(/^### /gm) || []).length,
code: (txt.match(/^```/gm) || []).length / 2,
lines: txt.split("\n").length,
};
}
for (const f of fs.readdirSync(EN_DIR)) {
const en = path.join(EN_DIR, f);
const zh = path.join(ZH_DIR, f);
if (!fs.existsSync(zh)) continue;
const a = metrics(en), b = metrics(zh);
if (Math.abs(a.h2 - b.h2) >= 2 || Math.abs(a.code - b.code) >= 2) {
console.log(`DRIFT ${f}: h2 en=${a.h2} zh=${b.h2}, code en=${a.code} zh=${b.code}`);
}
}
用 node scripts/audit-pair-structure.mjs 运行。输出会把最分叉的对排在前面,先同步这些。把 >= 2 的阈值按你的容忍度调整——>= 1 会很吵,因为差一个章节往往是合理的。
Step 2:每一对决定同步还是拆分
三个合法的出路:
- 同步:把落后那侧补到与领先侧对齐的结构
- 拆分:内容已经合理分叉,去掉 translationKey 配对,当成两篇独立文章对待
- 标单语:低流量页,去掉一侧的 translationKey,从另一侧删掉 hreflang alternate
不要把缺失的章节直接机翻补上。截至 2026 年 6 月,Google 的「大规模内容滥用」政策明确把自动翻译列入「结果对用户价值很低时可被当作垃圾内容处理」的转换手法之一,而 2026 年的执行(2026 年 3 月的 spam update)正好收紧了这一点。烂机翻比缺一段还糟。要么认真做一份真翻译,要么把这一对拆开。
Step 3:在 PR 层强制 translate-as-you-edit
加一个 CI 步骤,标记出任何动了 en/*.mdx 却没动对应 zh/*.mdx 的 PR:
# .github/workflows/translation-sync.yml 片段
- name: Check translation parity
run: |
CHANGED_EN=$(git diff --name-only origin/main -- 'src/content/articles/en/' | grep '\.mdx$' || true)
for f in $CHANGED_EN; do
zh=$(echo "$f" | sed 's|/en/|/zh/|')
if [ -f "$zh" ] && ! git diff --name-only origin/main | grep -q "$zh"; then
echo "::warning::EN changed: $f -- but ZH not updated: $zh"
fi
done
::warning:: 会在 GitHub Actions 日志和 PR 的 Files Changed 标注里显示出来,但不会让任务失败。把它保持为 warning 而不是硬失败:作者要么在同一个 PR 里更新 ZH,要么开一张 i18n 工单、明确承认这次漂移。如果以后想要硬闸门,把 ::warning:: 换成 ::error:: 并加上 exit 1。
Step 4:按价值刻意回填最糟的几对
按流量挑出漂移最严重的 top 20 对。在 Google Search Console 里打开 Performance,给 Page 加一个 /zh/articles/ 的过滤器,按 Impressions 排序——薄或对不上的 ZH 页正是在这里让你损失最大。手工同步这几对。长尾先放着,等它挣到这份工作量再说。
Step 5:验证 hreflang 仍然干净配对
同步后重查 sitemap 里的 hreflang 标注。每个 URL 条目都应当列出每一个变体(包括它自己,自引用是必需的),且两个页面必须互指:
<url>
<loc>https://site.com/en/articles/slug/</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://site.com/en/articles/slug/" />
<xhtml:link rel="alternate" hreflang="zh" href="https://site.com/zh/articles/slug/" />
</url>
Google 严格执行的两条规则(其 localized-versions 文档确认,2026 年 6 月):
- 互相呼应(reciprocity)。 如果 EN 把 ZH 列为它的 alternate,ZH 必须反过来列上 EN。任何一侧的 return tag 缺失,Google 就会把两条标注都忽略。
- 集合里每个 URL 都返回 HTTP 200。 任意一个 alternate 返回
301、404或500,都会让整个页面集合的标注作废——这正是改名 slug 那一类(原因 3)造成的故障。
如果某页被标成了单语,就把它的 alternate 完全去掉。半发的 hreflang 集合比完全没有还糟,因为它可能把还在工作的那个方向也一起拖垮。
如何确认已修好
- 重新跑
node scripts/audit-pair-structure.mjs——你修过的那一对不应再打印DRIFT行。 - 并排打开两个 URL:相同的
##章节、相同顺序、相同数量的代码块、相同的 FAQ 问题。 - 验证线上 alternate 链接返回 200:
curl -I https://site.com/zh/articles/<slug>/应当显示HTTP/2 200,而不是 301 或 404。 - 在 Search Console 里打开 Indexing → Pages(或还能用的旧版 International Targeting 报告),确认这一对没有「no return tags」之类的 hreflang 错误。
预防
- 只要 EN/ZH 单独变更就发 CI warning,作者必须回应
- 结构审计(
h2/h3/代码块计数)每周跑一次,或在 prebuild 时对着origin/main跑 - 一侧新增
## FAQ段,merge 前需要先开一张 i18n 工单 - 改 slug 必须在同一个 PR 里改两端,由一条 lint 规则强制
- 低流量页明确标为单语,而不是留着任它漂移
- 季度回顾:按 GSC impressions 排出的 top 20 漂移对纳入排期同步
FAQ
EN 页比它的 ZH 翻译长很多,是不是 SEO 问题?
单凭这一点不是。Google 不要求等长,而且中文通常用更少的字表达同样的内容。真正伤的是实质内容缺失——一侧有、另一侧没有的章节、步骤或答案——以及断裂的 hreflang。对齐结构和实质,忽略原始行数。
为了快点补上差距,直接把缺的 ZH 章节机翻行不行?
别当成原始倾倒。截至 2026 年 6 月,Google 的「大规模内容滥用」政策明确把自动翻译列为「在价值很低时可被当作垃圾内容处理」的转换手法之一,而 2026 年 3 月 24 日的 spam update 正好收紧了对这一模式的执行。微妙之处在于:对真正有用的内容做 AI 翻译、再人工编辑校准准确度和地道表达,是可以的——Google 说过它仍然会用这类页面。风险来自未经审阅的机翻倾倒。所以要么把新章节认真翻好(AI 辅助 + 人工编辑),要么把这一对拆成两篇独立文章。
FAQ rich results 都没了,我还需要 FAQ 结构化数据吗?
rich result 是没了——FAQ 摘要从 2026 年 5 月 7 日起不再出现在搜索里,相关报告和 Rich Results Test 支持在 2026 年 6 月陆续退役。但 FAQPage 仍是有效的 Schema.org 类型,Google 说留着没用上的结构化数据并无害处。如果你的布局会自动发出这段 markup,就留着,只是别再指望它带来 SERP 功能。保留一份强 FAQ 的真正理由,是它能在页面上回答读者的疑问。
为什么一条断裂的 hreflang 会把两个方向都搞坏?
Google 要求互相呼应的标注:页面 A 必须指向 B,B 也必须反指 A,且集合里每个 URL 都返回 HTTP 200。如果改名 slug 让某个 alternate 变成 404 或 301,Google 会丢掉整个集合的标注,于是连还在工作的那一侧也丢掉 alternate 信号。这就是为什么修 slug(原因 3)、再重新验证 200 响应(Step 5)是价值最高的修复。
该修的漂移和正常的啰嗦差别在哪?
漂移体现在结构上:## 章节数不同、代码块数不同、小标题翻译了一个对面已经没有的概念、或链接指向不同的页面。啰嗦只体现在长度上,而章节、步骤、代码块全都对得上。审计脚本以计数(而非字节)为锚,正是为了忽略啰嗦、抓住漂移。