Command Palette

Search for a command to run...

如何在 Next。js(应用程序路由器)中添加 Hreflang

如何在 Next。js(应用程序路由器)中添加 Hreflang

T
Toolz Team
|Aug 23, 2026|13 最小读数

搜索引擎优化工具 合集的一部分

Toolz 在九种语言的 Next。js App 路由器上运行,hreflang 设置在向右之前经历了三个错误的版本。首先放置一个模块范围 alternates object in layout,它不能因地域设置而异,所以每个翻译页面都宣布英文网址为它的规范,那就是删除翻译的bug:它告诉谷歌 /es/pricing 是 的副本 /pricing 并且不应该被索引,这会悄悄地取消翻译工作,而每一页仍然正确地呈现。

第二个版本修复了规范,并将每个配置的区域设置作为备用区域进行广告,包括尚未生成翻译的区域。因此,该网站发布了一个 hreflang 集群命名页面,该页面是西班牙语 URL 下的英文文本。第三个版本,即现在运行的版本,从实际翻译覆盖范围中得出备用集,每页。

本指南将浏览工作设置:区域设置配置 generateMetadata 实现、站点地图变体以及上面的三个错误,以便您可以跳过它们。它位于 完整的 hreflang 指南 旁边 WordPress 版本

TL;博士: App路由器中,hreflang来自于 alternates.languages 返回由 generateMetadata并且规范必须从活动区域设置构建,而不是在模块作用域声明一次。从一个区域设置地图构建两者,在组中的每个页面上发出完整的倒数集,并包含 x-default。仅宣传其内容真实存在的本地,或者您发布指向未翻译页面的集群。 hreflang 生成器 对于根据经过验证的参考集检查渲染的输出非常有用。

Next。js 开箱给你什么?

元数据 API 直接支持 hreflang。返回 alternates.languagesgenerateMetadata 产生 <link rel="alternate" hreflang> 头部标签:

export async function generateMetadata({ params }) {
  const { locale } = await params
  return {
    alternates: {
      canonical: 'https://example.com/de/preise',
      languages: {
        'en-US': 'https://example.com/pricing',
        'de-DE': 'https://example.com/de/preise',
        'x-default': 'https://example.com/pricing',
      },
    },
  }
}

Next。js 将它们渲染到头部并处理 x-default 钥匙无特殊外壳,作为其 元数据 API 参考 documents。it不做的是决定哪些locales属于集合,保持规范同步,或者停止你把整个对象放在模块作用域,它不能改变。那些是你必须得到正确的部分,它们是断裂的部分。

另请注意 metadataBase here 影响相对的 URL,Hreflang 需要绝对的 URL,所以任一设置 metadataBase and 使用路径,或者自己构建绝对的 URL,我更喜欢从配置的源代码显式构建它们,因为一个发布生产 URL 的预览部署是它自己的问题类别。

步骤 1:一张区域图,而且只有一张

下游的一切都从中读取。一个区域需要三个事实:路线段、hreflang 的 BCP 47 标签和 <html lang>的,以及目前是否正在发货。

// i18n/locales.ts
export const DEFAULT_LOCALE = 'en'

export const LOCALES = [
  { code: 'en', hreflang: 'en-US', label: 'English' },
  { code: 'de', hreflang: 'de-DE', label: 'Deutsch' },
  { code: 'fr', hreflang: 'fr-FR', label: 'Français' },
  { code: 'ja', hreflang: 'ja-JP', label: '日本語' },
]

/** '/pricing' -> '/de/pricing', and '/pricing' for the default locale. */
export function localize(path: string, locale: string): string {
  return locale === DEFAULT_LOCALE ? path : `/${locale}${path}`
}

codehreflang value是故意分割的字段,路由段是 de 因为没有人想要 /de-DE/pricing 在他们的 URL 中,hreflang 值为 de-DE 因为那是你想宣布的。将它们混为一谈意味着要么是丑陋的 URL,要么是赤裸裸的 de 在注释中,以及添加的那一刻 pt-BRpt-PT 混为一谈根本停止了。

第 2 步:构建该集的帮助者

1个函数, 每页都使用, 所以形状不能在路由之间漂移:

export function hreflangAlternates(path: string, locales: string[], base = SITE_URL) {
  const eligible = LOCALES.filter((l) => locales.includes(l.code))
  // A single-entry cluster is not a cluster: emit nothing rather than
  // advertise a one-sided relationship.
  if (eligible.length < 2) return []

  const alts = eligible.map((l) => ({
    hreflang: l.hreflang,
    href: base + localize(path, l.code),
  }))
  alts.push({ hreflang: 'x-default', href: base + localize(path, DEFAULT_LOCALE) })
  return alts
}

