< BACK 木质办公桌上褪色的欧洲纸质地图,旁边放着浓缩咖啡杯,阴天社论光线,35mm 胶片颗粒

Next.js i18n 2026:路由、hreflang 和 SEO

2023 年,我为一家柏林的客户制作了一个多语言电商网站。十二种语言,App Router,一切看起来整洁有序。三个月后,他们的德语产品页面在英国搜索结果中排名,他们的英语页面被提供给慕尼黑的用户。hreflang 标签在技术上是存在的。只是以一种让我花了一个尴尬的下午才理清的方式出错了。那次经历教会了我的关于 Next.js i18n 的知识,比任何文档页面都要多。

这不是一篇"这是官方文档的总结"的文章。这是我在 Seahawk Media 构建多语言网站时学到的东西,是人们一再遇到的问题,以及在 2026 年实际影响排名的具体决策。

---

App Router 改变了一切(并非全部都更好)

Pages Router 有内置的 i18n 支持。你在 next.config.js 中放入一个配置块,Next.js 会自动处理语言检测、子路径路由和 Link 组件的语言切换。这并不完美,但它是固执己见的,这意味着踩坑更少。next.config.js and Next.js handled locale detection, sub-path routing, and Link component locale switching almost automatically. It wasn't perfect, but it was opinionated, which meant fewer footguns.

App Router 完全移除了原生 i18n 路由。next.config.js 中没有 i18n 键。没有自动语言区域检测。你现在需要自己负责完整的路由逻辑,而这正是大多数团队容易出问题的地方。i18n key in next.config.js. No automatic locale detection. You are now responsible for the full routing logic, and that responsibility is where most teams quietly make a mess.

2026 年的最佳实践是在你的 app/ 目录根部使用 [locale] 动态段。比如 app/[locale]/page.tsx 。配合 next-intl 或通过 react-i18next 的 i18next,再加上一个读取 Accept-Language 请求头并相应重定向的中间件文件,你就有了一个可行的基础。[locale] dynamic segment at the root of your app/ directory. Something like app/[locale]/page.tsx . Pair that with next-intl or i18next via react-i18next, a middleware file that reads Accept-Language headers and redirects accordingly, and you have a working foundation.

中间件为什么容易被误解

中间件文件在做两项人们经常混淆的工作。第一,语言区域检测:读取 Accept-Language 请求头(或 cookie、或 URL 前缀)并决定要提供哪种语言。第二,重定向或重写 URL,使用户最终访问的是 /de/produkte 而不是 /produkte 加上某种隐藏的语言状态。Accept-Language header (or a cookie, or a URL prefix) and deciding which locale to serve. Second, redirecting or rewriting the URL so the user ends up at /de/produkte rather than /produkte with some hidden locale state.

这两项都必须在页面渲染之前进行。这听起来很明显。但我见过有些团队在 React 上下文提供程序内部执行语言区域检测,这意味着发送给 Googlebot 的初始 HTML 完全没有语言信号。搜索引擎爬虫看到一个页面,它会在根 URL 下索引它,突然之间你就陷入了规范标签的混乱。before the page renders. That sounds obvious. But I've seen teams run locale detection inside a React context provider, which means the initial HTML served to Googlebot has no locale signal at all. The bot sees a page, it indexes it under the root URL, and suddenly you have a canonical mess.

把语言区域解析保留在中间件中。永远都要。

---

子路径 vs 子域名:选一个并坚持下去

永恒的争论。/en/about vs en.yoursite.com/about 。对于大多数项目,我推荐子路径路由,原因如下:它可以集中你的域名权重。指向你的 .com 的每个链接都会让所有语言版本受益。使用子域名的话,在谷歌眼中你实际上是在运营独立的网站,这意味着你需要为每个子域名独立建立权重。/en/about vs en.yoursite.com/about . For most projects I recommend sub-path routing, and here's why: it concentrates your domain authority. Every link pointing at your .com benefits all locales. With sub-domains you're effectively running separate sites in Google's eyes, which means you need to build authority for each one independently.

有一个例外。如果你所在的市场非常看重本地 TLD(德国的 .de 、法国的 .fr ),国家代码顶级域名在本地可信度上会优于子路径和子域名两者。这不是一个 SEO 说法,这是一个转化率说法。我看过 A/B 测试,.de vs yourbrand.com/de 在排名完全相同的情况下仍然显著改变了信任指标。.de in Germany, .fr in France), a country-code TLD beats both sub-paths and sub-domains for perceived local credibility. That's not an SEO claim, it's a conversion claim. I've seen A/B tests where .de vs yourbrand.com/de moved trust metrics meaningfully even when rankings were identical.

