首页/新闻资讯/正文详情

Next.js 16 多语言 SEO 实战:用一个路由注册表同时派生 hreflang、sitemap 和语言切换器(9 种语言、132 个 URL)

发布时间:2026/9/24 6:43:14 来源:云帆数科 栏目:资讯中心
Next.js 16 多语言 SEO 实战:用一个路由注册表同时派生 hreflang、sitemap 和语言切换器(9 种语言、132 个 URL)
这篇写给正在用 Next.js App Router 做多语言站点、需要每个页面正确输出 hreflang 和 sitemap 的开发者。要解决的问题只有一个hreflang、sitemap、语言切换器三处的语言映射如何保证永远一致。hreflang 是告诉搜索引擎「这个页面还有哪些语言版本、分别在哪个 URL」的一组link relalternate标签Google 要求每组里的每个 URL 都互相列出对方对称并且建议提供x-default。多语言站点常见的病是三处映射各写一份页面元数据里一份、sitemap 里一份、语言切换器里一份加一个翻译页要改三处漏一处搜索引擎就收到自相矛盾的信号。下面这套做法用一份注册表当唯一事实来源三处输出全部派生。线上实例是一个 9 种语言、sitemap 里 132 个 URL、906 条 hreflang 交叉引用的站点。先说利益关系示例站点是我参与的产品 EditTextImageedittextimage.com它的功能是改掉成品图里已经印上去的文字并保留原字体和背景文中的 URL 都是它的真实路由。环境版本Next.js 16.2.4App RouterTurbopack 构建TypeScript 5React 19部署在 Vercel页面全部静态预渲染验证日期2026-09-23Next.js 16 的 Metadata API 和 15 基本一致本文用到的alternates.canonical、alternates.languages和app/sitemap.ts在 14 以上都可用但 16 有若干与旧版不兼容的改动动手前建议先读一遍node_modules/next/dist/docs/里对应的文档。第一步定义注册表注册表就是一个数组每个条目描述「一个页面在哪些语言下存在、URL 各是什么」。硬约束只有一条每个条目必须有en默认语言这样任何派生出来的链接都不会指向不存在的页。// lib/i18n/config.tsexportconstLOCALES[en,es,pt,ja,ko,zh-tw,ru,de,fr]asconst;exporttypeLocale(typeofLOCALES)[number];exportconstDEFAULT_LOCALE:Localeen;exportconstBASE_URLhttps://edittextimage.com;// hreflang 用的语言代码。zh-tw 必须按 BCP 47 写成大小写混合的 zh-TW// 其余用纯语言码en 而不是 en-US面向全球而不是单个国家。exportfunctionhreflangCode(locale:Locale):string{if(localezh-tw)returnzh-TW;returnlocale;}// lib/i18n/routes.tsexportinterfacePageEntry{/** 页面用它查自己buildAlternates(id, locale) */id:string;/** 默认语言 URL 的 sitemap 优先级非默认语言自动 -0.05 */priority:number;changeFrequency?:weekly|monthly|yearly;/** 每种语言的路径必须包含 en */urls:PartialRecordLocale,string{en:string};}exportconstPAGES:PageEntry[][{id:screenshot,priority:0.9,urls:{en:/edit-text-in-screenshot,pt:/pt/editar-texto-em-print,ru:/ru/redaktirovat-tekst-na-skrinshote,ja:/ja/sukusho-moji-henshu,ko:/ko/seukeurinsyat-geulja-pyeonjip,zh-tw:/zh-tw/bianji-jietu-wenzi,de:/de/text-im-screenshot-bearbeiten,fr:/fr/modifier-texte-capture-ecran,es:/es/editar-texto-en-captura-de-pantalla,},},// 只有英文的页面也要登记否则 sitemap 和切换器都看不见它{id:meme,priority:0.85,urls:{en:/edit-text-in-meme}},// …];constBY_IDnewMap(PAGES.map((p)[p.id,p]));exportfunctionpageById(id:string):PageEntry|undefined{returnBY_ID.get(id);}/** 某页在 locale 下的 URL没有翻译时回退到英文永远不返回死链 */exportfunctionurlForLocale(id:string,locale:Locale):string|undefined{constentryBY_ID.get(id);returnentry?(entry.urls[locale]??entry.urls.en):undefined;}/** 反查一个路径属于哪个条目、当前是哪种语言 */exportfunctionpageByPath(path:string):{entry:PageEntry;locale:Locale}|undefined{for(constentryofPAGES){for(const[locale,url]ofObject.entries(entry.urls)){if(urlpath)return{entry,locale:localeasLocale};}}returnundefined;}两个设计点值得说明。每种语言的 slug 是各自语言的词不是英文 slug 加前缀/pt/editar-texto-em-print而不是/pt/edit-text-in-screenshot这是本地化 SEO 的基本要求注册表的价值之一就是把这种不规则映射集中管起来。翻译页的存在与否由注册表决定加一个翻译 在条目里加一行 URL 建一个页面文件hreflang、sitemap、切换器自动跟上。第二步从注册表派生 hreflang每个页面的metadata.alternates不再手写调一个函数// lib/i18n/metadata.tsimporttype{Metadata}fromnext;import{BASE_URL,DEFAULT_LOCALE,hreflangCode,typeLocale}from./config;import{pageById}from./routes;exportfunctionbuildAlternates(id:string,locale:Locale):Metadata[alternates]{constentrypageById(id);if(!entry){thrownewError(buildAlternates: unknown page id ${id});}constselfentry.urls[locale];if(!self){thrownewError(buildAlternates: page ${id} has no ${locale} url);}constlanguages:Recordstring,string{};for(const[loc,path]ofObject.entries(entry.urls)){languages[hreflangCode(locasLocale)]BASE_URLpath;}// x-default 指向默认语言英文的 URLlanguages[x-default]BASE_URL(entry.urls[DEFAULT_LOCALE]??entry.urls.en);return{canonical:BASE_URLself,languages,};}页面里只剩一行// app/pt/editar-texto-em-print/page.tsxexportconstmetadata{title:…,description:…,alternates:buildAlternates(screenshot,pt),};注意两处throwid 写错或者给不存在的语言调用构建期就失败而不是上线后静默输出一组错误的 hreflang。这是把映射集中化换来的最大好处——错误会在最早的时刻暴露。对称性是自动满足的同一个条目的 9 个页面各自调buildAlternates拿到的是同一张表所以每个页面都列出了包括自己在内的全部语言版本正好是 Google 的要求。第三步从注册表派生 sitemap// app/sitemap.tsimporttype{MetadataRoute}fromnext;import{BASE_URL,DEFAULT_LOCALE,hreflangCode,typeLocale}from/lib/i18n/config;import{PAGES}from/lib/i18n/routes;exportdefaultfunctionsitemap():MetadataRoute.Sitemap{constnownewDate().toISOString();constentries:MetadataRoute.Sitemap[];for(constpageofPAGES){constlocalesObject.keys(page.urls)asLocale[];for(constlocaleoflocales){// 每个 URL 都列出全部语言版本含自己 x-defaultconstlanguages:Recordstring,string{};for(constotheroflocales){languages[hreflangCode(other)]BASE_URLpage.urls[other]!;}languages[x-default]BASE_URLpage.urls[DEFAULT_LOCALE]!;entries.push({url:BASE_URLpage.urls[locale]!,lastModified:now,changeFrequency:page.changeFrequency??weekly,priority:localeDEFAULT_LOCALE?page.priority:Math.round((page.priority-0.05)*100)/100,...(locales.length1?{alternates:{languages}}:{}),});}}returnentries;}alternates.languages会被 Next.js 渲染成 sitemap 里的xhtml:link relalternate hreflang…这是 Google 支持的三种 hreflang 声明方式之一另两种是 HTMLlink和 HTTP 头。同一份注册表同时喂 HTML 和 sitemap两处永远一致。第四步语言切换器也从注册表来切换器最容易出的 bug 是「链到一个不存在的翻译页」。用pageByPath反查当前页属于哪个条目只列出这个条目真有的语言// components/language-switcher.tsx节选 export function LanguageSwitcher() { const pathname usePathname(); const match pageByPath(pathname); if (!match) return null; const { entry, locale: current } match; const locales Object.keys(entry.urls) as Locale[]; if (locales.length 1) return null; // 只有一种语言就不显示 return ( div {locales.map((loc) loc current ? ( span key{loc} aria-currenttrue{LANG_NAME[loc]}/span ) : ( Link key{loc} href{entry.urls[loc]!} hrefLang{hreflangCode(loc)} {LANG_NAME[loc]} /Link ) )} /div ); }hrefLang属性顺手也加上和 head 里的声明一致。输出结果线上页面/edit-text-in-screenshot的head截取2026-09-23linkrelcanonicalhrefhttps://edittextimage.com/edit-text-in-screenshot/linkrelalternatehrefLangenhrefhttps://edittextimage.com/edit-text-in-screenshot/linkrelalternatehrefLangpthrefhttps://edittextimage.com/pt/editar-texto-em-print/linkrelalternatehrefLangruhrefhttps://edittextimage.com/ru/redaktirovat-tekst-na-skrinshote/linkrelalternatehrefLangjahrefhttps://edittextimage.com/ja/sukusho-moji-henshu/linkrelalternatehrefLangkohrefhttps://edittextimage.com/ko/seukeurinsyat-geulja-pyeonjip/linkrelalternatehrefLangzh-TWhrefhttps://edittextimage.com/zh-tw/bianji-jietu-wenzi/linkrelalternatehrefLangdehrefhttps://edittextimage.com/de/text-im-screenshot-bearbeiten/linkrelalternatehrefLangfrhrefhttps://edittextimage.com/fr/modifier-texte-capture-ecran/linkrelalternatehrefLangeshrefhttps://edittextimage.com/es/editar-texto-en-captura-de-pantalla/linkrelalternatehrefLangx-defaulthrefhttps://edittextimage.com/edit-text-in-screenshot/同一页在sitemap.xml里的条目urllochttps://edittextimage.com/edit-text-in-screenshot/locxhtml:linkrelalternatehreflangenhrefhttps://edittextimage.com/edit-text-in-screenshot/xhtml:linkrelalternatehreflangpthrefhttps://edittextimage.com/pt/editar-texto-em-print/xhtml:linkrelalternatehreflangjahrefhttps://edittextimage.com/ja/sukusho-moji-henshu/xhtml:linkrelalternatehreflangzh-TWhrefhttps://edittextimage.com/zh-tw/bianji-jietu-wenzi/!-- …其余语言省略… --xhtml:linkrelalternatehreflangx-defaulthrefhttps://edittextimage.com/edit-text-in-screenshot/lastmod2026-09-22T10:47:53.735Z/lastmodchangefreqweekly/changefreqpriority0.9/priority/url整站规模sitemap 132 个 URL906 条xhtml:link交叉引用全部由注册表生成没有一处手写。验证方法三条命令就够不需要第三方工具# 1. head 里的 hreflang 是否成组且含 x-defaultcurl-shttps://你的域名/某个页面|grep-oElink relalternate[^]*# 2. sitemap 里 xhtml:link 的数量每个多语言 URL 应有 n1 条n 为语言数curl-shttps://你的域名/sitemap.xml|grep-oxhtml:link|wc-l# 3. 对称性抽查从 A 语言页取出 B 的 URL再从 B 页确认它列出了 A上线后再到 Google Search Console 的「国际定位」报告看有没有「无返回标记」错误——那就是不对称的信号。常见报错与排查构建报buildAlternates: unknown page id xxx。页面里传的 id 和注册表的id不一致多半是拼写。这是设计上的故意失败改 id 即可。构建报page xxx has no ja url。你建了app/ja/…/page.tsx但注册表条目里没加ja这一行。先加注册表再建页面顺序反了就会撞上。Search Console 报「hreflang 无返回标记」。某个语言页的 head 没列出其他语言。在这套方案里几乎只有一种可能那个页面没用buildAlternates而是自己手写了alternates。全局搜一下alternates: {就能找到。zh-TW 被 Search Console 判为无效代码。输出成了小写zh-tw。确认所有输出都经过hreflangCode()不要有地方直接把 locale 字符串拼进去。切换器链到 404。切换器没走pageByPath而是按固定语言列表拼 URL。切换器只能列出注册表里该条目真有的语言。sitemap 里 lastmod 全是构建时间。上面的代码用了new Date()这是已知的简化如果需要真实的更新时间在PageEntry上加一个updated字段并在 sitemap 里优先使用它。什么时候不适合这套做法页面是从数据库或 CMS 动态生成的例如几千篇文章各有多语言版本注册表会变成手工维护的负担这时映射应该存在数据层由数据驱动generateStaticParams和 sitemap。这套做法适合的是「几十到一两百个手写落地页」的规模——每个页面本来就是一个文件注册表只是把它们的语言关系集中起来。另外它假设 canonical 就是页面自己。如果你的站点有「多个 URL 指向同一内容、需要 canonical 指向别处」的情况buildAlternates里canonical: BASE_URL self这一行要改成可配置的。FAQhreflang 用en还是en-US面向全球用户的工具类站点用纯语言码en它匹配所有英语用户en-US只匹配美国。只有当同一语言在不同国家有真正不同的内容价格、法规时才用「语言-地区」码。zh-TW是例外繁体中文没有独立的语言码必须带地区。只有英文的页面要不要写 hreflang不需要输出alternates.languages上面 sitemap 代码里locales.length 1的判断就是干这个的但页面仍然要登记在注册表里否则 sitemap 和切换器都不知道它存在。canonical 照常输出。x-default 应该指向谁指向没有匹配语言时的兜底页通常是默认语言版本或语言选择页。这里指向英文版因为它是内容最完整的版本。九个语言版本的 x-default 都指向同一个英文 URL。加一种新语言要改几处两处LOCALES数组加一项、localeFromPathname加一个前缀判断然后逐页在注册表条目里加 URL 并建页面文件。hreflang、sitemap、切换器不用动。

