Zum Inhalt springen

Korrektes hreflang in Next.js - generiert, nicht gepflegt

Die meisten mehrsprachigen Next.js-Sites liefern einen kaputten hreflang-Cluster aus. Der Fehler ist immer derselbe - die Tags werden von Hand geschrieben.

10 Min. Lesezeit

Die hreflang-Spezifikation ist kurz und die Regeln sind einfach. Fast jede Implementierung, die wir auditieren, macht es trotzdem falsch, und fast immer aus derselben Grundursache: jemand pflegt die Alternates von Hand, die Übersetzungen bewegen sich, und die Tags nicht.

Die drei Regeln, die gebrochen werden

1. Jede Seite eines Clusters muss jede Seite des Clusters auflisten, sich selbst eingeschlossen. hreflang ist bidirektional. Zeigt Ihre englische Seite auf die deutsche, die deutsche aber nicht zurück, verwirft Google die Beziehung. Die Selbstreferenz ist nicht optional.

2. Werben Sie nicht mit einer Übersetzung, die es nicht gibt. hreflang="de" auf eine englische Seite zeigen zu lassen, weil die deutsche Fassung „bald kommt", ist schlimmer als sie wegzulassen. Sie sagen dem Crawler, dass Deutschsprachige eine deutsche Seite haben - und enttäuschen sie dann.

3. x-default ist für den Fallback, nicht für Englisch. Es markiert die Seite, die Nutzern ausgeliefert wird, deren Sprache Sie nicht adressieren - meist Ihre Sprachauswahl oder Ihre Standardsprache, falls Sie keine haben.

Warum handgepflegte Tags immer verrotten

Betrachten Sie den Lebenszyklus. Sie starten auf Englisch und Deutsch. Jemand schreibt die Metadaten:

// The version that will be wrong within a month
alternates: {
  languages: {
    en: 'https://example.com/pricing',
    de: 'https://example.com/de/pricing',
  },
}

Dann kommt Spanisch dazu, aber nur für die Marketing-Seiten. Dann wird ein deutscher Artikel depubliziert. Dann benennt jemand einen Slug um. Jedes davon ist ein eigener Pull Request, in einem anderen Teil der Codebasis, und keiner davon fasst dieses Objekt an.

Sechs Monate später meldet die Search Console „keine Rückverweis-Tags" für ein paar hundert URLs, und niemand kann rekonstruieren, warum.

Aus dem Inhalt generieren, nicht aus einer Liste

Die Lösung besteht darin, die Alternates zu einem abgeleiteten Wert zu machen. In der Codebasis dieser Site - die öffentlich ist - beantwortet die Content-Schicht eine Frage:

/**
 * Which locales actually have this document. Translations are published as
 * they are written, so a page's hreflang cluster must reflect reality.
 */
export function availableLocalesFor(
  collection: Collection,
  slug: string,
): Locale[] {
  return locales.filter((locale) =>
    fs.existsSync(path.join(collectionDir(collection, locale), `${slug}.mdx`)),
  );
}

Und der Metadaten-Helfer konsumiert sie:

const languages: Record<string, string> = {};
for (const l of availableLocales) {
  languages[hreflangMap[l]] = localizedUrl(l, path);
}
 
// `x-default` only makes sense when the default locale really has the page.
if (availableLocales.includes(defaultLocale)) {
  languages['x-default'] = localizedUrl(defaultLocale, path);
}
 
return {
  alternates: { canonical: localizedUrl(locale, path), languages },
  // ...
};

Löschen Sie eine Übersetzungsdatei, und das Tag verschwindet beim nächsten Build. Fügen Sie eine hinzu, und es erscheint. Es gibt keine Liste, die man zu aktualisieren vergessen kann, weil es keine Liste gibt.

Die Selbstreferenz kommt gratis: die aktuelle Sprache ist per Definition in availableLocales und wird deshalb immer ausgegeben.

Der Fehler, der einen ganzen Cluster zerstört: Canonical gegen hreflang

Alles oben setzt voraus, dass jede Seite auf sich selbst kanonisch ist. Ist sie das nicht, funktioniert nichts davon - und das ist der schädlichste Fehler, den wir in freier Wildbahn finden.

Das Muster sieht im Pull Request vernünftig aus. Jemand sorgt sich um Duplicate Content über Sprachen hinweg und richtet den Canonical jeder Sprachversion auf das englische Original:

// Jede übersetzte Seite bittet jetzt darum, nicht indexiert zu werden
alternates: {
  canonical: 'https://example.com/pricing',
  languages: { en: '...', de: '...', es: '...' },
}

