hreflang 配错不会报错:双向互指、语言码规范与 URL 结构的落地检查

站点做了一个英文版,或者给简体/繁体各做一个版本,第一个绕不过去的问题就是:怎么让搜索引擎知道「这几个页面是同一篇文章的不同语言版本,不要当成重复内容」。

答案是 hreflang。但 hreflang 是这类标签里最容易配错、配错之后又最难发现的一个——它不会报错,不会在浏览器里看到任何异常,页面完全正常显示,只是搜索引擎默默地把你判成了重复内容,或者干脆忽略了整个语言簇。我自己第一次配的时候,三个月后才发现有一半页面写错了国家码。

这篇文章按「什么时候该用、三个必须满足的硬条件、五种常见错误、以及怎么验证」四段来讲。

一、先判断该不该配:三种情况不该用

hreflang 不是「有多语言就要配」的装饰品,它解决的问题很具体:同一内容的多语言/多地区版本之间的对应关系。以下几种情况不该用。

机器翻译的质量不达标的版本。如果你的英文页是整段机翻、语法不通,配 hreflang 相当于主动把低质内容摆到搜索引擎面前。这种情况下更好的选择是不配、甚至不对外链接,而不是建立语言簇。

内容差异很大的版本。同一个 URL 结构下,中文页讲 A,英文页讲的是与 A 相关的 B,两者不是同一篇内容的不同语言版。这种情况应该各自独立做 SEO,而不是用 hreflang 绑成一组。

同一语言的不同地区,但内容完全相同。比如 zh-CN 和 zh-TW,如果两者内容完全一样(只是字体不同),配 hreflang 也不会让两个版本都获得排名——搜索引擎会选一个作为规范版本,另一个基本不参与竞争。这种情况浪费的是你自己的抓取预算。

真正适合用 hreflang 的场景是:同一篇文章,有真实翻译的不同语言版本,且每个版本自身有独立的内容价值。比如一篇讲 Nginx 配置的中文文章有对应的英文译版,两者都有人搜,都应该能排到。

二、三个硬条件:漏一个整簇失效

hreflang 是一组互相引用关系,不是单向声明。这是它最容易出错的地方。三个条件必须同时满足:

条件一:双向互指(reciprocal)。如果中文页声明「英文版在这里」,英文页必须也声明「中文版在那里」。单向链接等于无效。如果 A 指向 B、B 指向 C、但 A 不指向 C,那么这个三语言簇是断裂的,搜索引擎通常会忽略掉不完整的那部分。

条件二:每个页面都要包含自己。中文页的 hreflang 集合里,必须包含指向自己的 zh-CN 条目。很多人只写「指向其他语言」的条目,漏掉自指——这是最常见的错误之一。原因很直观:搜索引擎需要从任意一个节点出发都能找到整个簇,如果 A 页面不声明自己,从 A 出发的爬取会发现「A 不是任何语言版本」,整簇的锚点就塌了。

条件三:x-default 必须存在且只有一个。x-default 表示「没有匹配到任何语言时的兜底版本」,通常指向英文版或者语言选择页。每个 hreflang 集合里应该有且只有一个 x-default。缺了它,搜索引擎不知道给日语用户、法语用户展示哪个版本;写了两个,等于自相矛盾。

三条都满足的情况下,一个中英双语站的文章页,head 里大概是这个样子:

<link rel="alternate" hreflang="zh-CN" href="https://www.example.com/nginx-cache/" />
<link rel="alternate" hreflang="en"    href="https://www.example.com/en/nginx-cache/" />
<link rel="alternate" hreflang="x-default" href="https://www.example.com/en/nginx-cache/" />
<link rel="canonical" href="https://www.example.com/nginx-cache/" />

英文页里则是同样三行,只是 hreflang="zh-CN" 和 hreflang="en" 对应的 href 反过来,canonical 指向英文页自己。注意 canonical 是指向自己的,不是指向中文版——这一点很重要,很多人误以为多语言版本应该「canonical 到主语言」,那等于让所有语言版本都退出索引。

三、语言码怎么写:五种被忽略的写法错误

语言码的规范来自 BCP 47,格式是「语言-地区」,语言用小写,地区用大写。这是最容易出错的地方,而且错了不会报错,只是被忽略。

