去年春天,一个客户给我一份任务说明,这在 2022 年会让我叹气。五种语言。十一个市场。一个开发者。"我们已经设置好 WPML 了,"他说,那种信心十足的语气告诉你他从来没真正看过他的 Lighthouse 分数。
我在第二天就删除了 WordPress 安装。用 Astro 重建了整个网站。八周后,其中三个区域已经在谷歌首页。第四个在一个月内跟上。那次经历基本上结束了我与 WordPress 在多语言工作中的合作。
好的。这就是我现在的做法。
为什么 WordPress 多语言在性能上持续失利
听我说,我不喜欢贬低 WordPress。Seahawk 已经建立了数百个 WP 网站,我们可能还会建立数百个。但多语言项目上的性能差距确实很难为之辩护。
WPML 和 Polylang 都是通过在数据库层生成翻译文章并通过 PHP 进行路由来工作的。每个请求都会命中服务器。即使使用完整页面缓存(WP Rocket、W3TC 或你偏好的任何工具),你仍然要应对 PHP 初始化开销、低流量语言页面的缓存未命中,以及一旦超过两种语言就会变成噩梦的 hreflang 管理。
我测试了一个客户的五语言 WordPress 网站,意大利语版本的首字节时间(Time to First Byte)是 1.3 秒。同样的内容,用 Astro 重建为完全静态的 HTML:94ms。这不是边际改进。这是完全不同档次的产品。
Core Web Vitals 是确认的排名因素,从 CDN 边缘节点提供的静态文件在这场竞争中总是赢家。
Astro 的内置 i18n 路由(以及为什么它改变了一切)
Astro 在 v3 中推出了适当的 i18n 路由,并在 v4 中进行了显著改进。到 2026 年我们还在运营时,这个 API 已经很稳定,而且使用起来真正愉快。
这是我使用的基本设置。你的 astro.config.mjs 需要一个 i18n 块:
`` i18n: { defaultLocale: 'en', locales: ['en', 'de', 'fr', 'it', 'es'], routing: { prefixDefaultLocale: false } } ``
这一个配置告诉 Astro 在 / 提供英文,在 /de/ 提供德文,在 /fr/ 提供法文,以此类推。没有插件。没有额外的包。它是内置的。
prefixDefaultLocale: false 是刻意的。我大多数客户的主要受众是英语使用者,所以我不想让 /en/ 污染规范的 URL 结构。Google 可以通过 hreflang 来解析这种关系。
真正可扩展的文件结构
这正是大多数教程出错的地方。他们展示一个扁平的 pages/[locale]/index.astro,然后就完事了。对于宣传页面还行。一旦你有 40 条路由就会崩溃。
我的做法是:
`` src/ pages/ index.astro (English) about.astro (English) [locale]/ index.astro about.astro i18n/ en.json de.json fr.json it.json es.json ``
i18n/ 目录用扁平 JSON 文件保存你的翻译字符串。en.json 是唯一的真理来源。我对着它编写其他所有语言。de.json 里少了一个 key?德语页面会回退到该字符串的英文版本,而不是抛出错误,因为我添加了一个小的 t() 工具函数来优雅地处理回退。
翻译层:保持简单
我试过 Paraglide JS、i18next 和一些其他的。对于 Astro 静态构建,我真诚的建议是:别过度设计。
一个简单的工具文件,导入语言环境 JSON 并返回一个 getter 函数,足以应对 90% 的项目。如果你需要类型安全的翻译,并且你的团队规模足够在乎编译时缺失 key 的错误,Inlang 的 Paraglide JS 值得考虑。对于个人工作或小型代理,这是不必要的开销。
我在乎的是:保持翻译文件扁平而不是深层嵌套。"hero.cta.button.label" 作为 key 听起来很井井有条,直到你在晚上 11 点搜索它。"heroCTALabel" 很丑但容易找到。
处理复数和动态字符串
德语复数形式会让你谦虚。如果你涉足阿拉伯语也会(我曾帮一个客户扩展到阿联酋,光是数字格式就花了整个下午)。
对于复数形式,我使用一个小的内联帮助函数:
`` export function plural(count, one, other) { return count === 1 ? one : other; } ``
粗糙吗?是的。但它涵盖了欧洲语言的 95% 的情况。如果你要支持阿拉伯语、俄语或波兰语,你需要正确的 CLDR 复数规则。Unicode CLDR 项目对这些规则进行了详尽的记录。根据这些规范构建你的复数帮助函数,否则你会搞错。
hreflang:每个人都搞错的部分
没错。这正是多语言 SEO 的成败之处。
hreflang 告诉 Google 应该将页面的哪个版本提供给哪个语言地区。搞错了,你要么会自我蚕食排名,要么会把德文版本提供给法语使用者。我在接手的网站上两种情况都见过。
规则:
- 每个页面都必须为每个语言地区(包括它本身)包含 hreflang 标签。
- 这些标签必须是双向的。如果
/de/about引用 /fr/about,那么/fr/about也必须引用/de/about。 - 包含一个
x-default标签,指向你的默认语言地区。 - 始终使用绝对网址。一定要用。
在 Astro 中,我在 BaseLayout.astro 组件内生成这些标签,每个页面都会导入它。我传入当前路径,循环遍历区域设置数组,然后自动输出 <link rel="alternate"> 标签。无需手动维护。
缺少 x-default 标签是我看到的最常见的错误。Google 自己的 hreflang 文档对此表述非常清楚。认真读一遍。
区域特定的网站地图
一个混合了所有区域设置的网站地图是可以用的。但为每个区域设置单独生成网站地图(在 Google Search Console 中分别提交)能让你获得更清晰的索引数据,也更容易找出特定语言的爬虫错误。
我的 Astro 构建生成五个文件:sitemap-en.xml、sitemap-de.xml 等等。@astrojs/sitemap 通过 i18n 选项开箱即用地支持这一点。使用它。
多语言静态站点的内容策略
大多数开发者完全忽视的是:翻译本身。
你可以拥有一个技术上完美的 Astro 设置,有着完美无缺的 hreflang 和一个能让 Google 员工泪流满面的网站地图。但如果你的德文内容是用 DeepL 生成然后从未被人工审查过的,那么这个网站在德国不会排名很好。
我不是在装腔作势。我经常使用 DeepL。它是一个很好的初稿。但对于任何你真正想要排名的页面,需要有母语使用者来阅读。不一定要重写。只需阅读它,修复奇怪的部分,确认措辞听起来像德国人会实际说的话。
Seahawk去年有一个金融科技项目,客户要求七种语言,四周完成。我们用DeepL做基础翻译,然后通过Gengo雇了七个自由译者进行审核,大约每词£0.04。总翻译预算不到£800。那个网站现在在其中四个市场排名前五。经济效应显著。
区域特定元数据
标题标签和元描述也需要本地化。不只是翻译。"Best accounting software"在德语中不会直接对标搜索,因为关键词搜索量不同。
我在JSON翻译文件中的专门meta命名空间下保存区域特定元数据。页面模板从t('meta.title')调用,仅在缺少键时回退到默认值。这样SEO可以编辑一个区域的JSON文件,无需接触代码。
部署:五语言静态网站托管在哪里
Vercel和Netlify都能很好地处理静态Astro构建。但对于多语言,你需要按区域进行边缘缓存和设置区域特定请求头的能力。
我标准化了Cloudflare Pages。免费套餐满足大多数客户预算。请求从全球最近的边缘节点提供,当你的意大利用户从弗吉尼亚州数据中心获得文件时这很关键。Cloudflare网络在米兰、法兰克福、阿姆斯特丹都有节点。欧洲访客的TTFB差异是可测量的。
有一件事需要明确配置:Cache-Control请求头。静态文件应该有长缓存TTL(哈希资源用max-age=31536000、immutable)。HTML页面应该有较短的TTL或stale-while-revalidate策略,特别是如果你在内容更新后进行增量构建。
i18n构建调试上线前检查
我在每个多语言Astro构建推送到生产前都会检查几件事:
- 在本地运行 astro build,检查
dist/输出。每个语言文件夹都应该有所有页面。如果某个路由缺失,构建步骤是发现它的地方。 - 使用 Screaming Frog 爬取预览部署。按 hreflang 过滤。任何孤立页面(未被任何 hreflang 标签引用的页面)会立即显示。
- 在浏览器开发者工具中检查至少主页和一个内页的
x-default规范标签。 - 在浏览器中手动访问某个语言的 URL,并将浏览器设置为该语言。听起来很显而易见。但仍然能发现问题。
我遇到的最常见的构建时问题是添加 JSON 文件后忘记将新语言添加到 astro.config.mjs 的 locales 数组中。构建不会报错。它只是无声地忽略新语言。烦人。
常见问题
Astro 的 i18n 是否支持服务器端渲染 (SSR),还是仅支持静态输出?
两者都支持。Astro 的 i18n 路由在 SSR 模式下也能工作。getRelativeLocaleUrl() 和 getAbsoluteLocaleUrl() 辅助函数在两种输出模式下的工作方式完全相同。也就是说,如果你完全使用静态输出,性能优势会显著更大,因为你提供的是预构建的 HTML,没有运行时成本。
如何处理新访客的语言检测和重定向?
对于静态站点,我在 CDN/边缘层执行此操作,而不是在 Astro 中。Cloudflare Workers 可以读取 Accept-Language 标头并重定向到相应的语言路径。不要在 Astro 应用内部执行此操作,因为这会增加往返时间,使静态输出不确定。
随着网站增长,管理翻译文件的最佳方式是什么?
将翻译文件与代码一起放在版本控制中,不要放在单独的 CMS 或第三方翻译管理系统中,除非项目真正需要。一旦你有三个以上的语言版本和一个活跃的编辑团队,可以考虑 Tolgee 或 Localazy。两者都支持平面 JSON 文件集成和 GitHub 同步。达到这个规模之前,使用共享的 Notion 文档和手动更新 JSON 文件就足够了。
我可以在 i18n 中使用 Astro Content Collections 吗?
可以,这是我对博客类网站的首选方案。使用locale作为路径段来组织内容集合:src/content/blog/en/,src/content/blog/de/。然后在 getStaticPaths() 函数中按 locale 过滤。每个语言版本在构建时生成自己的静态 HTML。清晰、快速,编辑团队可以管理 markdown 文件,无需接触任何组件。
拥有多个 locale 页面会因为重复内容而伤害 SEO 吗?
不会,前提是你的 hreflang 实现正确。Google 会将正确标注的 locale 变体视为相关但不同的页面,而不是重复内容。风险来自 hreflang 实现不正确,特别是缺少相互关联的注解或不正确的 x-default 处理。解决这两个问题,重复内容就不是问题了。
---
从动态 PHP 渲染的多语言网站转向预构建的静态网站,并不真的是关于 Astro 本身。而是接受这样一个事实:大多数翻译内容不会每小时都变化。构建一次,缓存到处,让 CDN 来承担繁重工作。Astro 只是碰巧让这个工作流程确实令人愉快,而不是与框架的对抗。
五种语言,一个开发者,八周时间。工具都在那里。使用它们。
