去年春天,一个客户给我一份任务说明,这在 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 边缘节点提供的静态文件在这场竞争中总是赢家。, and static files served from a CDN edge node simply win that battle every time.
Astro 的内置 i18n 路由(以及为什么它改变了一切)
Astro 在 v3 中推出了适当的 i18n 路由,并在 v4 中进行了显著改进。到 2026 年我们还在运营时,这个 API 已经很稳定,而且使用起来真正愉快。
这是我使用的基本设置。你的 astro.config.mjs 需要一个 i18n 块:astro.config.mjs gets an i18n block:
`` i18n: { defaultLocale: 'en', locales: ['en', 'de', 'fr', 'it', 'es'], routing: { prefixDefaultLocale: false } } `` i18n: { defaultLocale: 'en', locales: ['en', 'de', 'fr', 'it', 'es'], routing: { prefixDefaultLocale: false } } ``
这一个配置告诉 Astro 在 / 提供英文,在 /de/ 提供德文,在 /fr/ 提供法文,以此类推。没有插件。没有额外的包。它是内置的。/, German at /de/, French at /fr/, and so on. No plugin. No extra package. It's baked in.
prefixDefaultLocale: false 是刻意的。我大多数客户的主要受众是英语使用者,所以我不想让 /en/ 污染规范的 URL 结构。Google 可以通过 hreflang 来解析这种关系。prefixDefaultLocale: false is deliberate. Most of my clients' primary audience is English-speaking, so I don't want /en/ polluting the canonical structure. Google can figure out the relationship via hreflang.
真正可扩展的文件结构
这正是大多数教程出错的地方。他们展示一个扁平的 pages/[locale]/index.astro,然后就完事了。对于宣传页面还行。一旦你有 40 条路由就会崩溃。pages/[locale]/index.astro and call it done. Fine for a brochure. Falls apart the moment you have 40 routes.
我的做法是:
`` src/ pages/ index.astro (English) about.astro (English) [locale]/ index.astro about.astro i18n/ en.json de.json fr.json it.json es.json `` 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() 工具函数来优雅地处理回退。i18n/ directory holds your translation strings as flat JSON files. en.json is the source of truth. I write every other locale against it. One key missing in de.json ? The German page falls back to English for that string rather than throwing an error, because I add a small t() utility function that handles fallback gracefully.
翻译层:保持简单
我试过 Paraglide JS、i18next 和一些其他的。对于 Astro 静态构建,我真诚的建议是:别过度设计。
一个简单的工具文件,导入语言环境 JSON 并返回一个 getter 函数,足以应对 90% 的项目。如果你需要类型安全的翻译,并且你的团队规模足够在乎编译时缺失 key 的错误,Inlang 的 Paraglide JS 值得考虑。对于个人工作或小型代理,这是不必要的开销。Paraglide JS from Inlang is worth considering if you need type-safe translations and your team is large enough to care about compile-time errors on missing keys. For solo work or small agencies, it's overhead you don't need.
我在乎的是:保持翻译文件扁平而不是深层嵌套。"hero.cta.button.label" 作为 key 听起来很井井有条,直到你在晚上 11 点搜索它。"heroCTALabel" 很丑但容易找到。"hero.cta.button.label" as a key sounds organised until you're searching for it at 11pm. "heroCTALabel" is ugly but findable.
处理复数和动态字符串
德语复数形式会让你谦虚。如果你涉足阿拉伯语也会(我曾帮一个客户扩展到阿联酋,光是数字格式就花了整个下午)。
对于复数形式,我使用一个小的内联帮助函数:
`` export function plural(count, one, other) { return count === 1 ? one : other; } `` export function plural(count, one, other) { return count === 1 ? one : other; } ``
粗糙吗?是的。但它涵盖了欧洲语言的 95% 的情况。如果你要支持阿拉伯语、俄语或波兰语,你需要正确的 CLDR 复数规则。Unicode CLDR 项目对这些规则进行了详尽的记录。根据这些规范构建你的复数帮助函数,否则你会搞错。The Unicode CLDR project documents these exhaustively. Build your plural helper against those specs or you will get it wrong.
hreflang:每个人都搞错的部分
没错。这正是多语言 SEO 的成败之处。
hreflang 告诉 Google 应该将页面的哪个版本提供给哪个语言地区。搞错了,你要么会自我蚕食排名,要么会把德文版本提供给法语使用者。我在接手的网站上两种情况都见过。
规则:
- 每个页面都必须为每个语言地区(包括它本身)包含 hreflang 标签。
- 这些标签必须是双向的。如果 /de/about 引用 /fr/about,那么 /fr/about 也必须引用 /de/about。
/de/aboutreferences/fr/about, then/fr/aboutmust reference/de/aboutback. - 包含一个 x-default 标签,指向你的默认语言地区。
x-defaulttag pointing to your default locale. - 始终使用绝对网址。一定要用。
在 Astro 中,我在 BaseLayout.astro 组件内生成这些标签,每个页面都会导入它。我传入当前路径,循环遍历区域设置数组,然后自动输出 <link rel="alternate"> 标签。无需手动维护。BaseLayout.astro component that every page imports. I pass in the current path, loop over the locale array, and spit out the <link rel="alternate"> tags automatically. No manual maintenance.
缺少 x-default 标签是我看到的最常见的错误。Google 自己的 hreflang 文档对此表述非常清楚。认真读一遍。x-default tag is the single most common mistake I see. Google's own hreflang documentation is genuinely clear on this. Read it once properly.
区域特定的网站地图
一个混合了所有区域设置的网站地图是可以用的。但为每个区域设置单独生成网站地图(在 Google Search Console 中分别提交)能让你获得更清晰的索引数据,也更容易找出特定语言的爬虫错误。
我的 Astro 构建生成五个文件:sitemap-en.xml、sitemap-de.xml 等等。@astrojs/sitemap 通过 i18n 选项开箱即用地支持这一点。使用它。sitemap-en.xml , sitemap-de.xml , and so on. @astrojs/sitemap supports this with the i18n option out of the box. Use it.
多语言静态站点的内容策略
大多数开发者完全忽视的是:翻译本身。
你可以拥有一个技术上完美的 Astro 设置,有着完美无缺的 hreflang 和一个能让 Google 员工泪流满面的网站地图。但如果你的德文内容是用 DeepL 生成然后从未被人工审查过的,那么这个网站在德国不会排名很好。
我不是在装腔作势。我经常使用 DeepL。它是一个很好的初稿。但对于任何你真正想要排名的页面,需要有母语使用者来阅读。不一定要重写。只需阅读它,修复奇怪的部分,确认措辞听起来像德国人会实际说的话。
Seahawk去年有一个金融科技项目,客户要求七种语言,四周完成。我们用DeepL做基础翻译,然后通过Gengo雇了七个自由译者进行审核,大约每词£0.04。总翻译预算不到£800。那个网站现在在其中四个市场排名前五。经济效应显著。
区域特定元数据
标题标签和元描述也需要本地化。不只是翻译。"Best accounting software"在德语中不会直接对标搜索,因为关键词搜索量不同。
我在JSON翻译文件中的专门meta命名空间下保存区域特定元数据。页面模板从t('meta.title')调用,仅在缺少键时回退到默认值。这样SEO可以编辑一个区域的JSON文件,无需接触代码。meta namespace. The page template pulls from t('meta.title') and falls back to a default only if the key is missing. This means an SEO can edit one JSON file per locale without ever touching the code.
部署:五语言静态网站托管在哪里
Vercel和Netlify都能很好地处理静态Astro构建。但对于多语言,你需要按区域进行边缘缓存和设置区域特定请求头的能力。
我标准化了Cloudflare Pages。免费套餐满足大多数客户预算。请求从全球最近的边缘节点提供,当你的意大利用户从弗吉尼亚州数据中心获得文件时这很关键。Cloudflare网络在米兰、法兰克福、阿姆斯特丹都有节点。欧洲访客的TTFB差异是可测量的。
有一件事需要明确配置:Cache-Control请求头。静态文件应该有长缓存TTL(哈希资源用max-age=31536000、immutable)。HTML页面应该有较短的TTL或stale-while-revalidate策略,特别是如果你在内容更新后进行增量构建。Cache-Control headers. Static files should have long cache TTLs ( max-age=31536000, immutable for hashed assets). HTML pages should have shorter TTLs or a stale-while-revalidate policy, particularly if you're doing incremental builds after content updates.
i18n构建调试上线前检查
我在每个多语言Astro构建推送到生产前都会检查几件事:
- 在本地运行 astro build,检查 dist/ 输出。每个语言文件夹都应该有所有页面。如果某个路由缺失,构建步骤是发现它的地方。
astro buildlocally and inspect thedist/output. Every locale folder should have every page. If a route is missing, the build step is the place to catch it. - 使用 Screaming Frog 爬取预览部署。按 hreflang 过滤。任何孤立页面(未被任何 hreflang 标签引用的页面)会立即显示。Screaming Frog to crawl the preview deployment. Filter by hreflang. Any orphaned pages (pages not referenced by any hreflang tag) show up immediately.
- 在浏览器开发者工具中检查至少主页和一个内页的 x-default 规范标签。
x-defaultcanonical in browser DevTools on at least the homepage and one inner page. - 在浏览器中手动访问某个语言的 URL,并将浏览器设置为该语言。听起来很显而易见。但仍然能发现问题。
我遇到的最常见的构建时问题是添加 JSON 文件后忘记将新语言添加到 astro.config.mjs 的 locales 数组中。构建不会报错。它只是无声地忽略新语言。烦人。astro.config.mjs locales array after adding the JSON file. The build doesn't error. It just silently ignores the new locale. Annoying.
常见问题
Astro 的 i18n 是否支持服务器端渲染 (SSR),还是仅支持静态输出?
两者都支持。Astro 的 i18n 路由在 SSR 模式下也能工作。getRelativeLocaleUrl() 和 getAbsoluteLocaleUrl() 辅助函数在两种输出模式下的工作方式完全相同。也就是说,如果你完全使用静态输出,性能优势会显著更大,因为你提供的是预构建的 HTML,没有运行时成本。getRelativeLocaleUrl() and getAbsoluteLocaleUrl() helper functions work identically regardless of output mode. That said, if you're going fully static, the performance benefits are significantly greater because you're serving pre-built HTML with no runtime cost.
如何处理新访客的语言检测和重定向?
对于静态站点,我在 CDN/边缘层执行此操作,而不是在 Astro 中。Cloudflare Workers 可以读取 Accept-Language 标头并重定向到相应的语言路径。不要在 Astro 应用内部执行此操作,因为这会增加往返时间,使静态输出不确定。Accept-Language header and redirect to the appropriate locale path. Don't do this inside your Astro app because it adds a round trip and makes the static output non-deterministic.
随着网站增长,管理翻译文件的最佳方式是什么?
将翻译文件与代码一起放在版本控制中,不要放在单独的 CMS 或第三方翻译管理系统中,除非项目真正需要。一旦你有三个以上的语言版本和一个活跃的编辑团队,可以考虑 Tolgee 或 Localazy。两者都支持平面 JSON 文件集成和 GitHub 同步。达到这个规模之前,使用共享的 Notion 文档和手动更新 JSON 文件就足够了。Tolgee or Localazy. Both integrate with flat JSON files and have GitHub sync. Below that threshold, a shared Notion doc and manual JSON updates is honestly fine.
我可以在 i18n 中使用 Astro Content Collections 吗?
可以,这是我对博客类网站的首选方案。使用locale作为路径段来组织内容集合:src/content/blog/en/,src/content/blog/de/。然后在 getStaticPaths() 函数中按 locale 过滤。每个语言版本在构建时生成自己的静态 HTML。清晰、快速,编辑团队可以管理 markdown 文件,无需接触任何组件。src/content/blog/en/, src/content/blog/de/. Then filter by locale in your getStaticPaths() function. Each locale gets its own static HTML at build time. Clean, fast, and the editorial team can manage markdown files without touching any components.
拥有多个 locale 页面会因为重复内容而伤害 SEO 吗?
不会,前提是你的 hreflang 实现正确。Google 会将正确标注的 locale 变体视为相关但不同的页面,而不是重复内容。风险来自 hreflang 实现不正确,特别是缺少相互关联的注解或不正确的 x-default 处理。解决这两个问题,重复内容就不是问题了。incorrectly implementing hreflang, particularly missing reciprocal annotations or incorrect x-default handling. Nail those two things and duplicate content is not a concern.
---
从动态 PHP 渲染的多语言网站转向预构建的静态网站,并不真的是关于 Astro 本身。而是接受这样一个事实:大多数翻译内容不会每小时都变化。构建一次,缓存到处,让 CDN 来承担繁重工作。Astro 只是碰巧让这个工作流程确实令人愉快,而不是与框架的对抗。
五种语言,一个开发者,八周时间。工具都在那里。使用它们。