第一个错误:用下划线而不是连字符。zh_CN 是错的,必须是 zh-CN。我见过好几个站把 HTML 里的 lang 属性写法照搬到 hreflang 上,两者语法不同:lang 属性允许下划线(HTML 规范的宽松处理),hreflang 严格按 BCP 47 要求连字符。

第二个错误:大小写反了。语言子标签必须小写、地区子标签必须大写:zh-cn 是错的,ZH-CN 是错的,zh-CN 才对。虽然 BCP 47 规定处理时应该大小写不敏感,但搜索引擎的解析器实现不一定宽容,实测中大小写错误会导致该条目被忽略。

第三个错误:写了不存在的地区码。有人写 zh-CH(以为是「中文-瑞士」,实际瑞士的中文用户地区码应该用 zh-Hans-CH 或者干脆只用 zh),有人写 en-UK——英国的正确码是 en-GB,UK 是「英国」的俗称但不是 ISO 3166-1 里的国家码。这类错误非常隐蔽,因为看起来很像那么回事。

第四个错误:语言和地区分不清。zh-TW 是「台湾地区使用的中文」,zh-Hant 是「繁体中文(不限地区)」。如果你只有一份繁体版,应该用 zh-Hant,而不是 zh-TW——用 zh-TW 会让香港的繁体用户匹配不上。同理,简繁混合的系统里用 zh-Hans/zh-Hant 比用具体地区码更准确。

第五个错误:x-default 写错。正确的是全小写 x-default。写成 x-Default、X-DEFAULT、xdefault 都会被当成未知语言码忽略。

把这些写进自动化检查里比人工核对可靠得多。一个正则就能覆盖大部分:

grep -oE 'hreflang=[^\x27\x22 ]+' page.html \
  | sed 's/hreflang=//;s/\x22//g' \
  | grep -vE '^(x-default|[a-z]{2,3}(-[A-Z][a-z]{3})?(-[A-Z]{2}|-[0-9]{3})?)$' \
  | sort -u

输出为空,说明格式全部合规。这条命令我在每次批量改模板之后都会跑一遍,抓出过 zh-cn、en-UK、zh_CN 各一次。

四、三种落地方式:HTML 标签、HTTP 头、Sitemap

hreflang 可以写在三个地方,各有适用场景。

HTML head 里的 link 标签是最常用的。优点是直观、可以按页面单独控制;缺点是每个页面都要输出完整的语言簇,页面数一多,模板里很容易漏。用 Typecho/WordPress 这类 CMS 的时候,通常要在模板的 header 里写一段循环,从文章的自定义字段里取各语言版本的 URL。

HTTP 响应头适合非 HTML 的资源,比如 PDF。格式是:

Link: <https://www.example.com/zh/doc.pdf>; rel="alternate"; hreflang="zh-CN",
      <https://www.example.com/en/doc.pdf>; rel="alternate"; hreflang="en"

对于 PDF 文件,因为不能改内容,只能靠 HTTP 头声明语言版本关系。这也是唯一能处理「非 HTML 内容多语言关联」的方式。

XML Sitemap 适合页面数量多、或者语言版本关系在数据库里集中维护的站点。写法是在每个 url 条目里加一组 xhtml:link:

<url>
  <loc>https://www.example.com/nginx-cache/</loc>
  <xhtml:link rel="alternate" hreflang="zh-CN" href="https://www.example.com/nginx-cache/" />
  <xhtml:link rel="alternate" hreflang="en" href="https://www.example.com/en/nginx-cache/" />
  <xhtml:link rel="alternate" hreflang="x-default" href="https://www.example.com/en/nginx-cache/" />
</url>

注意 sitemap 里必须声明命名空间 xmlns:xhtml="http://www.w3.org/1999/xhtml",漏了整段会被忽略。sitemap 方式的优点是集中维护、不会漏页;缺点是更新有延迟(要等 sitemap 被重新抓取)。

三种方式可以并存,搜索引擎会合并处理。但如果同一个页面在 HTML 里声明的和 sitemap 里声明的不一致,会以某一处为准(不同引擎行为不同),结果是不可预测。我的建议是只选一种作为权威来源,另外两种如果有,必须由同一个数据源生成,保证完全一致。

