打开一个多语言商品页,<head> 中至少应该看到类似下面的内容:
<link rel="canonical" href="https://example.com/zh-cn/product" />
<link rel="alternate" hreflang="zh-CN" href="https://example.com/zh-cn/product" />
<link rel="alternate" hreflang="en-US" href="https://example.com/en-us/product" />
<link rel="alternate" hreflang="x-default" href="https://example.com/product" />
这几行代码不会翻译页面,也不会替用户跳转。它们解决的是另一个问题:告诉搜索引擎,这几个 URL 是同一份内容面向不同语言或地区的版本。
对前端开发者来说,hreflang 的语法并不复杂。真正容易出错的是 URL 建模、页面间的双向关系,以及它和 canonical 的配合。
hreflang 到底解决什么问题
假设网站同时提供三个页面:
https://example.com/zh-cn/product
https://example.com/en-us/product
https://example.com/en-gb/product
它们介绍的是同一款产品,但语言、货币、配送范围或文案有所不同。搜索引擎需要知道:
- 中文用户更适合看到
zh-CN页面; - 美国英语用户更适合看到
en-US页面; - 英国英语用户更适合看到
en-GB页面; - 无法匹配时应该使用哪个默认入口。
hreflang 就是这组对应关系的声明。它主要帮助搜索引擎选择更合适的搜索结果,而不是提高页面排名的直接开关。
一组正确的声明长什么样
中文页面可以这样写:
<head>
<link rel="canonical" href="https://example.com/zh-cn/product" />
<link rel="alternate" hreflang="zh-CN" href="https://example.com/zh-cn/product" />
<link rel="alternate" hreflang="en-US" href="https://example.com/en-us/product" />
<link rel="alternate" hreflang="en-GB" href="https://example.com/en-gb/product" />
<link rel="alternate" hreflang="x-default" href="https://example.com/product" />
</head>
英文页面也要输出相同的完整映射,而不是只放一条指向中文页面的链接。
这里有三个关键规则。
1. 页面必须声明自己
zh-CN 页面需要包含指向自身的 zh-CN 条目。不要因为当前语言已经明确,就省略这一行。
2. 不同版本必须互相引用
如果页面 A 声明页面 B 是自己的英文版本,那么页面 B 也应该声明页面 A 是自己的中文版本。这通常被称为返回链接或双向引用。
只在默认语言页面维护一份 hreflang,其他语言页面什么都不写,会让整组关系变得不完整。
3. href 应该是可索引的绝对 URL
推荐使用完整地址:
<link rel="alternate" hreflang="ja" href="https://example.com/ja/product" />
目标 URL 应该返回正常页面,而不是 404、登录页或多次重定向。它也不应该被 robots.txt 或 noindex 阻止。
语言代码和地区代码怎么写
常见格式是:
语言代码-地区代码
例如:
zh-CN
zh-TW
en
en-US
en-GB
ja
ko
语言部分使用 ISO 639-1 代码,地区部分使用 ISO 3166-1 Alpha-2 代码。zh 表示中文,CN 表示中国大陆。
一个常见错误是写成 cn 并把它当成中文。地区不能替代语言。如果网站只有一个英文版本,直接使用 en 即可;只有当内容确实因地区而不同时,才需要继续区分 en-US 和 en-GB。
x-default 应该指向哪里
x-default 表示没有任何语言或地区匹配时使用的默认 URL:
<link rel="alternate" hreflang="x-default" href="https://example.com/product" />
它通常指向以下页面之一:
- 语言选择页;
- 不带地区前缀的默认入口;
- 根据用户选择进行分流的页面。
不要把 x-default 理解为英语。它表达的是兜底关系,而不是某一种语言。
hreflang 和 canonical 不是二选一
多语言 SEO 中最常见的配置错误,是让所有语言页面的 canonical 都指向默认语言:
/zh-cn/product -> canonical 到 /en-us/product
/en-us/product -> canonical 到 /en-us/product
这相当于告诉搜索引擎,中文页面不是独立的规范页面;与此同时,hreflang 又声称它是应该提供给中文用户的版本。两组信号互相冲突。
通常,每个语言页面应该 canonical 到自己:
/zh-cn/product -> canonical 到 /zh-cn/product
/en-us/product -> canonical 到 /en-us/product
然后由 hreflang 描述这些规范页面之间的语言关系。
React 中如何生成
如果站点使用 React,可以先把语言与 URL 的对应关系建模,再渲染 <link>:
const alternateUrls = [
{ locale: 'zh-CN', url: 'https://example.com/zh-cn/product' },
{ locale: 'en-US', url: 'https://example.com/en-us/product' },
{ locale: 'en-GB', url: 'https://example.com/en-gb/product' },
{ locale: 'x-default', url: 'https://example.com/product' },
]
export function HreflangLinks() {
return alternateUrls.map(({ locale, url }) => (
<link key={locale} rel="alternate" hrefLang={locale} href={url} />
))
}
注意 JSX 中的属性名是 hrefLang,最终生成的 HTML 属性仍然是 hreflang。
更重要的是,这些标签应该在 SSR 或 SSG 阶段进入初始 HTML。不要等页面挂载后再通过 useEffect 插入。SEO 元数据不应该依赖客户端 JavaScript 才能出现。
Next.js App Router 的实现
静态页面可以直接使用 Metadata API:
import type { Metadata } from 'next'
export const metadata: Metadata = {
alternates: {
canonical: 'https://example.com/zh-cn/product',
languages: {
'zh-CN': 'https://example.com/zh-cn/product',
'en-US': 'https://example.com/en-us/product',
'en-GB': 'https://example.com/en-gb/product',
'x-default': 'https://example.com/product',
},
},
}
文章页、商品页等动态路由,可以使用 generateMetadata:
import type { Metadata } from 'next'
const siteUrl = 'https://example.com'
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string; slug: string }>
}): Promise<Metadata> {
const { locale, slug } = await params
return {
alternates: {
canonical: `${siteUrl}/${locale}/${slug}`,
languages: {
'zh-CN': `${siteUrl}/zh-cn/${slug}`,
'en-US': `${siteUrl}/en-us/${slug}`,
'en-GB': `${siteUrl}/en-gb/${slug}`,
'x-default': `${siteUrl}/${slug}`,
},
},
}
}
实际项目中,不要默认每篇内容都拥有所有语言版本。如果某篇文章尚未翻译,就不应生成一个不存在的目标 URL。语言列表最好来自 CMS 中真实存在的翻译关系,而不是写死全部站点语言。
例如接口可以返回:
type Translation = {
locale: string
slug: string
}
页面只根据 translations 生成可用条目。这样可以避免发布流程不同步时产生大量 404 链接。
URL 结构比标签更早决定成败
前端添加 <link> 之前,团队需要先确定每个语言版本是否拥有稳定、唯一、可直接访问的 URL。常见方案包括:
example.com/zh-cn/product
zh-cn.example.com/product
example.cn/product
三种方案都能配合 hreflang。真正需要避免的是只有一个 URL,然后根据 Cookie 或浏览器语言返回不同内容。
如果 https://example.com/product 有时返回中文、有时返回英文,搜索引擎无法稳定抓取各语言版本,用户也无法分享一个明确的语言地址。前端路由和服务端渲染应该保证:同一个 URL 对应稳定的主要语言内容。
自动语言跳转也要谨慎。强制按照 IP 或 Accept-Language 重定向,可能阻止爬虫访问其他版本。更稳妥的方式是提供语言建议,同时保留用户访问和切换任意版本的能力。
上线前检查清单
可以逐页检查以下项目:
- 每种已发布语言都有独立且稳定的 URL;
- 每个页面都包含指向自身的
hreflang; - 同组页面输出相同的完整语言映射;
- 页面之间存在双向引用;
href使用绝对 URL;- 目标页面返回 200,且允许索引;
- 语言和地区代码有效;
- 每个语言页面的 canonical 通常指向自身;
x-default指向明确的默认入口;- 初始 HTML 中已经存在这些标签;
- 未发布的翻译不会被加入映射。
还可以直接查看服务端返回内容,而不是只看浏览器 Elements 面板:
curl -s https://example.com/zh-cn/product | grep -i hreflang
Elements 面板展示的是 JavaScript 执行后的 DOM。curl 或“查看网页源代码”更接近搜索引擎最先收到的 HTML,也更容易发现元数据只在客户端生成的问题。
最后记住三件事
第一,hreflang 声明的是页面之间的语言和地区关系,不负责翻译,也不负责跳转。
第二,同一组页面必须自引用、互相引用,并且只包含真实可访问的翻译版本。
第三,canonical 和 hreflang 各自解决不同问题。让每个语言页面 canonical 到自己,再用 hreflang 把它们连接起来,通常是最清晰、最稳定的配置。