Writing

@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 双语站选它。

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

这些坑单个看都不大,但都是「不报错、悄悄错」的类型——第一版里它们各自藏了几天才被发现。

More