hreflang 是主流 SEO 信号里,唯一一个「做一半」通常比「完全不做」更糟的。少一个标签,你损失的是一次优化机会;一个畸形的集群,你损失的可能是页面本身。
正是这种不对称,让我们把它当作构建期问题而不是清单项。下面是我们最常遇到的四类失效模式,按破坏力从大到小排列。
一、跨语言 canonical
破坏性最强,也最容易在不经意间引入。
一个中文页面写着 <link rel="canonical" href="https://example.com/pricing/">,指向英文原版。作者的意图是「这两个是同一个页面」。而这个信号的实际含义是:「这个 URL 是那个 URL 的副本,请收录那一个。」
搜索引擎会照办。中文页面离开索引——不是降权,是消失。
<!-- 错误:zh 页面把 canonical 指向了 en 页面 -->
<!-- 位于 https://example.com/zh/pricing/ -->
<link rel="canonical" href="https://example.com/pricing/" />
<!-- 正确:每个页面 canonical 指向自己,配对交给 hreflang 表达 -->
<link rel="canonical" href="https://example.com/zh/pricing/" />
<link rel="alternate" hreflang="en" href="https://example.com/pricing/" />
<link rel="alternate" hreflang="zh-Hans" href="https://example.com/zh/pricing/" />
<link rel="alternate" hreflang="x-default" href="https://example.com/pricing/" />
canonical 回答的是「哪个 URL 应当代表这份内容」,hreflang 回答的是「应该把哪个版本给这位用户」。这是两个不同的问题,而第一个问题答错是有破坏性的。
二、集群里指向了一个 404
hreflang 是作为一个集合被校验的。如果集群中某个页面返回 404、301 或带有 noindex,页面之间的互认链接就断了——实际观察中,搜索引擎往往会丢弃整个集群的注解,而不只是那条坏掉的边。
这也是为什么这个问题在内容站上格外常见:有人发了一篇新的英文文章,模板按配置为所有语言输出了 alternate,而中文翻译还不存在。于是全站每篇文章都在声明一个从未存在过的 URL。
正确的架构做法是:从内容推导 alternate,而不是从语言表推导。当且仅当某个语言存在已发布的翻译时,页面才为它输出 alternate。这是一次查询,不是一个配置常量。
三、非自反的集群
集群里的每个页面都必须列出集群中的每一个页面,包括它自己。省略自指 hreflang 的页面通常会被直接忽略。
这几乎总是一个模板 bug:有人写了 otherLocales(current) 然后遍历,产出了一个缺少对角线的矩阵。它看起来更整洁,但它不工作。
// 错误:排除了当前语言,集群永远无法被确认
LOCALES.filter((l) => l !== current).map(toAlternate)
// 正确:自反——每个页面列出每一个语言,包括自己
LOCALES.map(toAlternate)
四、x-default 指向了一个跳转器
x-default 是为那些语言和地区都无法给你有用信号的用户准备的兜底。它应该指向一个真实、可索引的页面——通常是同一个页面的主语言版本。
两种常见的错误答案:指向语言选择落地页,等于把你最缺乏信息的访客送到一个岔路口而不是内容;以及指向一个会做地域跳转的根 URL,意味着爬虫会把当天恰好被服务到的那个版本当作规范版本。
如何验证修复
不要靠读模板来验证 hreflang。要针对构建产物验证,因为 bug 恰恰就在模板里。
- 自反性:每个页面的 alternate 里必须包含它自己的 URL。
- 绝对性:相对路径的 hreflang 不被接受。断言每个 href 以
https://开头。 - 可达性:断言每个 hreflang 目标都能在构建产物中找到对应文件。这一条捕获的正是第二类失效,实现只需二十行脚本。
- 互认性:断言若 A 列出了 B,则 B 也列出了 A。
- 唯一 x-default:有且仅有一个,且可解析。
我们在每次构建时对 dist/ 跑完这五条。它并不精妙,但它比我们做过的任何其他检查都抓到了更多真实缺陷。