Ein Canonical heißt „indexiere diese andere URL statt meiner". Also bittet die deutsche Seite darum, verworfen zu werden, die spanische ebenso, und die Alternates zeigen auf Seiten, die sich aus dem Index abgemeldet haben. Der Cluster fällt auf eine Seite zusammen, und das Symptom - übersetzte Seiten, die in ihrer eigenen Sprache nie auftauchen - sieht seiner Ursache überhaupt nicht ähnlich.

Übersetzungen sind kein Duplicate Content. Jede Sprachversion ist auf sich selbst kanonisch, und hreflang ist das, was dem Crawler sagt, dass es Versionen voneinander sind. Das ist die gesamte Arbeitsteilung zwischen den beiden Tags.

Automatische Weiterleitungen, und warum der Crawler Ihre anderen Sprachen nie sieht

Der zweite strukturelle Fehler steckt gar nicht in den Tags. Es ist eine Middleware, die Accept-Language liest und weiterleitet.

Googlebot crawlt überwiegend von US-Adressen mit Accept-Language: en-US und variiert das nicht, um zu entdecken, was Sie sonst noch haben. Wird eine Anfrage an /de/pricing auf /pricing umgeleitet, weil der Crawler kein Deutsch verlangt hat, dann existiert die deutsche Seite für den Index nicht - und jedes hreflang-Tag, das auf sie zeigt, zeigt auf eine Weiterleitung.

Die Regel lautet: Sprachaushandlung darf vorschlagen, nie erzwingen:

  • Nur von einer nackten Wurzel ohne eigene Sprachversion weiterleiten, nie von einer URL, die bereits eine benennt.
  • Die Wahl in einem Cookie festhalten, damit jemand, der die Vermutung überschreibt, nicht bei jeder Navigation erneut bekämpft wird.
  • Wo möglich die Alternative als schließbaren Hinweis anbieten statt als Weiterleitung.

Ein Sprachpräfix in der URL ist eine Absichtserklärung - von einem Menschen oder einem Crawler. Es zu überschreiben ist der Weg zu drei Sprachversionen, die außerhalb des Büros nie jemandem ausgeliefert wurden.

Wenn nur ein Teil der Seiten übersetzt ist

Teilabdeckung ist der Normalzustand einer echten Website und kein Fehler. Der Fehler ist, den Cluster als Eigenschaft der Website zu behandeln statt als Eigenschaft der Seite.

Die Website unterstützt vier Sprachen; dieser Artikel existiert in zweien. Sein Cluster hat zwei Einträge. Der Artikel daneben hat vier. Beides ist korrekt, und beides wird berechnet statt deklariert - genau deshalb lohnt sich der abgeleitete Ansatz oben.

Der eine Fall, der eine Entscheidung braucht, ist eine Seite, die in einer Sprachversion existiert, aber unübersetzt ist: auf Englisch unter einer deutschen URL veröffentlicht, weil die Navigation die Route auflösen muss. Entweder übersetzen oder aus dem deutschen Build herausnehmen. Was Sie nicht tun sollten: Englisch unter /de/... ausliefern und es hreflang="de" taggen.

noindex und hreflang vertragen sich nicht

Eine Seite mit noindex kann an keinem Cluster teilnehmen. Sie wird verworfen, und weil hreflang bidirektional ist, schwächt ihr Fehlen die Reziprozität jeder Seite, die auf sie gezeigt hat.

Das trifft Staging-Domains und Preview-Deployments, die Produktions-Metadaten tragen. Liefert eine Preview-Umgebung dieselben Alternates wie die Produktion aus, bewerben Sie einen Cluster, dessen Mitglieder mit noindex antworten. Liefern Sie auf Previews Disallow: / und ein noindex, und leiten Sie die Origin in den Metadaten aus dem Deployment ab, statt sie so fest zu verdrahten, dass Previews sie erben.

Sprachcodes, und wann eine Region dazugehört

Verwenden Sie den bloßen Sprachcode, sofern Sie nicht wirklich unterschiedliche Inhalte an unterschiedliche Regionen ausliefern:

  • de - Deutschsprachige überall.
  • de-AT - nur wenn österreichische Besucher andere Preise, Verfügbarkeiten oder Rechtstexte bekommen als deutsche.

Ein Regionscode, den Sie nicht begründen können, teilt Ihre Signale ohne Nutzen auf zwei Cluster auf. Die üblichen Ausnahmen sind echt: en-GB gegen en-US für Schreibweise und Währung, es-MX gegen es-ES für Vokabular und Preise, pt-BR gegen pt-PT.

Die Region ist für sich genommen nie ein Länder-Targeting. hreflang="de" heißt nicht „in Deutschland zeigen"; es heißt „diese Seite ist für Deutschsprachige".

Arabisch und andere RTL-Sprachen

Rechts-nach-links-Sprachen brauchen von hreflang nichts Besonderes - ar ist ein Sprachcode wie jeder andere. Was sie brauchen, ist ein korrektes dir am Dokument, und das muss pro Sprache gesetzt werden:

