Command Palette

Search for a command to run...

Как добавить Hreflang в Next.js (маршрутизатор приложений)

Как добавить Hreflang в Next.js (маршрутизатор приложений)

T
Toolz Team
|Aug 23, 2026|13 Мин. читать

Часть коллекции SEO инструменты

Toolz работает на маршрутизаторе приложений Next.js на девяти языках, и установка hreflang прошла три неправильные версии, прежде чем она пошла правильно. Первый поставил модуль-скоп alternates объект в макете, который не может меняться в зависимости от местоположения, поэтому каждая переведенная страница объявляла английский URL-адрес своим каноническим. Это ошибка, которая удаляет переводы: она сообщает Google /es/pricing является дубликатом /pricing и не следует индексировать, что незаметно отменяет переводческую работу, пока каждая страница все еще отображается правильно.

Вторая версия исправила канонический и рекламировала каждую настроенную локаль как альтернативу, включая локали, переводы которых еще не были сгенерированы. Таким образом, сайт опубликовал страницы с именами в кластере hreflang, которые представляли собой английский текст под испанским URL-адресом. Третья версия, запущенная сейчас, получает альтернативный набор из фактического покрытия перевода на странице.

Это руководство проходит через рабочую настройку: конфигурацию локали, generateMetadata реализация, вариант карты сайта и три ошибки выше, чтобы вы могли их пропустить. Он находится под полный путеводитель по грефлангу рядом с Версия WordPress, йо-

TL;DR: В маршрутизаторе приложений грефланг приходит из alternates.languages возвращено по generateMetadata, и каноническое должно быть построено из активной локали, а не объявлено один раз в области модулей. Постройте оба из одной карты локали, излучайте полный обратный набор на каждой странице группы и включайте x-default. Только рекламируйте локали, контент которых действительно существует, или публикуете кластер, указывающий на непереведенные страницы. The генератор грефланга полезен для проверки отображаемых выходных данных по проверенному набору ссылок.

Что Next.js выдает вам из коробки?

API метаданных поддерживает напрямую hreflang. Возвращение alternates.languages из generateMetadata производит <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 метаданных документы. чего он не делает, так это решает, какие локали принадлежат набору, синхронизирует каноническое или не позволяет поставить весь объект в область действия модуля, где он не может меняться. это части, которые вы должны правильно получить, и это части, которые ломаются.

Обратите также внимание на это metadataBase влияет на относительные URL-адреса здесь. для Hreflang требуются абсолютные URL-адреса, поэтому любой набор metadataBase и использовать пути, или построить абсолютные URL-адреса самостоятельно. я предпочитаю строить их явно из настроенного происхождения, потому что развертывание предварительного просмотра, которое излучает производственные URL-адреса, является отдельной категорией проблемы.

Шаг 1: одна карта местоположения и только одна