但你无法在单个 Next.js 应用中轻松运行 ccTLD 设置。所以对于 99% 的项目来说:子路径、一个应用、一个域名。

---

hreflang:那个欺骗你的标签

这是我看到最多问题的地方。hreflang 在概念上并不难。它告诉搜索引擎:"这个页面在其他语言中有等效版本,它们在这里。"在实践中,实现有三种具体的失败模式,我一直在遇到。

失败模式 1:标签不相互关联

hreflang 集合中的每个页面都必须引用其他所有页面,包括自身。如果 /en/about 有一个指向 /de/about 的 hreflang,但 /de/about 没有一个指向回 /en/about 的 hreflang,Google 就会忽略整个集合。不是降低排名。是忽略。就像这些标签不存在一样。/en/about has an hreflang pointing to /de/about but /de/about does not have an hreflang pointing back to /en/about, Google ignores the whole set. Not penalises. Ignores. As if the tags don't exist.

Seahawk 去年的一个金融科技项目中,40% 的 hreflang 标签不相互关联,因为两个开发者在不同的组件中独立实现了它们而没有协调。修复花了一小时。排名恢复花了两个月。

失败模式 2:x-default 标签丢失或放置错误

x-default 用于不针对特定语言或地区的页面。通常是你的根目录 / 或语言选择器页面。有些团队将 x-default 放在他们的英文页面上,如果英文确实是你所有不匹配语言环境的备用语言,这没关系。但如果你为语言不支持的用户提供不同的页面,x-default 应该指向那里。 is for pages that don't target a specific language or region. Typically your root / or a language-selector page. Some teams put x-default on their English page, which is fine if English is genuinely your fallback for all unmatched locales. But if you serve a different page to users whose language you don't support, x-default should point there.

我见过 x-default 被完全省略的情况。Google 不会抛出错误。它只是自己决定为不匹配的查询显示哪个版本,这个决定通常是错误的。x-default omitted entirely. Google doesn't throw an error. It just makes its own decision about which version to show for unmatched queries, and that decision is often wrong.

失败模式 3:尾部斜杠和规范标签冲突

你的 hreflang 标签指向 https://yoursite.com/de/ueber-uns。同一页面上的规范标签指向 https://yoursite.com/de/ueber-uns/(注意尾部斜杠)。Google 将这两个视为不同的 URL。你的规范标签和 hreflang 现在指向不同的内容,创建了一个信号冲突,在 Search Console 中没有任何可见错误的情况下悄悄抑制排名。https://yoursite.com/de/ueber-uns . Your canonical tag on the same page says https://yoursite.com/de/ueber-uns/ (note the trailing slash). Google treats those as different URLs. Your canonical and hreflang are now pointing at different things, and you've created a signal conflict that quietly suppresses rankings without any visible error in Search Console.

在 next.config.js 中设置 trailingSlash,并保持一致。检查它。通过 curl 你自己的 URL 并读取输出来实际检查它。trailingSlash in next.config.js and be consistent everywhere. Check it. Actually check it by curling your own URLs and reading the output.

Google Search Central 关于 hreflang 的文档确实值得完整阅读。这是那些难得一见的、具体到足以有用的官方文档之一。Google Search Central documentation on hreflang is genuinely worth reading in full. It's one of those rare official docs that's specific enough to be useful.

---

在 Next.js 中规模化生成 hreflang

对于 5 语言网站,你可以手动编写 hreflang 标签。对于 3,000 个产品页面跨越 12 个地区,你需要一个系统化的方法。