相关推荐

在 Airbyte 中使用 smsmode SMS 连接器:基于 DeclarativeSource 的短信日志与用量同步实战
在 Airbyte 中使用 smsmode SMS 连接器:基于 DeclarativeSource 的短信日志与用量同步实战

数据工程数据集成ETL后端大数据 【免费下载链接】airbyte Open-source data movement for ELT pipelines and AI agents — from APIs, databases & files to warehouses, lakes, and AI applications. Both self-hosted and Cloud. 项目地址: https://gitcode.… · 2026/9/24 6:43:08

Monero 可引导构建完全指南:基于 Guix 实现可审计、可复现的二进制构建
Monero 可引导构建完全指南:基于 Guix 实现可审计、可复现的二进制构建

区块链金融科技 【免费下载链接】monero Monero: the secure, private, untraceable cryptocurrency 项目地址: https://gitcode.com/gh_mirrors/mo/monero 点击查看 免费下载 本文以 Monero 仓库 contrib/guix 目录下的官方文档为核心,系统讲解如何借助… · 2026/9/24 6:43:02

FPGA+FX3实现USB3.0高速数据传输:从原理到338MB/s实战调优
FPGA+FX3实现USB3.0高速数据传输:从原理到338MB/s实战调优

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 6:43:02

Skia 的 clang_ubuntu_noble 工具链资产:Linux 自研 Clang 编译器的构建、分发与 Bazel/GN 集成指南
Skia 的 clang_ubuntu_noble 工具链资产:Linux 自研 Clang 编译器的构建、分发与 Bazel/GN 集成指南

