国际化网站的路由和 SEO 怎样做?语言前缀、hreflang、x-default 和 Accept-Language 重定向要注意什么?
每个语言版本要有自己的 URL,常见做法是 /zh-CN/、/en/ 这样的语言前缀。用 hreflang 告诉搜索引擎这些页面互为不同语言的版本,每个版本都要列出自己和其他所有版本,并且互相指回,否则会被忽略;x-default 指定没有匹配语言时的兜底页面。Google 建议不要按猜测的语言自动重定向,因为 Googlebot 默认从美国的 IP 抓取、不带 Accept-Language,自动跳转可能让其他语言版本无法被抓到。
简答
比如一个产品站有中文、英文、日文三个版本。常见的路由是给 URL 加语言前缀,像
/zh-CN/pricing、/en/pricing,每个语言版本都有一个固定的地址。Google 也建议每种语言用不同的 URL,而不是靠 Cookie 或浏览器设置在同一个地址上切换语言。搜索引擎要知道这几个页面是同一内容的不同语言版本,就要用 hreflang。在每个页面的
<head>里用<link rel="alternate" hreflang="en" href="...">列出所有语言版本,包括自己;再加一条hreflang="x-default",给没有匹配语言的用户一个兜底页,比如语言选择页。这样做的好处是,搜索结果能给不同语言的用户展示对应的版本,用户分享的链接也总是同一种语言。
要注意三点。第一,hreflang 必须双向,A 指向 B,B 也要指回 A,否则会被忽略。第二,地址要写完整的绝对 URL,语言代码用 ISO 639-1,地区用 ISO 3166-1 Alpha 2,不能只写地区。第三,不要按
Accept-Language或 IP 自动重定向到别的语言版本,Googlebot 默认从美国抓取、不带Accept-Language,自动跳转可能让其他版本抓不到;更好的做法是提示用户并提供切换链接。
URL 结构怎样选
Google 的多语言站点文档比较了几种 URL 结构:
| 结构 | 例子 | 文档列出的特点 |
|---|---|---|
| 国家代码顶级域名 | example.de | 地区信号最清楚,但贵,需要更多基础设施,一个域名只能面向一个国家 |
| 子域名 | de.example.com | 容易搭建,可以放在不同服务器上,但用户不一定看得出 de 是语言还是国家 |
| 子目录 | example.com/de/ | 容易搭建,维护成本低,同一个主机,但不同版本不太好拆开部署 |
| URL 参数 | example.com?loc=de | 不推荐 |
前端项目最常见的是子目录,也就是语言前缀。它和单页应用的路由天然契合:路由的第一段就是语言,下面的路径各语言保持一致,切换语言只换前缀。
为什么不推荐只靠 Cookie 切换?Google 文档的建议是每个语言版本用不同的 URL,而不是用 Cookie 或浏览器设置改变同一个页面的语言。因为搜索引擎抓到的只有一个 URL、一种语言,其他语言的内容就无法被单独收录。
hreflang 怎样写
Google 支持三种等价的写法:HTML 的 <link> 标签、HTTP 响应头 Link、站点地图里的 <xhtml:link>。三种效果一样,选一种维护就行,同时用三种没有额外好处,反而更难维护。下面用 HTML 标签的写法生成三个语言版本的链接:
// 每个语言版本都要列出全部版本(包括自己),再加一个 x-default。
const origin = 'https://example.com';
const locales = ['zh-CN', 'en', 'ja'];
function alternates(path) {
const links = locales.map((lang) => `<link rel="alternate" hreflang="${lang}" href="${origin}/${lang}${path}" />`);
links.push(`<link rel="alternate" hreflang="x-default" href="${origin}${path}" />`);
return links;
}
const links = alternates('/pricing');
console.log(links.length); // 4
console.log(links[1]); // <link rel="alternate" hreflang="en" href="https://example.com/en/pricing" />
// 按 Accept-Language 的 q 值排序,找出第一个支持的语言;没有请求头时返回 null。
function pickLocale(header) {
if (!header) return null;
const wanted = header
.split(',')
.map((part) => {
const [tag, q = 'q=1'] = part.trim().split(';');
return { tag: tag.toLowerCase(), q: Number(q.split('=')[1]) };
})
.sort((a, b) => b.q - a.q);
for (const { tag } of wanted) {
const exact = locales.find((lang) => lang.toLowerCase() === tag);
if (exact) return exact;
const base = locales.find((lang) => lang.toLowerCase() === tag.split('-')[0]);
if (base) return base;
}
return null;
}
console.log(pickLocale('en-US,en;q=0.9,zh-CN;q=0.8')); // en
console.log(pickLocale('fr-CH, fr;q=0.9, ja;q=0.5')); // ja
console.log(pickLocale(undefined) ?? '无'); // 无
三个语言加一个 x-default,一共 4 条。这 4 条在中文、英文、日文三个页面上都要出现,内容完全一样。Google 文档的规则是:每个语言版本都要列出自己和其他所有版本,地址要写完整,包括 https://,不能写成 //example.com/foo 或 /foo。
为什么要求自己也列上、而且双向?Google 解释说,如果两个页面没有互相指向,这组标注就会被忽略,这是为了防止别的网站随便声明自己是你页面的另一个语言版本。
x-default 的意思是“页面上列出的语言都不匹配时,用这个”。Google 建议给语言选择页、或者会自动跳转的首页加上它。
语言代码也有规定:先写 ISO 639-1 的语言代码,可以再加一个 ISO 3166-1 Alpha 2 的地区代码,比如 en-GB。只写地区不行,es-419 这种不在这两个标准里的代码也不支持。大小写不敏感,但地区代码习惯写成大写。另外 Google 说明,它不用 hreflang 或 HTML 的 lang 属性来判断页面是什么语言,而是用算法判断,所以页面内容本身要真的是对应的语言。
Accept-Language 自动重定向为什么要小心
后半段的 pickLocale 是很多网站首页的做法:读请求里的 Accept-Language,按 q 值从高到低找第一个支持的语言,再跳转过去。第二个例子里,用户首选法语,网站不支持,就退到权重 0.5 的日语。q 值的写法见 MDN 的 Accept-Language 说明。
问题出在最后一行。Google 的“按语言区域自适应的网页”文档写明,Googlebot 默认的 IP 看起来在美国,而且发请求时不设置 Accept-Language。如果网站按请求头或 IP 决定显示哪种语言,Google 可能无法抓取、收录全部语言版本。
所以 Google 在多语言站点文档里建议:不要按你猜测的用户语言,把用户从一个语言版本自动跳转到另一个,这会让用户和搜索引擎都看不到全部版本;应该在页面上放其他语言版本的链接,让用户自己选。如果根路径 / 这个没有语言的入口确实要跳转,前面提到 Google 建议给这类会自动跳转的首页加上 x-default;具体的语言页面则不要跳转,最多显示一个切换语言的提示。按 Accept-Language 返回不同内容时,还要带上 Vary,见《HTTP 内容协商会怎样影响前端资源和接口响应?》。
canonical 和 hreflang 怎样配合
每个语言版本是不同的内容,canonical 应该指向它自己,而不是统一指向英文版。Google 的重复网址文档说,使用 hreflang 时,要指定同一种语言的规范页面;带 hreflang 属性的 rel="canonical" 不会被用于规范化,语言版本之间的关系要用 rel="alternate" hreflang 表达。
同一种语言出现在多个 URL 上时,比如 example.de/ 和 example.com/de/ 内容相似,Google 建议选一个首选版本,用 rel="canonical" 和 hreflang 一起标注。SEO 的其他手段见《前端 SEO 有哪些具体手段?》。
常见追问
单页应用切换语言时,hreflang 要怎样更新?
hreflang 是给搜索引擎看的,关键是抓取到的 HTML 里要有正确的标签。服务端渲染或静态生成时,每个语言页面直接输出完整的
<link>;纯客户端渲染的话,要保证渲染后的<head>里有这些标签。渲染方式对 SEO 的影响见《前端渲染方式怎样影响 SEO 和部署?》。
页面 URL 里的路径要不要翻译成当地语言?
可以。Google 文档说 URL 里用本地化的词、用国际化域名都没问题,但要用 UTF-8 编码,链接时正确转义。代价是各语言的路径不再一一对应,切换语言时要多做一层路径映射。
为什么设置了 hreflang,搜索结果还是显示别的语言版本?
先检查是不是双向的:Google 说明没有互相指回的标注会被忽略。再检查地址是不是完整的绝对 URL、语言代码是不是合法、标签是不是在
<head>里。Google 不靠 hreflang 判断页面语言,如果页面主要内容其实没翻译,它也可能被当成重复页面。