这是我使用的方法:

  1. 在单个配置文件中定义你的地区列表一次。类似 locales.config.ts 导出一个数组:['en', 'de', 'fr', 'es', 'nl'...]。locales.config.ts exporting an array: ['en', 'de', 'fr', 'es', 'nl'...].
  2. 构建一个实用函数 getHreflangAlternates(pathname: string),它接收一个与地区无关的路径(/about),并为每个地区返回一个 { hreflang, href } 对象数组。getHreflangAlternates(pathname: string) that takes a locale-agnostic path (/about ) and returns an array of { hreflang, href } objects for every locale.
  3. 在你的根 layout.tsx 中调用该函数,并通过 Next.js 的 metadata API 使用 alternates.languages 字段注入结果。这是在 Next.js 13.3 中添加的,它会自动在 <head> 中生成正确的 <link rel="alternate" hreflang="..."> 标签。layout.tsx and inject the results via Next.js's metadata API using the alternates.languages field. This was added in Next.js 13.3 and it generates the correct <link rel="alternate" hreflang="..."> tags in <head> automatically.
  4. 对于包含动态内容的页面(博客文章、产品页面)中并非每个语言区域都有翻译的情况,将 supportedLocales 数组传递给你的工具函数,仅为内容实际存在的语言区域生成标签。指向返回 404 的页面的 hreflang 标签比没有 hreflang 标签更糟。supportedLocales array to your utility and only generate tags for locales where the content actually exists. A hreflang tag pointing to a page that returns 404 is worse than no hreflang tag.

metadata API 中的 alternates.languages 方法是我找到的处理这个问题最简洁的方式。不需要自定义 <Head> 操作,也不需要为标签本身引入第三方包。alternates.languages approach in the metadata API is the cleanest way I've found to handle this. No custom <Head> juggling, no third-party packages for the tags themselves.

---

语言区域检测而不显得烦人

这里有一个 UX 陷阱,同时也会影响 SEO。基于浏览器语言区域的激进自动重定向对两者都有害。

如果德国用户访问 yoursite.com/en/article,而你的中间件硬性将他们重定向到 /de/artikel 而没有征求同意,你就破坏了他们的预期导航。你也可能破坏了链接分享:有人分享了英文 URL,法国的收件人被转向法文,而他们本应看到的内容可能还没有法文版本。yoursite.com/en/article and your middleware hard-redirects them to /de/artikel without asking, you've just broken their intended navigation. You've also potentially broken link sharing: someone shares the English URL, the recipient in France gets bounced to French, and the content they were meant to see might not exist in French yet.

我的原则是:检测、建议、不强制。使用 Accept-Language 标头在首次访问时设置合理的默认值(存储在 Cookie 中),但让用户手动覆盖它,并在随后的每次访问中尊重该覆盖。Cookie 优先于标头。detect, suggest, don't force. Use the Accept-Language header to set a sensible default on first visit (stored in a cookie), but let users override it manually and honour that override on every subsequent visit. The cookie takes priority over the header.

对于 SEO 来说:Googlebot 不发送有意义的 Accept-Language 标头。它会按原样爬取你的 URL。因此,请确保每个语言区域的 URL 都可以直接访问而不需要重定向,并且你的网站地图明确包含所有语言区域变体。Accept-Language headers. It will crawl your URLs as-is. So make sure every locale URL is directly accessible without redirection, and that your sitemap includes all locale variants explicitly.

next-intl 中间件文档很好地涵盖了这种检测策略,这是我会向任何启动新项目的人推荐的地方。next-intl middleware documentation covers this detection strategy well, and it's where I'd send anyone starting a new project.

---

多语言网站的网站地图

对于较小的网站,单个 sitemap.xml 文件包含所有语言版本就可以了。对于大型网站(跨多个语言版本有数万个网址),我会进行拆分:在 /sitemap.xml 处放置一个网站地图索引,然后在 /sitemaps/en.xml、/sitemaps/de.xml 等处放置按语言版本分开的网站地图文件。sitemap.xml that includes every locale variant is fine for smaller sites. For large sites (tens of thousands of URLs across multiple locales) I split it: one sitemap index at /sitemap.xml , then separate sitemap files per locale at /sitemaps/en.xml , /sitemaps/de.xml, and so on.

每个 URL 条目应该包含 <xhtml:link rel="alternate"> 标签,与你的 hreflang 设置相对应。这虽然与页面内的标签重复,但它有助于 Googlebot 连接它尚未爬取过的页面。<xhtml:link rel="alternate"> tags mirroring your hreflang setup. This is redundant with the in-page tags but it helps Googlebot connect the dots on pages it hasn't crawled yet.

在 Next.js 14+ 中,你可以通过 app/sitemap.ts 动态生成网站地图。返回类型支持 alternates.languages,其形状与元数据 API 相同。将其关联到你之前构建的 getHreflangAlternates 工具函数,你就能获得一致性。app/sitemap.ts . The return type supports alternates.languages in the same shape as the metadata API. Wire it up to the same getHreflangAlternates utility you built earlier and you get consistency for free.

一个具体数字:在拥有 8 个语言版本和 4,500 个产品的项目中,根据 GSC 爬取统计,拆分网站地图使每个语言版本的平均爬取时间减少了约 30%。不是突破性的改进,但很有意义。