Все, что ниже по течению, читается из этого. Локалу нужны три факта: сегмент маршрута, тег BCP 47 для hreflang и <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}`
}

выше code и тот hreflang значение - это намеренно отдельные поля. Сегмент маршрута de потому что никто не хочет /de-DE/pricing в их URL-адресах и значении hreflang de-DE потому что это то, что вы хотите объявить. сдувать их означает либо уродливые URL-адреса, либо голые de в аннотации и момент добавления pt-BR рядом pt-PT конфляция вообще перестает работать.

Шаг 2: помощник, создающий набор

Одна функция, используемая каждой страницей, поэтому форма не может перемещаться между маршрутами:

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
}

Два решения там стоит украсть.

Раннее возвращение менее чем в двух местах. Страница, существующая только на английском языке, вообще не должна издавать рефланг. Одна самореферентная аннотация не является неправильной, но это шум, и версия этого кода, которая его излучала, делала каждую страницу, доступную только на английском языке, похожей на разбитую кластерную в отчетах о сканировании.

x-default добавляется помощником, а не вызывающим абонентом. Каждая реализация, которую я видел, которая оставляет резервную копию вызывающему абоненту, в конечном итоге с одним шаблоном, который забыл ее. Сложите его в вещь, которая строит набор, и его нельзя забыть.

Шаг 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, что означает, что он не может жить в константе модуля-области, не может жить в корневой компоновке и не может быть совместно использован в сегменте локали. Каждая страница в группе в конечном итоге имеет идентичный languages карта и уникальное для себя каноническое, которое является именно формой грефланг и канонический требовать.

Поскольку карта идентична по всей группе, взаимность удовлетворяется структурно, а не дисциплиной. на немецкой странице перечислены немецкий, английский, французский, японский и x-по умолчанию; так же и на английской странице. нет логики для каждой страницы, которая могла бы опустить запись.

Шаг 4: рекламируйте только то, что существует

Это шаг, который пропускает большинство гидов, и именно он создал худшую версию нашей установки.

Если ваши переводы генерируются асинхронно или локаль наполовину заполнена, настроенный список локалей и переведенный контент представляют собой разные наборы. Реклама настроенного списка означает публикацию грефланга для страниц, которые являются текстом на английском языке, под локализованным URL-адресом. 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 } на страницах, которые существуют в локали, но пока не имеют переведенной копии. Они остаются доступными для всех, кто приходит из вашей собственной навигации, и они остаются вне индекса, пока не приземлится копия. Оба автоматически отходят, как только покрытие заполняется, без последующего развертывания.

Шаг 5: вариант карты сайта

Если вы предпочитаете держать грефланг в заголовках страниц, тот же помощник подает маршрут карты сайта. Один <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> требуется и легко забыть; без него альтернативные записи игнорируются. Обратите внимание на структуру: перечисление одной <loc> на локаль умножает файл на количество локалей и ставит каждый перевод в конкуренцию своему каноническому. Тот же охват обнаружения, часть записей.

Выберите один метод. если вы излучаете и теги заголовка, и записи карты сайта для одних и тех же URL-адресов, у вас есть два источника, которые будут дрейфовать. Наши теги заголовка для страниц и чередуются карты сайта для каталога инструментов, которые являются непересекающимися наборами.

Получает ли локаль по умолчанию префикс URL-адреса?

Это решение о маршрутизации, которое изменяет каждый URL-адрес в вашем наборе hreflang, поэтому сделайте это до того, как напишете помощник, а не после.

Две стратегии являются общими. Необходимый префикс обслуживает локаль по умолчанию без ограничений /pricing и все остальное при /de/pricing, йо- Всегда префикс обслуживает каждую локаль в сегменте, в том числе /en/pricing, с /pricing перенаправление. Промежуточное программное обеспечение Next.js поддерживает оба, а next-intl раскрывает его как a localePrefix настройка.

По мере необходимости (/pricing, /de/pricing)) Всегда (/en/pricing, /de/pricing))
Существующие английские URL Сохранился Каждый становится перенаправлением
грефланг для локали по умолчанию Точки на непредопределенном URL-адресе Точки на /en/...
цель по умолчанию Непредопределенный корень, который также является en сводка Должен выбрать одну префиксную локаль
Симметрия в коде localize() требуется ветка по умолчанию Нет ветки; каждая локаль одинакова

Я использую при необходимости префикс на существующем сайте, потому что переписывание каждого индексированного английского URL-адреса для получения симметрии - это большие затраты для небольшого выигрыша в порядке, и перенаправления на ваших страницах с наибольшим трафиком не бесплатны. В новой сборке всегда-префикс чище: the localize helper теряет свой особый случай, и никогда не возникает вопроса о том, есть ли /pricing а /en/pricing являются той же страницей.

Для hreflang в любом случае важна согласованность.URL в аннотации, URL в каноническом и URL-адрес, который обслуживает 200 без перенаправления, должен быть одной и той же строкой. Единственная комбинация, которая надежно ломается, - это всегда префикс с hreflang, по-прежнему указывающий на непрефиксированные URL-адреса: каждая запись для локали по умолчанию затем называет перенаправление, и возвращаемый тег живет на пункте назначения, а не на URL-адресе, который вы назвали.

Какой библиотекой пользоваться?

Большинство настроек App Router i18n в конечном итоге включаются next-intl или next-i18next, и ни один из них не генерирует для вас рефланг. Они решают маршрутизацию и загрузку сообщений; аннотации по-прежнему могут излучать вы. Это нормально, потому что помощник выше - двадцать строк, и вы все равно хотите, чтобы это было под вашим собственным контролем.

Забота Управляется библиотекой Ваш
Маршрутизация локалей и промежуточное программное обеспечение да
Каталоги сообщений и резервные копии да
<html lang> обычно Убедитесь, что оно соответствует значению грефланга
alternates.canonical по локали нет Стройте из активного локаля
alternates.languages нет Стройте по карте локали
x-default нет Добавить в помощницу

Единственная вещь, которую следует проверить в конкретной библиотеке: <html lang>. Структурированные данные и рефланг, противоречащие языку документов, хуже, чем их отсутствие, а также макет с жестким кодом lang="en" пока служение немецкому языку - удивительно распространенный остаток.

Как вы это проверяете?

Создайте и прочитайте фактический результат, поскольку шаблон не является доказательством:

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

Три проверки на этом выводе. канонический является URL-адресом, который вы получили. есть ровно одна запись на локаль плюс одна x-default. Каждый href является абсолютным и соответствует URL-адресу, по которому обслуживается страница, включая конечную косую черту.

Затем возьмите брата или сестру и разделите их languages блоки. они должны быть байт-идентичными; отличается только канонический. если они не идентичны, что-то на вашей странице строит набор из текущей локали, а не из группы, что является отказом взаимности в ее наиболее распространенной маскировке.

Наконец, вставьте набор в генератор меток Грефланг чтобы проверить сами коды. он проверяет каждое значение по форме ISO, помечает нераспознанные подтеги, ловит дубликаты и предупреждает, когда набор не имеет резервного копирования. Он запускает клиентскую сторону, поэтому структура промежуточного URL-адреса остается конфиденциальной. The руководство инструмента проходит рабочий процесс, и 12 распространенных ошибок грефланга покрывает, что искать, как только наценка будет действительна.

Часто задаваемые вопросы

Как добавить теги hreflang в маршрутизатор приложений Next.js?

Вернуть а alternates.languages объект из generateMetadataсопоставление каждого тега BCP 47 с его абсолютным URL-адресом и набором alternates.canonical из активного локали Next.js отображает оба в голову.

Почему каноническое должно быть построено внутри генерировать метаданные?

Потому что он должен варьироваться в зависимости от локали. Канонический модуль не может, поэтому каждая переведенная страница объявляет URL-адрес локали по умолчанию каноническим, что удаляет перевод из индекса.

Создает ли next-intl теги hreflang автоматически?

№ next-intl обрабатывает маршрутизацию и загрузку сообщений. аннотации hreflang и канонические данные для каждого местоположения по-прежнему доступны вам generateMetadata, йо-

Как добавить x-default в Next.js?

Добавить 'x-default' ключ к alternates.languages объект, указывающий на резервный URL-адрес. Next.js пропускает ключ без специальной обработки. добавьте его внутрь помощника, который создает набор, чтобы ни один шаблон не мог его забыть.

Должен ли хрефланг попасть в голову или на карту сайта в Next.js?

Либо работает, но не оба для одних и тех же URL-адресов. Теги головы через generateMetadata проще отладить Маршрут карты сайта подходит для больших каталогов и сохраняет головки страниц постными; помните xmlns:xhtml декларация о <urlset>, йо-

Что, если локаль переведена лишь частично?

Рекламируйте только те места, содержимое которых действительно существует для этой страницы, и отмечайте непереведенные страницы noindex, follow. Реклама в настроенном списке локалей публикует кластер, указывающий на непереведенные страницы.

Должны ли URL-адреса hreflang в Next.js быть абсолютными?

Да. для Hreflang требуются URL-адреса с полной квалификацией. Либо установлен metadataBase или создайте URL-адреса из настроенного источника, чтобы развертывания предварительного просмотра не выдавали производственные URL-адреса.

Как проверить визуализированный вывод hreflang?

Запустите производственную сборку, возьмите две страницы братьев и сестер и сравните. The languages блоки должны быть идентичными по байтам во всей группе, в то время как каждая каноническая точка должна находиться в своем собственном URL-адресе.


Comments

0 comments

0/2000 characters

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