你的双语站从底层接线就坏了:EN 和 ZH 页面互相声明 alternate,但声明不对称、语言代码不一致、有一端还缺 x-default。用 Screaming Frog 或 Ahrefs 一爬,几十对页面亮起 “Missing reciprocal hreflang (no return-tag)“,外加几条 “invalid language code” 和 “non-200 hreflang URL”。后果很具体:只要任一方向的互引断了,Google 会把这一对的两条声明全部忽略,于是它无法可靠地把正确的语言版本换进 SERP——用户被发错了语言。
最快的修法: 别再手写 hreflang。从单一真相源(translationKey)自动生成,让每一对从结构上就互引;语言代码只选一种(zh 或 zh-CN)全站统一;始终带上自引标签和 x-default;然后在信任全站之前,先用免费的 Merkle hreflang 校验器(technicalseo.com/tools/hreflang/)验一个修好的 URL。
有一处自本文最初撰写以来(也比许多老教程新)发生了变化,必须说清楚:Search Console 的 International Targeting 报告——也就是旧的 “Language / hreflang” 错误标签页——已被 Google 弃用并下线。 Google 仍然读取并遵守 hreflang,但它不再在 Search Console 里报告 hreflang 错误。截至 2026 年 6 月,你必须自己用第三方爬虫或单 URL 校验器来验 hreflang。任何还让你”去看 International Targeting 报告”的教程都已过时。
你属于哪一类
| 爬虫 / 校验器里的症状 | 最可能的原因 | 跳到 |
|---|---|---|
| ”Missing reciprocal hreflang (no return-tag)“ | 有一端没回链,或回链了不同的 URL | 原因 1 |
| ”Invalid / unknown language code” | zh vs zh-CN vs zh-Hans 不一致 | 原因 2 |
| return tag 在、但仍被标记 | 末尾斜杠或 http/https URL 不匹配 | 原因 3 和 6 |
| ”No self-reference” 警告 | 页面缺一条指向自己的 alternate | 原因 4 |
| alternate URL 返回 404 / 3xx | 给不存在的翻译发了 hreflang | 原因 5 |
| 校验器和爬虫结果不一致 | sitemap 与 HTML head 里的 hreflang 冲突 | 原因 7 |
常见原因
1. 一边声明了对、另一边没有(no return tag)
EN 页声明 ZH 为自己的 alternate;ZH 页没有声明 EN 为 alternate,或声明了不同的 EN URL(旧 slug、http 与 https、带或不带末尾 /)。hreflang 是双向的:如果 A 页把 B 页列为 alternate,B 页就必须把 A 页回列。任一方向缺失,Google 就把两条声明全部丢弃。这是最常见的 hreflang 故障。
如何判断: 取回两页,对比它们的 link rel="alternate" 区块。两者必须以完全相同的 URL 互相列出。
curl -s https://site.com/en/articles/foo/ | grep 'hreflang'
curl -s https://site.com/zh/articles/foo/ | grep 'hreflang'
两段输出应当字节级地引用对方的 URL。
2. 语言代码不一致(zh vs zh-CN vs zh-Hans)
EN 页写 hreflang="zh";ZH 页写 hreflang="zh-CN"。对中两页的代码必须完全相同,否则 Google 视为不同声明。全站只选一种约定。
两个常踩的坑,截至 2026 年 6 月都成立:
- hreflang 用 ISO 639-1 语言代码,加上可选的 ISO 3166-1 Alpha 2 地区码(所以是
zh或zh-CN,不是zh-china)。 - Google 确实接受
zh-Hans(简体)、zh-Hant(繁体)这类 BCP-47 的 script 子标签,但对只有两个 locale 的 EN/ZH 站来说,它们给的精细度你用不上,zh或zh-CN就够了。真正会把你搞坏的是混用:如果 EN 页写zh-Hans、ZH 页写zh-CN,这一对就配不上,声明会被丢弃。选定一种写法,全站统一。真正”非法”的值——比如拼错成zh-china、或根本不是合法 ISO 语言的代码——才是多数校验器报 “invalid code” 的来源。
如何判断: grep 模板里出现的每一个 hreflang 代码。
grep -rn 'hreflang=' src/layouts/ src/components/
同一种语言出现了一个以上的代码,就是这病。
3. 末尾斜杠不一致
EN 页把 ZH alternate 写成 https://site.com/zh/articles/foo,但实际 ZH URL 是 https://site.com/zh/articles/foo/(带末尾 /)。Google 把它们当作两个不同的 URL——于是回链”并不存在”于它被许诺的那个 URL 上。
如何判断: 拿 sitemap 里的 canonical URL 和 hreflang URL 对比,必须字节级匹配,包括末尾斜杠。
4. 缺少自引标签(self-reference)
除了声明各个 alternate,每个页面还应包含一条指向自己的 hreflang。Google 的 John Mueller 称自引在技术上是可选的,但强烈推荐保留,而且大多数校验器(包括 Merkle 校验器)会把 “self-reference: not found” 报为警告。最省事的保证办法:为每一个存在的 locale 发一条 alternate,包括当前页自己的 locale。
如何判断: technicalseo.com/tools/hreflang/ 的 Merkle 校验器会逐 URL 显示 “self-reference” 状态。“Not found” 即表示页面漏了自引标签。
5. 缺少 x-default
你声明了 en 和 zh 的 alternate,却没声明 x-default。没有它,对于语言/地区两边都不匹配的用户,Google 没有任何指示。最佳实践:x-default 指向你的主语言(这里是 EN)。
如何判断:
curl -s https://site.com/en/articles/foo/ | grep 'x-default'
结果为空,就是缺 x-default。
6. 给没有翻译的页面发 hreflang
你给每一篇 EN 都发 zh alternate——即便没有对应的 ZH。这条 “alternate” 指向 404 或被重定向到根目录。非 200 的 hreflang 目标无效,这一对也就永远不互引。
如何判断: 任何返回 3xx 或 4xx 的已声明 alternate URL。Screaming Frog 会把它们标为 “Non-200 hreflang URLs”。
7. sitemap 与 HTML head 里的 hreflang 互相打架
你可以在 XML sitemap 或 HTML head(或 HTTP headers)里声明 hreflang——二选一。两处都做且取值冲突是最坏的情况:读 head 的爬虫和读 sitemap 的校验器会报出不同结果,Google 也会收到互相矛盾的信号。
如何判断: 拿 sitemap 条目里的 hreflang 和页面 HTML 对比,任何不一致都是 bug。统一到一个来源。
最短修复路径
Step 1:从 translationKey 自动发 hreflang
单一真相源。在 article layout 中按 translationKey 查兄弟,为每个真有对应的 locale 发一条 alternate。由于当前页本身也是这些兄弟之一,这同时把自引标签自动带上了:
---
import { getCollection } from "astro:content";
const { article } = Astro.props;
const all = await getCollection("articles");
const siblings = all.filter(a => a.data.translationKey === article.data.translationKey);
const SITE = "https://site.com";
---
{siblings.map(s => (
<link
rel="alternate"
hreflang={s.data.lang === "zh" ? "zh-CN" : s.data.lang}
href={`${SITE}/${s.data.lang}/articles/${s.data.urlSlug}/`}
/>
))}
<link rel="alternate" hreflang="x-default" href={`${SITE}/en/articles/${article.data.urlSlug}/`} />
这保证了互引:只要 ZH 兄弟存在,EN 页就发 ZH alternate,而且 ZH 页发 EN alternate,二者出自同一份数据。兄弟不存在时,就不为那个 locale 发 alternate——顺带修掉了原因 6。
Step 2:选定一种语言代码约定并统一应用
两个合理选择:
- "zh" (仅语言) —— 最简单;只有一种中文版本时够用
- "zh-CN" (语言+地区)—— 显式;未来可能加 zh-TW 时推荐
对一个简单的 EN/ZH 站,优先用 zh 或 zh-CN,而不是 script 子标签 zh-Hans / zh-Hant——Google 接受 script 形式,但它给的粒度两个 locale 的站用不上,而且把它和地区码混用是常见的不匹配来源。无论选哪种,都在模板里写死,然后用 grep -rn 'hreflang=' 确认任何地方都不再出现别的代码。
Step 3:规范 URL(末尾斜杠)
Astro 默认给 content collection 的 URL 带末尾 /。锁定它,让 hreflang、canonical、sitemap 三者一致:
export default defineConfig({
trailingSlash: "always",
build: { format: "directory" },
});
然后确认每条 hreflang 标签发出的 URL 与该页自己的 rel="canonical" URL 完全一致。这里不一致,会在 Step 1 之后重新制造出原因 3。
Step 4:信任全站之前,先验一个 URL
部署后,先用单 URL 校验器查一对有代表性的页面,再爬全站:
- technicalseo.com/tools/hreflang/ (Merkle,免费)—— 贴 URL;显示自引状态、标签数量、每条 alternate 是否返回 200
- hreflang.org/ —— 较老的免费校验器;快速查一对仍可用
- Screaming Frog SEO Spider —— 全站爬取;"Hreflang" 标签页标出缺失的回链、错误代码、非 200 目标
- Ahrefs / Sitebulb Site Audit —— 与整体技术审计一起呈现 hreflang 健康度
如今已没有 Search Console 的 hreflang 报告(International Targeting 报告已下线),所以这些第三方工具就是权威检查。
Step 5:加一条 prebuild 断言
在上线前就抓住 hreflang 回退。断言每个 translationKey 分组内部一致,且没有任何分组引用了不存在的 slug:
# scripts/audit-hreflang-pairs.mjs
import { getCollection } from "astro:content";
const all = await getCollection("articles");
const byKey = new Map();
for (const a of all) {
if (!a.data.translationKey) continue;
if (!byKey.has(a.data.translationKey)) byKey.set(a.data.translationKey, []);
byKey.get(a.data.translationKey).push(a);
}
let problems = 0;
for (const [key, group] of byKey) {
// 同一分组的所有成员必须共用同一个 urlSlug,EN/ZH 的 URL 才能干净配对
const slugs = new Set(group.map(a => a.data.urlSlug));
if (slugs.size > 1) {
console.error(`translationKey "${key}" has mismatched urlSlugs: ${[...slugs].join(", ")}`);
problems++;
}
// 一个分组里出现两个同 lang 的成员,就是重复 / 打错了
const langs = group.map(a => a.data.lang);
if (new Set(langs).size !== langs.length) {
console.error(`translationKey "${key}" has duplicate langs: ${langs.join(", ")}`);
problems++;
}
}
console.log(`Hreflang audit: ${problems} problems`);
process.exit(problems > 0 ? 1 : 0);
把它挂进 prebuild,让坏掉的一对直接挂掉构建,而不是流到 Google。
Step 6:重抓 + 对修好的页面请求 indexing
Google 按自己的节奏重抓。要加速,就用 Search Console 的 URL Inspection 工具,对一批修好的页面点 “Request indexing”,让纠正后的 tags 被重新读取。然后等下一次抓取后重跑你的爬虫(Step 4),确认 “no return-tag” 的数量已降到零。这以天计,不是几分钟。
如何确认已修好
- 对一个 EN URL 和它的 ZH 对页跑 Merkle 校验器。两者都应显示:互相列为 alternate、存在自引、存在
x-default、且每条 alternate 都返回 HTTP 200。 - 用 Screaming Frog(或 Ahrefs Site Audit)爬全站。“Missing return links” / “no return-tag” 的数量应为 0,且不应有 “non-200 hreflang URL” 的行。
- 在一个线上页面查看源码,确认 hreflang 的 URL 与该页
rel="canonical"完全一致,末尾斜杠也一样。 - 你的 prebuild 断言(Step 5)以 0 退出。
FAQ
Search Console 的 hreflang 报告去哪了? Google 弃用并下线了 International Targeting 报告,里面包含 “Language” / hreflang 错误标签页。Google 仍读取 hreflang,但不再在 Search Console 报告 hreflang 错误。改用第三方爬虫(Screaming Frog、Ahrefs、Sitebulb)或单 URL 的 Merkle 校验器来验。
该用 zh 还是 zh-CN?
两者都合法。只有一种中文版本就用 zh;将来可能加繁体中文(zh-TW)就用 zh-CN,让代码不含糊。Google 也接受 zh-Hans / zh-Hant 这类 script 子标签,但对两个 locale 的站来说有点过头,而且容易和地区码搞混。最重要的一条规则:全站保持一致。
自引 hreflang 标签真的需要吗?
Google 的 John Mueller 说技术上可选,但强烈推荐,而且大多数校验器在它缺失时会警告。Step 1 的自动发模板免费包含了它,因为当前页本身就是它自己 translationKey 兄弟之一。
“no return tag” 到底是什么意思? A 页把 B 页列为 alternate,但 B 页没把 A 页回列(或列了一个略有差异的 URL)。hreflang 必须互引;任一方向缺失,Google 就把两条声明都丢弃,于是两页都得不到好处。在源头修好互引(Step 1)能消灭这一整类错误。
hreflang 该放 HTML head 还是 XML sitemap? 都行,但别两处都放且取值不同。对于已经渲染逐文章 layout 的内容站,HTML head 通常最简单。选一个来源,并确保没有别的东西发出冲突的集合。
Google 多久能反映修复? 以天计,不是几分钟。Google 要重抓两页并重新评估这一对。对一批样本请求 indexing 能加速;下一次抓取后重跑爬虫,确认错误数已下降。
预防
- hreflang 从
translationKey生成,绝不手写。 - 全站统一一种语言代码约定(如
zh-CN),用 grep/lint 强制;别把它和zh-Hans或裸zh混用。 - 自引标签和
x-default始终存在,x-default 指向主语言。 - 末尾斜杠政策在 Astro config 里锁死;hreflang URL 与 canonical URL 字节级匹配。
- prebuild 断言:每个
translationKey分组共用一个 slug、无重复 lang、不引用缺失文件。 - 每次大部署后跑一遍外部校验器(Merkle 校验器或 Screaming Frog)。
- 每季度爬一次全站,因为 Search Console 已不再替你标记 hreflang。