Nuxt 多语言游戏目录预渲染实践:不向浏览器下发数据库
译注:本文编译自 dev.to 文章《Prerendering a multilingual Nuxt game catalog without shipping the database》,作者分享了将一个包含 153 款游戏的多语言 Nuxt 目录站改造为完全静态预渲染架构的经验。原文链接见文末。
游戏目录看起来很像一个客户端应用:筛选、搜索、卡片、详情页,还有可玩的 iframe。但这并不意味着应该把完整的内容数据库下发给浏览器,让每个爬虫或每台手机在 JavaScript 加载后自行重建页面。
作者最近将一个包含 153 款游戏的 Nuxt 目录本地化为四种语言。公开结果是 676 条可索引路由,但架构上有三条严格约束:
- 每条可索引路由都预渲染为完整 HTML;
- 缺失的本地化内容会让构建失败,而不是回退到英文;
- 浏览器不会收到 Nuxt Content 的 SQLite/WASM 引擎,也不会收到完整的游戏记录。
让路由集合显式化
只依赖爬虫意味着一个意外缺失的链接就可能让某个页面从静态构建中消失。目录本身已有唯一数据源,应当用它生成完整路由列表。
基础集合包含:首页、热门、最新、搜索,以及四个站点/法律页面;每个分类一个页面;每款游戏一个页面。153 款游戏加 8 个分类,共 169 条基础路由。语言映射对英文不加前缀,其他语言分别加 /id/、/it/、/pt-br/ 前缀,英文 slug 在前缀后保持不变。
169 base routes × 4 locales = 676 indexable routes本地化的 404 页面同样预渲染,但标记为 noindex,不进入 sitemap。这份列表同时驱动 Nitro 预渲染和 sitemap 生成器。sitemap 不被当作“页面大概存在”的证据;构建后会检查每个 <loc> 是否对应到实际生成的 HTML 文件。
产品事实保持单一数据源
游戏标题、iframe URL、开发商、日期、嵌入类型和稳定 slug 属于产品事实,译者不应改写。描述、目标、操作说明、技巧、分类文案和法律文本则需要本地化。生成步骤会把翻译与事实字段合并,写出 Nuxt Content 在构建时消费的 Markdown 文档。
对每个非英文语言,生成器期望:153 个游戏文档、8 个分类文档、4 个站点/法律文档。即每个语言 165 个文档,共 495 个本地化 Markdown 文件。缺失键、多余 slug、重复条目或事实漂移都会导致构建错误。
核心规则很简单:可索引的本地化路由不能悄悄借用英文正文内容。可见的回退在应用界面中有用,但当它生成一个向搜索引擎宣称是另一种语言的页面时就很危险。
本地化页面,而非嵌入的游戏
目录可以翻译导航、元数据、操作指南、目标和警告,但它并不拥有每个嵌入游戏的 UI。这条边界应当明确:页面语言会变化,而游戏 iframe 可能仍是英文。试图暗示相反情况会产生误导性的元数据和用户预期。
语言切换器应保留当前路由:
/game/ember-vault/
/it/game/ember-vault/
/pt-br/game/ember-vault/不要根据 Accept-Language 或浏览器设置做重定向。自动重定向会让 URL 对爬虫不稳定,也会让有意选择其他语言的用户感到意外。
从同一语言模型生成 SEO 信号
每个页面都需要自引用 canonical 和完整的 alternate 集合:en、id-ID、it-IT、pt-BR,以及指向英文的 x-default。文档的 lang、Open Graph locale、可见文案和 Schema 的 inLanguage 必须一致,sitemap 也重复同样的 alternate。
这里适合放一个确定性校验器。对全部 676 个 HTML 文件检查:预期的 html lang;精确的 canonical URL;全部四个 hreflang 条目加 x-default;本地化的 Open Graph locale;Schema 语言;没有未解析的消息键;非英文页面上没有已知的英文小节标题。
计数出奇地有价值。如果预期路由数是 676,而校验器只看到 675,构建会在部署前失败。
拆分卡片数据与详情数据
完整游戏记录包含网格永远不需要的字段:iframe URL、长操作说明、特性、开发商信息和相关游戏详情。把这个对象导入共享 composable,可能让整个目录进入客户端 chunk。
构建步骤会投影出一个精简的卡片记录,只含搜索和网格使用的字段:
slug, title, description, category, tags,
thumbnail, embed type, difficulty, updated date, popular详情字段留在服务端/构建端。详情路由在预渲染时接收单个游戏,只序列化该页面所需内容。法律和编辑页面不会仅仅因为共享布局就预加载卡片目录。
再加一个泄漏测试,使用一个已知的、仅详情页才有的 iframe URL。如果这个标记出现在共享 JavaScript chunk 中,说明拆分已经退化。
当所有查询都预渲染时,移除客户端数据库
Nuxt Content 可以下发一个 SQLite/WASM 客户端用于浏览器端查询。如果所有内容查询都在预渲染时运行、结果从页面 payload 水合,那么完全静态的目录并不需要它。
生产构建会把客户端数据库模块别名到一个绝不执行的 stub。构建后检查会在发现以下内容时失败:SQLite WASM 或 worker 产物;公开的内容数据库转储;对完整目录的运行时 fetch;以及在水合后覆盖预渲染描述的旧代码。
最后一项保护的不只是性能。运行时替换可能导致无 JavaScript 时显示一个描述,有 JavaScript 时显示更短且不同的描述,让可访问性和索引变得不可预测。
为剩余 JavaScript 设定预算
国际化有真实的包体成本。懒加载语言字典有帮助,但前提是应用不会在每条路由上预加载所有语言。
同时跟踪最大 chunk 和总输出 JavaScript,并扫描语言 chunk 中是否出现多于一种语言的标记。预算不是通用性能评分,而是防止目录或字典意外重复的绊线。
浏览器检查应直接访问本地化游戏页面,而不是从英文导航过去。这能捕获那些只有在语言包已加载后才正常工作的实现。
也要测试游戏边界
如果播放按钮无法启动 iframe,再完美的本地化外壳也是坏的。对一小组有代表性的自托管游戏,浏览器检查应:打开本地化详情路由;验证本地化的初始 HTML;启动 iframe;发送真实的键盘或触摸输入;观察游戏进度而非仅看耗时;验证暂停、重启、焦点和移动端尺寸。
对于第三方嵌入,应记录其可用性属于外部依赖。不要像处理目录内容那样翻译或打包它们的二进制文件。
本文讨论的参考实现是 SonoTap。可迁移的模式是把静态生成当作完整性边界:路由、翻译、元数据、内容和客户端 payload 都是构建可以计数并拒绝的产物。