Command Palette

Search for a command to run...

So fügen Sie Hreflang in Next.js (App Router) hinzu

So fügen Sie Hreflang in Next.js (App Router) hinzu

T
Toolz Team
|Aug 23, 2026|13 min lesen

Teil der Sammlung SEO-Tools

Toolz läuft auf dem Next.js App Router in neun Sprachen, und das hreflang-Setup durchlief drei falsche Versionen, bevor es richtig lief. Der erste legte einen Modulumfang fest alternates Objekt im Layout, das nicht nach Gebietsschema variieren kann, so dass jede übersetzte Seite die englische URL als kanonisch ankündigteDas ist der Fehler, der Übersetzungen löscht: Es sagt Google /es/pricing ist ein Duplikat von /pricing Und nicht indiziert werden sollte, wodurch die Übersetzungsarbeit leise wieder rückgängig gemacht wird, während jede Seite noch korrekt rendert.

Die zweite Version fixierte das Kanonische und bewarb jedes konfigurierte Gebietsschema als Alternative, einschließlich der Orte, deren Übersetzungen noch nicht generiert worden waren. Daher veröffentlichte die Website einen Hreflang-Cluster, auf dem Seiten benannt wurden, die englischer Text unter einer spanischen URL waren. Die dritte Version, die jetzt läuft, leitet den alternativen Satz pro Seite aus der tatsächlichen Übersetzungsabdeckung ab.

Diese Anleitung geht durch das Arbeits-Setup: die Locale-Konfiguration, die generateMetadata Implementierung, die Sitemap-Variante, und die drei Fehler oben, damit man sie überspringen kann Es sitzt unter der Komplette Hreflang-Führung Neben der WordPress-versiondrohen

tl; dr: Im App Router kommt hreflang von alternates.languages Zurückgegeben von generateMetadata, und das Kanonische muss aus dem aktiven Gebietsschema aufgebaut werden, anstatt einmal im Modulbereich deklariert zu werden Erstellen Sie beide aus einer Gebietsschema-Karte, geben Sie den vollständigen reziproken Satz auf jeder Seite der Gruppe aus und schließen Sie ein x-default. Werben Sie nur für Lokale, deren Inhalt wirklich existiert, oder Sie veröffentlichen einen Cluster, der auf nicht übersetzte Seiten verweist Die hreflang-generator Nützlich ist, um die gerenderte Ausgabe mit einem validierten Referenzsatz zu vergleichen.

Was gibt Ihnen Next.js aus der Box?

Die Metadata API unterstützt hreflang direkt Zurückgeben alternates.languages aus generateMetadata produziert das <link rel="alternate" hreflang> Tags im Kopf:

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',
      },
    },
  }
}

Als nächstes rendert js diese in den Kopf und kümmert sich um die x-default Schlüssel ohne spezielles Gehäuse, da es Metadaten API Referenz Dokumente. was es nicht tut, ist zu entscheiden, welche Lokale in die Menge gehören, das Kanonische synchron zu halten, oder Sie stoppen Sie das ganze Objekt in Modulumfang setzen, wo es nicht variieren kann Das sind die Teile, die Sie richtig bekommen müssen, und sie sind die Teile, die brechen.

Beachten Sie auch, dass metadataBase Relative URLs hier beeinflusst Hreflang benötigt absolute URLs, also entweder gesetzt metadataBase Pfade verwenden, oder selbst absolute URLs bauen, ich baue sie lieber explizit aus einem konfigurierten Ursprung, denn eine Preview Deploy, die Produktions-URLs aussendet, ist eine eigene Problemkategorie.

Schritt 1: eine Gebietsschema-Karte und nur eine

Daraus liest sich alles flussabwärts Ein Locale braucht drei Fakten: das Streckensegment, das BCP 47 Tag für hreflang und <html lang>, und ob es gerade versendet wird.

// 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}`
}

der code und das hreflang Wert sind bewusst getrennte Felder Das Streckensegment ist de Weil niemand will /de-DE/pricing In ihren URLs ist der Hreflang-Wert de-DE Denn das ist es, was Sie bekannt geben wollen Sie zu konflieren bedeutet entweder hässliche URLs oder eine nackte de In der Anmerkung und in dem Moment, in dem Sie hinzufügen pt-BR Neben pt-PT Die Verschmelzung funktioniert überhaupt nicht mehr.

