@nuxtjs/i18n 前缀策略踩坑
给 Nuxt 站点做中英双语,prefix 策略是最常见的方案:/zh/blog、/en/blog 各是一套 URL,默认语言 zh。看起来简单,用起来有一串坑。这篇记录我在实际项目里踩过的六个。
先交代为什么选 prefix,不是其他三种:
| 策略 | URL | 取舍 |
|---|---|---|
| prefix | /zh/blog /en/blog |
每种语言独立 URL,SEO 最友好;默认语言多一层路径 |
| subpath | /blog(zh)/en/blog |
默认语言没有前缀,其余语言带——SEO 次优,两种 URL 形态不一致 |
| query | /blog?lang=zh |
参数不参与收录,SEO 最差 |
| domain | zh.site.com en.site.com |
独立域名权重分散,还要额外解析和证书 |
对内容站来说 prefix 是「双语内容各自有稳定 URL」和「成本」之间的平衡点——这也是为什么多数 Nuxt 双语站选它。
坑 1:语言 cookie 会挡 CDN 缓存
detectBrowserLanguage 会在响应上种一个 i18n_redirected cookie,用来记住用户的语言偏好。这个 cookie 有个副作用:带 Set-Cookie 的响应不会被 CDN 缓存(Cloudflare Pages 上连配了缓存规则都会被拒)。
也就是说,「给页面加缓存头提速」和「i18n 记忆语言」这两件事在 Pages 上直接冲突。解法是绕开 CDN,在 worker 里用 Cache API 自己管(见缓存那篇),或者接受页面不缓存、只缓存接口。
坑 2:canonical 要指回无参数版本
合集筛选走查询参数(/blog?c=tech),那是同一份内容的子集,不该各自成为可收录的 URL。canonical 一律指回未筛选的 /blog:
const pageUrl = computed(() => `${siteConfig.url}${route.path}`)
// 用 route.path 而不是 fullPath:?c=tech 是同一份内容的子集
坑 3:og 图片必须是绝对地址
相对路径会被大多数抓取器丢弃。og:image 要拼完整地址:
const ogImage = computed(() => `${siteConfig.url}/images/home/headshot.webp`)
坑 4:模板里用 t() 而不是 $t()
legacy: false 下,$t 在模板里有类型缺口——vue-tsc 检查模板时会报 TS2339,本地类型补丁也救不回来。t() 反而带着真实的 { zh, en } locale 类型,模板里一律用它。
坑 5:locale JSON 的缩进必须是 4 空格
用脚本批量改语言包时,JSON.stringify(j, null, 2) 会把整个文件重排,产生几百行纯格式噪音 diff,git blame 直接报废。要用 null, 4,并且保留原有的行尾符。
坑 6:站外链接的本地化
子站地址带 {locale} 占位符(比如 tools.greatpi.dev/{locale}/colorx),渲染时用一个统一的函数替换:
export function resolveProjectUrl(link: string, locale: string): string {
return link.replace('{locale}', locale)
}
不要在每个消费方各写一份替换——两处漂移是迟早的事。
总结
- 语言 cookie 和 CDN 缓存冲突,页面缓存要走 Cache API
- canonical 用 route.path 而不是 fullPath,筛选参数不参与收录
- og 图片绝对地址,模板用 t() 不用 $t()
- locale JSON 4 空格缩进,站外链接统一 resolveProjectUrl
这些坑单个看都不大,但都是「不报错、悄悄错」的类型——第一版里它们各自藏了几天才被发现。