其中两个决定值得窃取。

提前返回少于两个地点。 只存在英文的页面应该根本不会发出hreflang,单个自指注释并不是完全错误的,而是噪声,而这个代码发出它的版本让每个只用英文的页面看起来都像是爬行报表中一个破碎的集群。

x-default 由帮助者附加,而不是由呼叫者附加。 I’看到的每个实现都将后备留给调用者,最终会有一个模板忘记了它。将其折叠到构建集合的东西中,它不能被忘记。

第 3 步:将其连接到生成元数据中

// app/[locale]/pricing/page.tsx
import { localize, hreflangAlternates, SHIPPING_LOCALES } from '@/i18n/locales'
import { SITE_URL } from '@/lib/site'

export async function generateMetadata({ params }) {
  const { locale } = await params
  const path = '/pricing'

  const alternates = hreflangAlternates(path, SHIPPING_LOCALES)

  return {
    alternates: {
      // Built from the ACTIVE locale. This is the line that matters.
      canonical: `${SITE_URL}${localize(path, locale)}`,
      ...(alternates.length
        ? { languages: Object.fromEntries(alternates.map((a) => [a.hreflang, a.href])) }
        : {}),
    },
  }
}

正典是要盯着的线。它必须是 的函数 locale(module-scope constant)中,这意味着它不能住在模块范围常量中,不能住在根布局中,也不能跨区域设置段共享,组中的每个页面最终都有一个相同的页面 languages map和本身特有的规范,这正是形状 hreflang 和规范 需要。

由于整个组的地图是相同的,因此互惠在结构上而不是在纪律上得到满足。德语页面列出了德语、英语、法语、日语和 x 默认值;英语页面也是如此。没有可以省略条目的每页逻辑。

4步:只宣传存在的东西

这是大多数指南跳过的步骤,也是产生我们设置的最差版本的步骤。

如果您的翻译是异步生成的,或者某个区域设置是半填充的,则配置的区域设置列表和翻译的内容是不同的集合。在配置列表中做广告意味着在本地化 URL 下发布英文文本页面的 hreflang。Google 遵循注释,找到承诺德语的英语,并且您已同时在九个区域中制造了重复内容。

修复是通过真正的覆盖范围而不是配置:

// Coverage is per-page, not global: this page might be translated
// into three languages while the one next to it has none.
const locales = await localesForPage(path)
const alternates = hreflangAlternates(path, locales)

配对 robots: { index: false, follow: true } locale 中存在但尚未翻译副本的页面上,对于从您自己的导航到达的任何人来说,它们都保持可触及性,并且它们一直处于索引之外,直到副本落地。一旦覆盖范围填满,两者都会自动丢弃,并且不会进行后续部署。

第 5 步:站点地图变体

如果您宁愿将 hreflang 放在页面头之外,同一个助手会提供站点地图路线。一个 <loc> 每条内容都是儿童时期交替的内容,而不是一个 <loc> 每个区域:

// app/sitemap-pages.xml/route.ts
const entries = PAGES.map((path) => {
  const alts = hreflangAlternates(path, SHIPPING_LOCALES)
  const links = alts
    .map((a) => `\n    <xhtml:link rel="alternate" hreflang="${a.hreflang}" href="${a.href}"/>`)
    .join('')
  return `  <url>\n    <loc>${SITE_URL}${path}</loc>${links}\n  </url>`
})

const xml =
  `<?xml version="1.0" encoding="UTF-8"?>\n` +
  `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" ` +
  `xmlns:xhtml="http://www.w3.org/1999/xhtml">\n${entries.join('\n')}\n</urlset>`

xmlns:xhtml 声明于 <urlset> is必填 易忘记;没有它 备用条目被忽略 注意结构:列出一个 <loc> per locale 将文件乘以 locale 计数,并将每个翻译都与自己的规范竞争。相同的发现覆盖范围,即条目的一小部分。

Pick一种方法,如果同时发出相同URL的头部标签和站点地图条目,那么你有两个来源会漂移,我们的是页面的头部标签和工具目录的站点地图替代品,它们是不相交的集合。

默认区域设置是否得到 URL 前缀?

这是一个路由决策,会更改 hreflang 集中的每个 URL,因此请在编写帮助程序之前而不是之后进行。

有两种策略是常见的。 根据需要加前缀 服务于未修复的默认区域设置 /pricing 以及其他一切 /de/pricing始终加前缀 为段下的每个区域提供服务,包括 /en/pricing,与 /pricing redirecting。next。js 中间件两者都支持,并且 next-intl 将其暴露为 a localePrefix 设置。