Schritt 2: ein Helfer, der das Set aufbaut

Eine Funktion, die von jeder Seite verwendet wird, sodass die Form nicht zwischen den Routen driften kann:

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
}

Zwei Entscheidungen darin sind es wert, gestohlen zu werden.

Die vorzeitige Rückkehr an weniger als zwei Orten. Eine Seite, die nur auf Englisch existiert, sollte überhaupt keinen Hreflang aussenden. Eine einzelne selbstreferenzielle Anmerkung ist nicht genau falsch, aber es ist Rauschen, und die Version dieses Codes, die sie ausgesendet hat, hat jede reine englische Seite wie ein kaputter Cluster in Crawl-Berichten aussehen lassen.

x-default Wird vom Helfer angehängt, nicht vom Anrufer. Jede Implementierung, die ich gesehen habe und die den Fallback dem Anrufer überlässt, hat am Ende eine Vorlage, die sie vergessen hat Falte sie in das Ding, das das Set aufbaut, und sie darf nicht vergessen werden.

Schritt 3: Übertragen Sie es in generateMetadata

// 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])) }
        : {}),
    },
  }
}

Das Kanonische ist die Linie, auf die man starren kann Es muss eine Funktion von sein locale, was bedeutet, dass es nicht in einer Modul-Umfangskonstante leben kann, nicht im Root-Layout leben kann und nicht über das Ortsschema hinweg geteilt werden kann. Jede Seite in der Gruppe hat am Ende eine identische languages Karte und ein für sich einzigartiges kanonisches Element, das genau die Form hat hreflang und kanonisch Erfordern.

Da die Karte gruppenübergreifend identisch ist, wird die Reziprozität eher strukturell als durch Disziplin befriedigt Die deutsche Seite listet Deutsch, Englisch, Französisch, Japanisch und x-default auf; ebenso die englische Seite. Es gibt keine Logik pro Seite, die einen Eintrag weglassen könnte.

Schritt 4: Werben Sie nur für das, was existiert

Dies ist der Schritt, den die meisten Guides überspringen, und er ist derjenige, der die schlechteste Version unseres Setups hervorgebracht hat.

Wenn Ihre Übersetzungen asynchron generiert werden, oder ein Gebietsschema zur Hälfte ausgefüllt ist, sind die konfigurierte Gebietsschema-Liste und der übersetzte Inhalt verschiedene Sätze Werbung für die konfigurierte Liste bedeutet, Hreflang für Seiten, die englischer Text sind, unter einer lokalisierten URL zu veröffentlichen Google folgt der Anmerkung, findet Englisch, wo Deutsch versprochen wurde, und Sie haben doppelte Inhalte an neun Orten gleichzeitig hergestellt.

Die Lösung besteht darin, eine echte Abdeckung und nicht die Konfiguration zu übergeben:

// 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)

Kombinieren Sie das mit robots: { index: false, follow: true } Auf Seiten, die in einem Gebietsschema existieren, aber noch keine übersetzte Kopie haben Sie bleiben für jeden erreichbar, der von Ihrer eigenen Navigation kommt, und sie bleiben aus dem Index heraus, bis die Kopie landet Beide fallen automatisch weg, sobald die Abdeckung ausgefüllt ist, ohne dass ein Follow-up bereitgestellt wird.

Schritt 5: die Sitemap-Variante

Wenn Sie lieber den Hreflang aus den Seitenköpfen heraushalten möchten, speist derselbe Helfer eine Sitemap-Route. Eine <loc> Pro Inhalt mit Ersatzkindern statt einem <loc> pro Gebietsschema:

// 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>`

der xmlns:xhtml Erklärung am <urlset> Erforderlich ist und leicht zu vergessen ist; ohne sie werden die alternativen Einträge ignoriert Beachten Sie die Struktur: eine auflisten <loc> Pro locale multipliziert die Datei mit der locale-Zählung und stellt jede Übersetzung in Konkurrenz zu ihrem eigenen kanonischen Same discovery coverage, ein Bruchteil der Einträge.