---

翻译库:我实际在用的

有三个值得了解。next-intl 是我在 App Router 项目中的默认选择。它对服务器组件有一流的支持,正确处理复数形式,API 也很简洁。自 2023 年中期以来,我已在大约 40 个项目中使用过它。next-intl is my default for App Router projects. It has first-class support for server components, handles pluralisation properly, and the API is clean. I've used it on about 40 projects since mid-2023.

react-i18next 经过充分测试,生态系统庞大,但它的设计思路是为客户端组件构建的,通过 i18next 核心添加服务器组件支持感觉是后来才加的,与 next-intl 的原生方式相比不够优雅。i18next core feels bolted-on compared to next-intl's native approach.

如果你的翻译工作流涉及专业翻译人员而非 CMS,Lingui 值得一看。它的提取工具更好用。但对于大多数内容来自 Contentful 或 Sanity 的代理项目,next-intl 更简单。

我会避免的是:自己动手做。我在 2020 年做过一次,当时客户坚持要直接复用他们现有的 PHP 翻译文件。勉强可用,但我花了两年时间维护一些东西,现在 next-intl 用一个 useTranslations() 调用就搞定了。useTranslations() call.

---

FAQ

Next.js App Router 是否支持旧的 `next.config.js` i18n 配置块?

不支持。i18n 配置键特定于 Pages Router。如果你要迁移到 App Router,需要通过 [locale] 动态段和中间件文件来实现路由。App Router 项目中旧配置块会被默认忽略。i18n configuration key was specific to the Pages Router. If you're migrating to App Router, you need to implement routing via a [locale] dynamic segment and a middleware file. The old config block is silently ignored in App Router projects.

我应该在 URL 路径中使用 `lang` 还是依赖子域名?

对大多数项目来说,在 URL 路径中使用 lang(子路径路由如 /de/)是正确选择。子域名会分散你的域名权重,并需要单独的 DNS 配置。ccTLD 只有在本地信任信号确实在你的特定市场推动转化率时,才值得处理这种复杂性。lang in the URL path (sub-path routing like /de/) is the right call. Subdomains fragment your domain authority and require separate DNS configuration. ccTLDs are only worth the complexity if local trust signals are genuinely moving conversion rates in your specific markets.

Googlebot 如何处理 JavaScript 渲染的语言版本内容?

有时处理得很糟。如果你的语言检测完全在 React 上下文中的客户端运行,而初始 HTML 中没有语言标记,Googlebot 可能会索引水合前的状态。始终在中间件中解析语言,并确保服务器渲染的 HTML 中 <html> 元素上有正确的 lang 属性。通过查看源代码来检查这一点,而不是使用浏览器开发者工具(它显示的是水合后的状态)。lang attribute is on the <html> element in the server-rendered HTML. Check this by viewing source, not by using browser devtools (which show post-hydration state).

对于没有语言选择器页面的网站,正确的 `x-default` hreflang 值应该是什么?

将其指向你的主要语言版本的主页,通常是 /en/。这向Google表明英文是你对不支持的语言用户的备用语言。这样做没有惩罚,只是一个Google可能会也可能不会遵守的软性偏好信号。/en/. It signals to Google that English is your fallback for users whose language you don't support. It's not a penalty to do this, it's just a soft preference signal Google may or may not honour.

我可以在没有付费SEO工具的情况下验证我的hreflang实现吗?

可以。Google Search Console的URL检查工具会显示任何已索引网址的检测到的hreflang标签。它不会自动验证互惠性,但你可以手动检查几个代表性页面。对于批量验证,Screaming Frog(免费最多500个网址)在其hreflang标签页中处理互惠性检查。Google Search Console's URL Inspection tool will show you detected hreflang tags for any indexed URL. It won't validate reciprocity automatically, but you can manually check a few representative pages. For bulk validation, Screaming Frog (free up to 500 URLs) handles reciprocity checks in its hreflang tab.

---

让多语言Next.js正确运行是真正的费力工作。涉及配置文件、URL一致性,以及确保两个开发人员没有在不同组件中重复实现同样的东西。但那些我花时间正确实现的网站,其排名在各地区都能保持,而那些我走捷径的网站教会了我我在这里写下的经验教训。

顺便说一下,柏林的那个客户最终让他们的德文页面在德国获得排名。花了我们一个完整的重新爬取周期和大量的咖啡。

< BACK