多语言站点最容易出问题的不是翻译,而是搜索引擎不知道该把哪个语言版本给哪个用户。英文用户搜到了中文页,日文用户点进去发现只有简体,Google 收录了五种语言但流量全堆在一个版本上。这类问题的根源只有一个:站点没有告诉搜索引擎「这几篇是同一内容的不同语言版本」。hreflang 就是干这件事的,但它有几个非常具体的坑,配错了比不配更糟。
一、先搞清楚 hreflang 不是重定向
很多站长第一次接触 hreflang,会以为加上它就等于自动跳转。不是。hreflang 只是一个「提示」,告诉搜索引擎这几个 URL 是同一内容的语言变体,搜索引擎据此在结果页展示对应语言版本。用户访问哪个 URL,完全由你的跳转逻辑决定。
如果你希望用户真的被送到对应语言,需要在服务端或前端做语言协商,常见做法是按 Accept-Language 请求头判断并 302 到对应语言目录。这里有个反直觉的点:语言协商跳转不要对搜索引擎爬虫生效,否则爬虫永远只能看到被跳转后的一个版本,其他语言版本根本抓不到。务实的做法是:只在用户主动点语言切换时跳,首页语言协商只对无 cookie 的真实用户做,同时确保爬虫 UA 一律返回原文页面而不是跳转。
另一个常见误解是 hreflang 值必须和页面语言完全一致。实际上 hreflang 用的是 ISO 639-1 语言码加可选的 ISO 3166-1 地区码,比如 zh-Hans、zh-Hant、en、en-US、pt-BR。只用语言码 en 表示「所有英语用户」,用 en-GB 只表示英国用户。混用时要小心:如果同时写了 en 和 en-GB,Google 会把 en-GB 视为更具体,而 en 变成兜底,两个 URL 若指向不同页面,就容易出现「英国用户被送到通用英语页」的情况。
二、三种实现方式的取舍
hreflang 有三种落地形式,选错会让维护成本翻倍。
第一种:HTML head 里的 link 标签。每个语言版本页面里写一组指向所有版本的 <link rel="alternate" hreflang="xx" href="...">,并且必须包含指向自己的那条。这是最直观的方式,缺点是页面多了之后每篇要输出 N 条,模板里必须动态生成,写死以后新增语言就漏。
第二种:HTTP 响应头。通过 Link 头输出,适合 PDF 这类非 HTML 资源。Nginx 里可以这样加:
location ~ \.(pdf)$ {
add_header Link '<https://www.example.com/cn/manual.pdf>; rel="alternate"; hreflang="zh-Hans"';
add_header Link '<https://www.example.com/en/manual.pdf>; rel="alternate"; hreflang="en"';
}注意 Nginx 的 add_header 在同一个 location 里写多次会输出多行 Link 头,这是合法的;但如果上层 server 块也加了 add_header,子块里的会覆盖掉父块的(这条规则坑过无数人,和安全响应头是同一个坑)。
第三种:XML 站点地图。在 sitemap 的每个 url 节点里用 xhtml:link 声明所有语言变体。这种方式的好处是所有语言关系集中在一处、不需要改页面模板,坏处是错误不容易被发现——页面改版了但 sitemap 没更新,两边就打架。
实操建议:页面数量在几十到几百、语言固定三种以内,用 head link 最稳;页面上万或语言版本经常增删,用 sitemap 集中管理;两者别同时用不同数据源,Google 会把冲突的信号当成噪音直接忽略整组。
三、x-default 到底该指哪儿
x-default 是这组标签里最常被配错的一个。它的含义是「没有任何语言匹配时的兜底页面」,通常指向语言选择页或默认语言首页。三个要点:
第一,x-default 只能有一条,写两条以上整组失效。第二,它必须出现在每一个语言版本的页面里,不只是默认语言页。第三,不要在 x-default 后面再加地区码,x-default-GB 这种写法不存在。
如果站点是「中文为主、英文为辅」,x-default 指中文首页是合理的;如果目标用户一半在海外,指一个带语言切换的落地页更合适。这取决于业务,但没有「必须指英文」的规矩。
四、必须遵守的四条对称性规则
hreflang 不是单向声明,它是双向约定。下面四条任意一条破了,整组标签都会被忽略:
规则一:回指必须存在。A 页面声明了指向 B,B 页面也必须声明指向 A。缺一条,Google 就不认这组关系。批量生成时最容易漏的是「新加的语言版本忘了回头改老页面」。
规则二:URL 必须完全一致。带不带 www、带不带结尾斜杠、http 还是 https、URL 里参数顺序,任何一个字符不同都算不同 URL。这是排查时消耗时间最多的部分,因为肉眼看不出差别。
规则三:只能指向 200 且可索引的页面。指向 301 跳转页、404、noindex 页,那条声明直接作废。
规则四:一组里每个语言只能出现一次。两个 URL 都声明自己是 zh-Hans,等于自相矛盾。
五、批量生成别手写,用脚本从数据源出
如果站点是动态生成的,hreflang 应该从「翻译关系表」生成,而不是在模板里拼字符串。下面是一个从数据库读语言变体并输出 head 标签的示例逻辑(PHP 语法):
$variants = [
'zh-Hans' => '/cn/nginx-gzip.html',
'zh-Hant' => '/tw/nginx-gzip.html',
'en' => '/en/nginx-gzip.html',
];
$base = 'https://www.example.com';
foreach ($variants as $lang => $path) {
printf('<link rel="alternate" hreflang="%s" href="%s%s">' . PHP_EOL,
$lang, $base, $path);
}
printf('<link rel="alternate" hreflang="x-default" href="%s/cn/nginx-gzip.html">' . PHP_EOL, $base);关键是要在生成后做一次自检:抓出全站所有 hreflang 声明,按「同一组的 URL 集合」做聚类,检查每个集合里的成员是否互相都声明了对方。用 Python 几十行就能写完,一次能扫出所有不对称的页面,比人工点链接快几十倍。
自检脚本的判定逻辑大致是:建立「源 URL → 声明目标集合」的字典,然后对每个源,检查它声明的每个目标是否也在字典里、且反向声明了源。不满足的就列出来。这套脚本我放在发布前的检查清单里,每次加语言都跑一遍。
六、验证与常见误判
Google Search Console 的「国际定位」报告在 2023 年之后已经下线,现在没有官方专门的 hreflang 报表,验证只能靠:
一、看页面源码里的标签是否完整输出、有没有被模板截断。二、用 curl -sI 检查响应头里的 Link 头格式是否正确(注意多个 Link 头是分行的,不是逗号拼接)。三、在 GSC 的「网页」报告里看各语言目录的收录数量,如果某个语言目录收录数明显为 0 而其他正常,多半是那组 hreflang 断链或者页面本身被 robots 挡了。
还有两个常见的误判:其一,配好 hreflang 后排名并没有立刻变化,因为搜索引擎需要重新抓取并重建聚类,通常要几周,期间看到「没效果」是正常的,别急着改来改去。其二,hreflang 不能解决重复内容问题——它不会让两个语言页面互相「合并权重」,那是 canonical 的事。两个标签的职责不同:canonical 说「这些是同一页」,hreflang 说「这些是不同语言页」。同一组多语言页面里,canonical 应该各指自己,而不是全部指向默认语言,这是最容易导致「除默认语言外全部不收录」的错误配置。
七、什么时候干脆别做
如果站点只有一两种语言、且内容量很小,hreflang 的收益很有限,做好基本的语言目录划分和内部链接就够了。真正需要认真配的场景是:语言三种以上、内容量大、目标市场跨地区(比如繁简中文、英美英语、巴西葡语)。盲目加一堆标签反而会消耗抓取预算,把本来该抓的新页面挤掉。
最后交代一个顺序:先定 URL 结构(语言目录还是子域名),再定 canonical,最后加 hreflang。顺序反了,后面每加一种语言都要回头改一遍前面所有页面,工程量翻倍。
八、上线顺序与两处容易忽视的细节
具体落地时我按这个顺序做,可以避免大部分返工:先把 URL 结构定死(语言目录还是子域名,一旦上线再改就是全站跳转),再统一 canonical 为自指,最后一次性批量生成 hreflang。反过来的顺序会让你在每次新增语言时都要回头改历史页面。
第一个细节是 <html lang="zh-Hans"> 这个属性。它和 hreflang 不是同一件事,但两者不一致会让搜索引擎判断困难:页面里写 lang="en"、head 里声明 hreflang="zh-Hans",属于自相矛盾。这个属性应该由同一个语言变量生成,不要一处写死。
第二个细节是自动翻译页面的处理。现在很多站点会用机翻生成几十种语言,这类页面如果内容质量过不去,会被判为「低质量自动生成」,反而拖累整站。稳妥的做法是只保留人工校对过的语言版本对它声明 hreflang,机翻版本一律加 noindex 并且不给它声明任何语言变体关系,让它既不被收录也不污染聚类。
做完这些之后,把「新增语言」写进自己的检查清单:新增语言目录 → 补 canonical → 补 lang 属性 → 重跑 hreflang 对称性自检脚本 → 提交 sitemap → 两周后看 GSC 的目录收录数。这条流水线跑顺了,以后加语言就是十来分钟的事。