Wählen Sie eine Methode aus Wenn Sie sowohl Head-Tags als auch Sitemap-Einträge für dieselben URLs aussenden, haben Sie zwei Quellen, die driften. Unsere sind Head-Tags für Seiten und Sitemap-Alternativen für den Toolkatalog, bei denen es sich um disjunkte Sätze handelt.

Erhält das Standard-Locale ein URL-Präfix?

Dies ist eine Routing-Entscheidung, die jede URL in Ihrem Hreflang-Set ändert. Machen Sie sie also, bevor Sie den Helfer schreiben, und nicht danach.

Zwei Strategien sind üblich. Je nach Bedarf Voranstellung Dient dem Standard-Locale, das vorbestimmt ist, unter /pricing Und alles andere unter /de/pricingdrohen Immer voranstellen Erfüllt jeden Standort unter einem Segment, einschließlich /en/pricing, mit /pricing Weiterleiten. Next.js Middleware unterstützt beides und next-intl stellt es als dar localePrefix Einstellung.

Nach Bedarf (/pricing, /de/pricing) Immer (/en/pricing, /de/pricing)
Bestehende englische URLs Erhalten Jeder wird zu einer Weiterleitung
hreflang für die Standard-Locale Punkte an der nicht fixierten URL Punkte bei /en/...
x-standardziel Die vorfixierte Wurzel, die auch die en URL Muss ein vorangestelltes Gebietsschema wählen
Symmetrie im Code localize() Benötigt eine Standard-Locale-Filiale Kein Zweig; Jeder Ort ist einheitlich

Ich verwende auf einer bestehenden Website das erforderliche Präfix, da das Umschreiben jeder indizierten englischen URL zur Erlangung der Symmetrie hohe Kosten für einen kleinen Ordentlichkeitsgewinn verursacht und Weiterleitungen auf Ihren Seiten mit dem höchsten Traffic nicht kostenlos sind. Auf der grünen Wiese ist das Herstellen immer sauberer: die localize Helfer verliert seinen Spezialfall, und es stellt sich nie die Frage, ob /pricing und /en/pricing Seite sind.

Was für hreflang so oder so zählt, ist Konsistenz Die URL in der Annotation, die URL im Canonical und die URL, die eine 200 ohne Umleitung bedient, muss dieselbe Zeichenfolge sein. Die einzige Kombination, die zuverlässig bricht, ist immer die Vorabfestigung, wobei hreflang immer noch auf die nicht fixierten URLs zeigt: Jeder Eintrag für das Standard-Locale benennt dann eine Weiterleitung, und das Return-Tag lebt auf dem Ziel und nicht auf der von Ihnen benannten URL.

Welche Bibliothek sollten Sie nutzen?

Die meisten App Router i18 n-Setups landen auf next-intl oder next-i18next‘und keiner generiert für dich hreflang Sie lösen Routing und Nachrichtenladen; die Annotationen sind immer noch deine, die du aussenden musst Das ist in Ordnung, denn der Helfer oben ist zwanzig Zeilen und du willst es sowieso unter deiner eigenen Kontrolle haben.

Besorgnis erregend Von der Bibliothek verwaltet Ihre
Locale Routing und Middleware ja
Nachrichtenkataloge und Fallback ja
<html lang> üblicherweise Überprüfen Sie, ob es mit dem hreflang-Wert übereinstimmt
alternates.canonical pro Standort kein Aus dem aktiven Gebietsschema aufbauen
alternates.languages kein Bauen Sie aus der Gebietskarte
x-default kein Im Helfer anhängen

Die einzige bibliotheksspezifische Sache, die überprüft werden muss, ist <html lang>. Strukturierte Daten und Hreflang, die der Dokumentsprache widersprechen, sind schlechter als keine und ein Layout, das Hardcodes enthält lang="en" Während des Servierens ist Deutsch ein überraschend häufiger Rest.

Wie verifizierst du es?

Erstellen und lesen Sie die eigentliche Ausgabe, da die Vorlage nicht der Beweis ist:

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