五、验证:四层检查顺序

hreflang 配完必须验证,因为错误是完全静默的。四层检查从粗到细。

第一层,看源码里有没有输出。一个页面本该有 3 条 hreflang,实际输出 0 条,说明模板分支没走到——常见于「文章没有设置英文版 URL 字段时整段跳过」,但你其实已经设置了,只是字段名拼错。

curl -s https://www.example.com/nginx-cache/ | grep -c 'rel="alternate"'

第二层,验证双向性。写个脚本,从每个页面抓出所有 hreflang 指向的 URL,再抓那些 URL,确认它们也指回来。这个检查能发现「英文版指向中文版,但中文版忘写英文版」这类不对称。

import re, urllib.request
def hr(u):
    h = urllib.request.urlopen(u, timeout=15).read().decode('utf-8', 'ignore')
    return dict(re.findall(r'hreflang="([^"]+)"\s+href="([^"]+)"', h))
a = hr('https://www.example.com/nginx-cache/')
for lang, url in a.items():
    back = hr(url)
    me = 'https://www.example.com/nginx-cache/'
    ok = me in back.values()
    print(lang, url, 'RECIPROCAL' if ok else 'BROKEN')

第三层,检查语言码格式,用上一节那条正则。第四层,看搜索引擎是否识别:在 Google Search Console 的「国际化」报告里,能看到「缺少返回链接」「无效语言码」「重复 x-default」这几类问题,条目会精确到具体 URL。这个报告需要站点有一定抓取量才会填充,小站可能一直是空的,那就只能靠前三层。

六、一个更根本的问题:语言版本的 URL 结构

hreflang 只是「告诉搜索引擎关系」,语言版本本身的 URL 结构才是决定这套体系能不能长期维护的基础。三种常见结构,优劣很清楚。

子目录(/en/)是最推荐的。同一个域名下、路径前缀区分语言,主域名的权重能共享,配置 Nginx 的 location 规则也简单。缺点是「根路径默认是哪种语言」需要明确:根路径给中文,/en/ 给英文,那么 /en/ 下的页面要不要也提供 /zh/ 镜像?不提供的话,中文页只有根路径一个入口,hreflang 里自指就写根路径,完全可行。

子域名(en.example.com)在技术上等价于「独立站点」,搜索引擎会当成两个站看待,权重不共享。如果你打算让英文版独立运营、独立做外链,子域名合适;如果只是想给中文内容加个译版,子域名会拖慢英文版的收录。

参数(?lang=en)是三种里最差的。它把语言差异塞进了查询参数,缓存和 CDN 都要额外配 Vary,而且搜索引擎对参数 URL 的抓取预算通常更保守。除非是过渡期的临时方案,不要用。

定好结构之后,hreflang 的 URL 应该完全由该结构推导出来,而不是手写。中文页 /nginx-cache/ 对应的英文页恒为 /en/nginx-cache/,这个映射关系写进模板函数,自然就不会出现「A 页面写对了、B 页面写错了」的情况。手工维护 hreflang 的站点,错误率几乎百分之百。

七、把这套东西交接给别人时,我会写下来的几句话

最后总结成可以直接放进交接文档的几条,都是踩过坑之后的结论:

hreflang 是关系声明不是内容声明,它不改变页面的排名能力,只是告诉搜索引擎「这些是同一篇」。指望加了 hreflang 就让英文版排上去,一定会失望。

把 hreflang 的生成绑定到 URL 结构,不手工维护。模板函数接受「当前语言」和「文章 slug」,输出完整的语言簇,包括自指条目。

语言码按 BCP 47 写,语言小写、地区大写。zh-Hans/zh-Hant 表达文字体系比 zh-CN/zh-TW 表达地区更准确,除非你的内容真的分地区运营。

每个语言簇必须有且只有一个 x-default,双向互指必须成立,自指条目不能漏。这三条是硬约束,漏一条整簇白配。

最后,把它当成一个发布时的自动检查项,而不是上线前做一次就结束的工作。新加语言版本、改 URL 结构、换模板,每一个动作都可能破坏原来的对称性。你不检查,它就一直错着,而且永远不会有任何报错提示你。

Last modification:November 14th, 2026 at 07:31 pm

Leave a Comment