根据需要(/pricing/de/pricing) 总是(/en/pricing/de/pricing)
现有的英文 URL 保存 每一个都变成了重定向
hreflang 用于默认区域设置 未修复 URL 处的点 点在 /en/...
x-默认目标 未加固的根,也是 en 网址 必须选择一个前缀区域设置
代码的对称性 localize() 需要一个默认本地分支 没有分支;每个地点都是统一的

I在现有站点上使用按需前缀,因为重写每个索引的英文URL以获得对称性对于小整洁获胜来说是一个很大的成本,并且在您流量最高的页面上重定向不是免费的,在greenfield构建上,始终前缀更干净:the localize helper 丢失了它的特例,从来没有一个问题就是是否 /pricing/en/pricing 是同一页。

hreflang无论哪种方式都重要的是一致性,注释中的url,规范中的url,以及服务于200而不重定向的url必须是同一个字符串,可靠地断开的一个组合总是- 前缀,hreflang仍然指向未预先固定的url:默认区域设置的每个条目然后命名一个重定向,返回标签就住在目的地而不是你命名的url上。

您应该使用哪个库?

大多数应用程序路由器 i18n 设置最终都会启动 next-intlnext-i18next的,而且都不是给你生hreflang的,解决路由和消息加载;注释还是你发的,那就好了,因为上面的帮手是二十行,反正你要自己控制。

由图书馆处理 你的
区域设置路由和中间件 是的
消息目录和后备 是的
<html lang> 通常 验证它是否与 hreflang 值匹配
alternates.canonical 每个区域 从活动区域设置构建
alternates.languages 从区域图构建
x-default 在助手中附加

需要检查的图书馆专用内容之一是 <html lang>。与文档语言相矛盾的结构化数据和 hreflang 比没有更糟糕,并且是硬编码的布局 lang="en" 在为德语服务时,这是一个令人惊讶的常见剩菜。

如何验证?

Build并读取实际输出, 因为模板不是证据:

next build && next start
curl -s http://localhost:3000/de/preise | grep -E 'rel="(canonical|alternate)"'

3次检查那个输出,规范就是你取的URL,每个区域设置正好有一个条目加一个 x-default.每 href 是绝对的,并且与页面所服务的 URL 匹配,包括尾部斜杠。

然后找一个兄弟姐妹并区分两人 languages block。they应该是字节相同的;只有规范不同。如果它们不相同,那么你的页面中的一些东西是从当前区域设置而不是从组构建集合,这是最常见的伪装中的互惠失败。

最后,将集合粘贴到 hreflang 标签生成器 验证代码本身。它根据 ISO 形状检查每个值,标记未识别的子标签,捕获重复项,并在集合没有后备时发出警告。它运行客户端,因此分期 URL 结构保持私密性 工具指南 走过工作流程,并且 12个常见的hreflang错误 涵盖标记上线后要寻找的内容。

常见问题

如何在 Next。js 应用程序路由器中添加 hreflang 标签?

返回一个 alternates.languages 对象来自 generateMetadata、将每个BCP 47标签映射到其绝对URL,并进行设置 alternates.canonical 从活动区域设置。 Next。js 将两者渲染到头部。

为什么必须在生成元数据内部构建规范?

因为它必须因区域设置而异。模块范围的规范无法做到这一点,因此每个翻译页面都会声明默认本地 URL 为规范,从而从索引中删除翻译。

Next-intl 是否会自动生成 hreflang 标签?

No。next-intl 处理路由和消息加载。hreflang 注释和全本地规范仍然是您要发出的 generateMetadata

如何在 Next。js 中添加 x 默认值?

添加一个 'x-default' 的钥匙 alternates.languages object指向你的后备网址。next。js通过密钥,无需特殊处理。将其附加到构建集合的帮助程序中,这样就没有模板可以忘记它。

hreflang 是应该进入 Next。js 中的头部还是站点地图?

要么有效,但不能同时适用于相同的 URL。头标签通过 generateMetadata 更容易调试。站点地图路线适合大型目录并保持页面头部精简;记住 xmlns:xhtml 声明于 <urlset>

如果一个区域仅部分翻译怎么办?

仅宣传该页面真正存在其内容的地点,并标记未翻译的页面 noindex, follow。在广告中发布配置的区域设置列表,发布指向未翻译页面的集群。

Next。js 中的 hreflang URL 需要绝对吗?

是的。Hreflang 需要完全合格的 URL。任一设置 metadataBase 或者从配置的源构建 URL,以便预览部署不会发出生产 URL。

如何检查渲染的 hreflang 输出?

运行一个生产构建,获取两个兄弟页面并进行比较。 languages 块在整个组中应该是字节相同的,而每个规范点都位于自己的 URL 上。


Comments

0 comments

0/2000 characters

No comments yet. Be the first to share your thoughts!