Drei Überprüfungen auf dieser Ausgabe Das Kanonische ist die URL, die Sie abgerufen haben Es gibt genau einen Eintrag pro Gebietsschema plus einen x-defaultJeder href Absolut ist und mit der URL übereinstimmt, unter der diese Seite bedient wird, wobei ein nachfolgender Schrägstrich enthalten ist.

Holen Sie dann ein Geschwisterchen und verteilen Sie die beiden languages Blöcke. Sie sollten byte-identisch sein; nur das kanonische unterscheidet sich, wenn sie nicht identisch sind, baut etwas auf Ihrer Seite die Menge aus dem aktuellen Gebietsschema auf und nicht aus der Gruppe, was der Fehler der Reziprozität in ihrer häufigsten Verkleidung ist.

Zum Schluss fügen Sie das Set in die hreflang Tag Generator Um die Codes selbst zu validieren Es prüft jeden Wert mit der ISO-Form, markiert unerkannte Untertags, fängt Duplikate und warnt, wenn ein Set keinen Fallback hat Es läuft clientseitig, so bleibt eine Staging-URL-Struktur privat Die Werkzeugführung Durch den Workflow geht, und 12 häufige Hreflang-Fehler Deckt ab, worauf Sie achten müssen, sobald der Markup live ist.

Häufig gestellte Fragen

Wie füge ich hreflang-Tags im Next.js App Router hinzu?

Rückgabe ein alternates.languages Objekt von generateMetadata, ordnet jedes BCP 47-Tag seiner absoluten URL zu und setzt es alternates.canonical Aus dem aktiven Gebietsschema. next.js rendert beide in den Kopf.

Warum muss das Kanonische in generateMetadata erstellt werden?

Da es je nach Gebietsschema variieren muss. Ein kanonisches Modulumfang kann dies nicht tun, daher würde jede übersetzte Seite die Standard-Ortsmaßstab-URL als kanonisch deklarieren, wodurch die Übersetzung aus dem Index entfernt wird.

Generiert next-intl automatisch Hreflang-Tags?

No. next-intl übernimmt Routing und Nachrichtenladen Die hreflang-Anmerkungen und das kanonische Per-Locale-Anmerkungen können Sie weiterhin aussenden generateMetadatadrohen

Wie füge ich x-default in Next.js hinzu?

Fügen Sie ein hinzu 'x-default' Schlüssel zum alternates.languages Objekt, das auf Ihre Fallback-URL zeigt Next.js gibt den Schlüssel ohne besondere Handhabung durch Anhängen Sie ihn in den Helfer, der das Set erstellt, damit keine Vorlage es vergessen kann.

Sollte Hreflang in den Kopf oder die Sitemap in Next.js gehen?

Beides funktioniert, aber nicht beides für die gleichen URLs Head Tags via generateMetadata Einfacher zu debuggen sind Eine Sitemap-Route passt zu großen Katalogen und hält Seitenköpfe schlank; denken Sie an die xmlns:xhtml Erklärung am <urlset>drohen

Was ist, wenn ein Gebietsschema nur teilweise übersetzt wird?

Werben Sie nur für die Orte, deren Inhalt tatsächlich für diese Seite vorhanden ist, und markieren Sie die nicht übersetzten Seiten noindex, follow. Werbung für die konfigurierte Gebietsschema-Liste veröffentlicht einen Cluster, der auf nicht übersetzte Seiten verweist.

Müssen Hreflang-URLs in Next.js absolut sein?

Ja. Hreflang benötigt voll qualifizierte URLs. Entweder gesetzt metadataBase Oder erstellen Sie die URLs von einem konfigurierten Ursprung aus, damit Vorschaubereitstellungen keine Produktions-URLs aussenden.

Wie überprüfe ich die gerenderte hreflang-Ausgabe?

Führen Sie einen Produktionsaufbau durch, holen Sie zwei Geschwisterseiten ab und vergleichen Sie. Die languages Blöcke sollten in der gesamten Gruppe byteidentisch sein, während jede kanonische Blöcke auf ihre eigene URL zeigen.


Comments

0 comments

0/2000 characters

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