图形学 【免费下载链接】skia Skia is a complete 2D graphic library for drawing Text, Geometries, and Images. See documentation for contribution instructions. 项目地址: https://gitcode.com/gh_mirrors/ski/skia 点击查看 免费下载 导读 本文围绕 Skia… · 2026/9/24 7:32:58

YOLOv11实时人体行为识别与异常事件预警:安防监控新范式
YOLOv11实时人体行为识别与异常事件预警:安防监控新范式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 7:32:22

EMQX 升级 gen_rpc 3.5.1:根治节点不可达时的 Crash 日志长尾与 `failed_to_connect_server` 刷屏
EMQX 升级 gen_rpc 3.5.1:根治节点不可达时的 Crash 日志长尾与 `failed_to_connect_server` 刷屏

后端物联网消息队列通信 【免费下载链接】emqx The most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles 项目地址: https://gitcode.com/gh_mirrors/em/emqx 点击查看 免费下载 导读 本文围绕 EMQX 官方变更记录 fix-16453.en.md … · 2026/9/24 7:31:45

ESP32-S3-BOX-3实战:智能语音与物联网联动开发指南
ESP32-S3-BOX-3实战:智能语音与物联网联动开发指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 7:31:33

电脑故障处理打印版:一张纸搞定蓝屏、C盘满、重装排查
电脑故障处理打印版:一张纸搞定蓝屏、C盘满、重装排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 7:31:27

松下A5/A6伺服X4接口位置模式接线指南:7个关键引脚与PLC匹配接法
松下A5/A6伺服X4接口位置模式接线指南:7个关键引脚与PLC匹配接法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 7:31:08

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13

1D-CNN时间序列建模实战:从Conv1d原理到工业落地
1D-CNN时间序列建模实战:从Conv1d原理到工业落地

简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26

柔软的L:汉语语流中被忽视的舌肌张力控制
柔软的L:汉语语流中被忽视的舌肌张力控制

1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码