新闻详情

新闻详情

首页 / 资讯中心 / 详情

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

发布时间:2026/11/7 0:15:50来源:尧图网络
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、切换器不用动。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

【外设】之大彩串口显示屏 2026/11/6 6:37:16

【外设】之大彩串口显示屏

大彩串口屏初步使用 1 .官网下载 STM32 屏幕 GUI 设计资料 http://www.gz-dc.com/category/typeid/4112 找到 STM32 Keil 工程,移植相关代码因项目而异进行移植,由于项目简单,本人只对用到的指令接口进行修改。 比如:注意事项&…

阅读更多 →
无法下载Windows系统iso文件 2026/11/6 0:32:51

无法下载Windows系统iso文件

当我遇到这个问题的时候,我打开了一个网站: 登录 然后我打算下载的时候: 突然那个官方的连接就可以下载了:

阅读更多 →
【清华代码熊】DeepSeek V4.1 Flash 后训练详解 2026/11/6 1:10:54

【清华代码熊】DeepSeek V4.1 Flash 后训练详解

📌 上期解析了 DeepSeek V4.1 Flash 模型架构改进,本期解析 DeepSeek V4.1 Flash 预训练/后训练技术: 🌟 预训练:45T 文本 多模态混合语料、直接训练 sparse attention(取消 DeepSeek V4 的 dense 冷启动&…

阅读更多 →
Shuffle-R1: Efficient RL framework for Multimodal Large Language Models via Data-centric Dynamic ... 2026/11/6 17:25:36

Shuffle-R1: Efficient RL framework for Multimodal Large Language Models via Data-centric Dynamic ...

文章主要内容和创新点 主要内容 本文聚焦于多模态大语言模型(MLLM)强化学习(RL)训练中的效率问题,提出了一个名为Shuffle-R1的框架。研究发现,当前RL训练存在两个关键缺陷: 优势值坍缩(Advantage Collapsing):批次中大多数优势值集中在零附近,导致有效梯度信号被淹…

阅读更多 →
PRvL: Quantifying the Capabilities and Risks of Large Language Models for PII Redaction 2026/11/6 14:29:05

PRvL: Quantifying the Capabilities and Risks of Large Language Models for PII Redaction

一、文章主要内容总结 本文聚焦于利用大型语言模型(LLMs)实现个人身份信息(PII)脱敏的研究,旨在解决传统脱敏方法(如基于规则的系统、领域特定命名实体识别(NER)模型)泛化能力差、跨格式/跨语境适应性弱的问题。 研究通过全面评估多种LLM架构(包括密集型LLM(D-LLM…

阅读更多 →
LLaVA-RE: Binary Image-Text Relevancy Evaluation with Multimodal Large Language Model 2026/11/6 14:29:07

LLaVA-RE: Binary Image-Text Relevancy Evaluation with Multimodal Large Language Model

文章主要内容和创新点 主要内容 本文聚焦于二进制图像-文本相关性评估任务(判断图像与文本“相关”或“不相关”),针对该任务中文本格式多样、相关性定义随场景变化等挑战,提出了基于多模态大语言模型(MLLM)的解决方案LLaVA-RE。 模型设计:LLaVA-RE基于LLaVA 1.5架构,…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