export default async function LocaleLayout({ children, params }) {
  const { locale } = await params;
 
  return (
    <html lang={locale} dir={dirFor(locale)}>
      {/* ... */}
    </html>
  );
}

Schreiben Sie Ihr Layout dann in logischen CSS-Eigenschaften - padding-inline-start statt padding-left, margin-inline-end statt margin-right, text-align: start statt left. Tailwinds ps-*, pe-*, ms-* und me-* bilden genau das ab. Machen Sie das von Anfang an, und das arabische Layout spiegelt sich von selbst; rüsten Sie es später nach, und Sie verbringen eine Woche damit, die zwölf Stellen zu finden, die es nicht taten.

Die Ausnahmen, die nicht spiegeln sollten: Codeblöcke, E-Mail-Adressen, URLs und Markennamen in lateinischer Schrift. Setzen Sie dort explizit direction: ltr.

Wie Sie es überprüfen

Drei Prüfungen, in aufsteigender Verlässlichkeit:

  1. Den Quelltext ansehen. Nicht den Inspektor - der zeigt das hydratisierte DOM. curl -s https://example.com/de/pricing | grep alternate zeigt, was der Crawler bekommt.
  2. Search Console → Internationale Ausrichtung. Meldet „keine Rückverweis-Tags" und „unbekannter Sprachcode" für die gesamte Property.
  3. Crawlen. Screaming Frog oder Sitebulb kartieren den vollständigen Cluster und sagen Ihnen, welchen Seiten reziproke Tags fehlen. Das fängt die Fälle ab, in denen die Tags einzeln gültig sind, der Graph aber unvollständig ist.

Es gibt einen vierten, und er ist der Einzige, der den Fehler abfängt, bevor er ausgeliefert wird. Diese Website ruft bei jedem Push jede URL ihrer eigenen Sitemap ab und lässt den Build scheitern, wenn eine Seite in ihrem eigenen hreflang-Cluster fehlt, wenn ein Canonical fehlt oder woanders hinzeigt, oder wenn eine doppelte URL-Form antwortet statt weiterzuleiten. Ein generierter Cluster beseitigt die Fehlerklasse, bei der jemand vergisst, eine Liste zu aktualisieren; eine Prüfung in der CI beseitigt die Klasse, bei der die Generierung selbst falsch ist.

Die Sitemap muss zustimmen

hreflang kann im HTML-Head oder in der Sitemap stehen. Wenn Sie beides ausgeben

  • und das sollten Sie - müssen sie exakt übereinstimmen, denn ein Widerspruch zwischen ihnen wird unvorhersehbar aufgelöst.

Dasselbe Prinzip gilt: generieren Sie die Sitemap aus demselben Helfer, den auch die Seiten verwenden. Das behandeln wir, zusammen mit der Korrektheit kanonischer URLs, in Canonicals und Sitemaps, die nicht auseinanderdriften können.

Verwandte Fragen

Sagt hreflang Google, in welchem Land die Seite gezeigt werden soll?
Nein. Es deklariert die Sprache, für die eine Seite geschrieben ist, und optional die Region, die diese Sprachvariante bedient. Länder-Targeting ist ein eigenes Signal. Eine mit hreflang="de" markierte Seite wird deutschsprachigen Nutzern angeboten, wo immer sie sind - nicht Besuchern in Deutschland.
Unser Canonical zeigt auf jeder Sprachversion auf die englische Seite. Ist das ein Problem?
Das ist der eine Fehler, der einen ganzen Cluster zerstört. Ein Canonical sagt „indexiere diese andere URL statt meiner", also bittet jede übersetzte Seite darum, nicht indexiert zu werden. Die Alternates zeigen dann auf Seiten, die sich abgemeldet haben. Jede Sprachversion muss auf sich selbst kanonisch sein.
Gehört hreflang in den Head oder in die Sitemap?
Beides funktioniert, und beides zusammen ist in Ordnung, solange sie exakt übereinstimmen. Ein Widerspruch zwischen ihnen wird unvorhersehbar aufgelöst, was schlechter ist, als nur eines davon auszuliefern. Generieren Sie beide aus derselben Quelle, dann können sie sich nicht widersprechen.
Wir haben nur die Hälfte der Seiten übersetzt. Ist ein Teil-Cluster trotzdem gültig?
Ja, und das ist der Normalfall. Ein Cluster ist die Menge der Sprachen, in denen eine bestimmte Seite existiert - nicht die Menge, die die Website unterstützt. Der Fehler ist nicht die Teilabdeckung, sondern eine Übersetzung zu bewerben, die nie geschrieben wurde.

← Zurück zu allen Artikeln

Anrufen+1 848 272 7583WhatsApp+90 850 308 5436E-Mailinfo@codefacture.comKontaktseite