本文由 Huifer 撰写——TanStack Ship 的独立开发者与维护者。 2026 年 3 月,我在 Cloudflare Workers 上为 TanStack Start 应用上线了多语言层,结果
/de/pricing在 Google 中沉默了 19 天才被我发现——原因是一个缺失的 return-link bug。此后我又遇到了八种不同的 hreflang 失败场景。这篇指南汇集了所有这些事故的后验,也是我现在每次上线多语言时都会打开的参考手册。已验证来源: Google Search Central — 管理多地区和多语言网站 · Google Search Central — hreflang 文档 · schema.org hreflang 属性 · Ahrefs hreflang 指南 · Moz hreflang 指南 · Semrush hreflang 指南 · Screaming Frog hreflang 验证 · Bing hreflang 支持 · Yandex language meta · TanStack Ship GitHub Organization 最后更新: 2026-08-11 · 更新日志
TL;DR: hreflang 标签告诉搜索引擎应该将哪个语言和地区版本的页面展示给哪个用户。它们存在于三个位置——HTML
<link>标签、XML sitemap,或非 HTML 文件的 HTTP 头。实现很简单,但失败模式却很复杂。以下八大避坑经验涵盖了我在生产环境中调试过的绝大多数 hreflang bug。
hreflang 标签的实际作用
hreflang 是一个信号,而非指令。搜索引擎可以选择使用它,但并非必须。使用正确时,它完成三项具体工作:
- 语言到 URL 的映射。 当柏林的用户的搜索查询命中了
/en/pricing的排名时,hreflang 告诉 Google 优先为德语查询展示/de/pricing,即使英语页面拥有更高的域名权重。 - 翻译页面聚类。 hreflang 将翻译页面归为一组。这可以防止 Google 将英语和德语的定价页面视为重复内容并同时降权。
- 地区定向。 使用地区代码如
en-GB和en-US,你可以告诉 Google 应该展示哪个地区版本——英国页面显示英镑定价,美国页面显示美元定价。
根据 Google Search Central 文档,hreflang 是 Google 官方唯一正式支持的跨语言页面关系信号。
hreflang vs canonical——最常见的混淆
rel="canonical"说的是:"这两个 URL 是同一页面,选一个。"rel="alternate" hreflang="xx"说的是:"这些 URL 是翻译版本,根据用户展示对应的那一个。"
hreflang 必须配合自引用 canonical 一起使用。将所有语言版本的 canonical 都指向英语页面是错误的——它告诉 Google 英语页面是所有语言版本的规范版本,与 hreflang 集群相矛盾。多语言 SEO 策略指南 详细介绍了 canonical 与 hreflang 的配合方式。
hreflang 标签的结构
rel="alternate" hreflang="xx" 模式
<link rel="alternate" hreflang="en" href="https://tanstackship.com/en/pricing" />
<link rel="alternate" hreflang="de" href="https://tanstackship.com/de/pricing" />
<link rel="alternate" hreflang="zh-Hans" href="https://tanstackship.com/zh-Hans/pricing" />
<link rel="alternate" hreflang="x-default" href="https://tanstackship.com/en/pricing" />
每个标签表示:"这个页面在 X 语言下有一个备选版本,位于此 URL。"集群中的每个页面都必须列出所有其他页面——包括自引用。
语言代码 vs 地区代码
根据 Google hreflang 文档,有两种格式:
- 仅语言:
en、de、zh。同一种内容服务于所有使用该语言的地区。 - 语言-地区:
en-GB、en-US、pt-BR、zh-Hans、zh-Hant。内容因地区而异——货币、单位、法律条款。
分隔符是连字符。不是下划线。有一次我写成了 en_US,这个 bug 过了四天才暴露,因为 Google 对无效代码是静默忽略的。详见避坑 #2。
x-default——没人设置的后备方案
x-default 是规范中的逃生舱。它告诉搜索引擎在没有其他版本匹配用户语言时应该展示哪个页面。通常设置为语言切换页面或默认语言落地页。如果跳过它,Google 会自行判断——而这个判断约 30% 的情况是错的。Ahrefs hreflang 指南 详细解释了 x-default 的语义。
实现方式对比
HTML head 标签
最简单的方式。每个页面渲染指向所有其他语言版本的 <link rel="alternate"> 标签。当内容有路由时需要每个页面动态生成——没有 SSR 的纯客户端 React SPA 向爬虫发送的是没有 hreflang 标签的空 HTML。这是现代 SPA hreflang 失败的根本原因。
XML sitemap 中的 xhtml:link
集中式声明。每个 URL 在 sitemap 中只出现一次,每个备选版本对应一条 <xhtml:link rel="alternate" hreflang="x" href="..."/>。Google 推荐大型多语言站点使用此方式。可扩展性强。
HTTP Link 头
适用于非 HTML 文件——PDF、仅图片页面、视频清单:
Link: <https://tanstackship.com/en/whitepaper.pdf>; rel="alternate"; hreflang="en",
<https://tanstackship.com/de/whitepaper.pdf>; rel="alternate"; hreflang="de"
仅在 HTML 和 sitemap 不可行时使用。大多数团队会忘记维护它。
最佳实践:HTML 和 sitemap 同时使用
HTML 标签用于小型站点的快速发现。sitemap 用于大型站点的完整性保障,并在每个页面渲染失败时作为兜底。Google 爬虫会优先使用先发现的信号。
在单页应用中实现 hreflang
SPA 是大多数 hreflang 失败的起点。静态 HTML 文件可以硬编码标签。SPA 不行。
为什么 SPA 会破坏简单场景
纯客户端 React 应用在服务端渲染时 <head> 是空的,在水合(hydration)之后才填充。Googlebot 可以等待,但渲染后的 DOM 中必须包含正确的 <link> 标签——而许多 SPA 路由默认不生成语言备选链接。
三种解决方案,按 SEO 可靠性排序:
- 服务端渲染(推荐)。 TanStack Start、Next.js App Router 和 Remix 都支持在每条路由中注入 head 标签。hreflang 标记在初始 HTML 响应中就已存在,爬虫立即可见。这是我在 2026 年 3 月实际使用的方式——详见 TanStack Ship i18n 模块。
- 按语言预渲染。 在构建时为每条路由、每种语言生成静态 HTML 文件。标签直接固化在文件中。
- 客户端注入后验证。 在路由变化后通过 JavaScript 注入。Google 已公开声明 HTML link 标签形式的 hreflang 是更优选项。
TanStack Start 根路由中的服务端注入
// src/lib/hreflang.ts
interface HreflangConfig {
baseUrl: string
currentPath: string
locales: readonly string[]
defaultLocale: string
}
export function buildHreflangLinks(config: HreflangConfig): Array<{ lang: string; href: string }> {
const { baseUrl, currentPath, locales, defaultLocale } = config
const normalizedPath = stripLocalePrefix(currentPath, locales)
const links = locales.map((locale) => ({
lang: locale,
href: `${baseUrl}/${locale}${normalizedPath}`,
}))
links.push({
lang: "x-default",
href: `${baseUrl}/${defaultLocale}${normalizedPath}`,
})
return links
}
function stripLocalePrefix(path: string, locales: readonly string[]): string {
const re = new RegExp(`^/(${locales.join("|")})(?=/|$)`)
return path.replace(re, "") || "/"
}
// src/routes/__root.tsx
export const Route = createRootRoute({
head: ({ match }) => {
const links = buildHreflangLinks({
baseUrl: "https://tanstackship.com",
currentPath: match.pathname,
locales: ["en", "de", "zh-Hans", "es"],
defaultLocale: "en",
})
return {
links: links.map(({ lang, href }) => ({
rel: "alternate",
hreflang: lang,
href,
})),
}
},
})
爬虫在初始 HTML 中收到完整的 hreflang 集群。无需 JavaScript 参与。
动态语言切换时不丢失备选链接
当用户点击语言切换器、URL 在客户端变化时,新路由必须重新发出集群。如果路由缓存了上一个路由的 head 标签,备选链接就会过时。TanStack Router 的 head 函数是 per-match 的,导航时会重新运行——但如果缓存了 head context,就必须在语言变化时使缓存失效。这个 bug 我犯过两次。
我实际踩过的八大避坑
每一条都是我曾在 TanStack Ship 基础设施上调试过的真实生产 bug。
避坑 #1——缺失的回链
全球排名第一的 hreflang 失败原因。如果页面 A 用 hreflang="de" 指向页面 B,页面 B 必须用 hreflang="en" 指回页面 A。这种关系是双向的。
2026 年 3 月,我上线了指向所有语言版本相互指向的 hreflang 标签——唯独在向集群添加 es 时忘记了重新生成德语页面的 head。德语页面仍然只指向英语和它自己。Google 判定整个集群是单向的并将其忽略。/de/pricing 沉默了 19 天。
修复方法:一个单元测试,断言每个页面的备选链接数量等于 locales.length + 1(+1 是 x-default)。我现在在每个涉及语言配置的 PR 中都附带这个测试。
避坑 #2——语言代码中的下划线
规范是 en-US,不是 en_US,也不是 EN-US。Google 静默忽略无效代码——没有控制台错误,没有 Search Console 警告,标签就是无法匹配任何内容。根据 schema.org hreflang 属性,格式遵循 BCP 47,连字符是标准分隔符。
避坑 #3——canonical 与 hreflang 信号冲突
需要自引用 canonical。将所有语言版本的 canonical 都指向英语页面是一个 bug——它告诉 Google 英语页面是所有语言的规范版本,与 hreflang 集群相矛盾。Moz hreflang 指南 有一个清晰的 canonical-hreflang 配对示例。
避坑 #4——忘记 x-default
最常缺失的标签。没有 x-default 的 hreflang 集群在技术上是有效的,但在运营上是不完整的。
避坑 #5——地区代码与页面内容语言不匹配
如果 HTTP Content-Language 头声明了 en,但 hreflang 标签声明了 en-US,两种信号就产生了分歧。Google 优先采信 hreflang,但这种不匹配会让审计工具感到困惑。选择一个权威数据源。
避坑 #6——仅有国家代码而没有语言代码
hreflang="US" 是无效的。第一段必须是语言代码。hreflang="en-US" 才是有效的。这个错误来自将 hreflang 与地区重定向或 CDN 地理定向混淆。
避坑 #7——客户端导航时动态路由的过期问题
前文已提过。当用户在客户端切换语言而没有完整页面重新加载时,之前路由的 head 标签可能会持续存在。每次语言变化时都必须重新发出集群,并断言导航后 100ms 内更新到位。
避坑 #8——实现了页面 A 的 hreflang 但没有实现页面 B 的 hreflang
与避坑 #1 相同,但单独列出是因为失败模式不同。页面 A 指向页面 B,但页面 B 完全没有 hreflang 标签——可能因为它是一个没有 head 管理的营销落地页。集群不完整;Google 将页面 B 视为未翻译。
验证:我如何验证 hreflang
三个检查,按成本从低到高排列。总耗时五分钟。
Google Search Console——国际定向
国际定向报告 显示每个 URL 定向的语言和国家。如果实现正确,每个页面都出现在其声明的语言下。如果某个页面缺失或出现在错误的语言下,说明集群有问题。
注意事项:报告更新有数周的延迟。新标签可能需要 3–6 周才会出现。Semrush hreflang 指南 讨论了这种延迟,建议在上线时运行一次,60 天后再运行一次。
Screaming Frog hreflang 验证
Screaming Frog SEO Spider 爬取整个网站,提取每个 hreflang 标签,并验证双向集群。它会标记缺失的回链、无效的语言代码和非规范的 hreflang URL。免费版最多覆盖 500 个 URL。
这是我的上线前检查。每个 hreflang PR 合并前都要经过 Screaming Frog 验证。
curl 快速检查
快速检查:
curl -s https://tanstackship.com/en/pricing | grep 'hreflang='
你应该看到每种语言对应一个 <link rel="alternate" hreflang="..."> 标签加上 x-default。如果数量少于预期,说明集群不完整。如果看到 hreflang="en_US",说明避坑 #2 已在生产中出现。
结语及优先上线清单
如果你在还没上线 hreflang 之前读到这篇文章,以下是我用血泪教训总结出的操作顺序:
- 在编码前确定语言和语言-地区格式。 使用带连字符的 BCP 47。从第一天就加上
x-default。 - 在服务端生成 hreflang 集群。 纯客户端注入对 SEO 不可靠。
- 写双向测试。 断言每个页面都有
locales.length + 1个备选链接。在每个 PR 中运行。 - 将 Screaming Frog 加入上线前检查清单。 五分钟。覆盖所有问题。
- 计划 60 天后的 Search Console 复审。 国际定向报告有延迟。
关于更广泛的多语言架构——语言路由、消息提取、货币处理——请参阅 i18n 架构模式 帖子,它完整介绍了整个技术栈。SaaS 全球化策略 帖子涵盖了定位层面:先本地化什么、跳过什么、如何衡量每个市场的 ROI。
hreflang 很小。但它也是技术 SEO 中失败率最高的环节之一,因为失败模式是静默的——Google 不会警告你,Search Console 延迟数周,错误语言的流量看起来像是本地化 bug 而不是 SEO bug。单元测试加 Screaming Frog 检查将静默失败变成了显式报警。
关于本文
- 作者:Huifer,TanStack Ship 的独立开发者与维护者。自 2023 年以来已在 Cloudflare Workers 上线了 12+ 个生产级 TanStack 应用。TanStack Ship i18n 模块 中的 hreflang 实现自 2026 年 3 月起已在生产环境运行。
- 已验证来源: Google Search Central — hreflang 文档 · schema.org hreflang 属性 · Ahrefs hreflang 指南 · Moz hreflang 指南 · Semrush hreflang 指南 · Screaming Frog hreflang 验证 · Bing hreflang 支持 · Yandex language meta · TanStack Ship GitHub Organization
- 最后更新: 2026-08-11 · 更新日志
开始使用 TanStack Ship — TanStack Start SaaS 入门套件,默认包含 hreflang、canonical 配对、语言路由以及本文提到的验证测试。查看定价 · 对比替代方案 · 阅读博客