Command Palette

Search for a command to run...

Come aggiungere Hreflang in Next.js (router app)

Come aggiungere Hreflang in Next.js (router app)

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

Parte della raccolta Strumenti SEO

Toolz funziona sul router dell'app Next.js in nove lingue e la configurazione hreflang ha attraversato tre versioni sbagliate prima di andare bene. Il primo ha messo un modulo-ambito alternates oggetto nel layout, che non può variare in base alla locale, quindi ogni pagina tradotta ha annunciato l'URL inglese come canonica Questo è il bug che elimina le traduzioni: lo dice a Google /es/pricing è un duplicato di /pricing e non dovrebbe essere indicizzato, il che annulla silenziosamente il lavoro di traduzione mentre ogni pagina esegue ancora il rendering corretto.

La seconda versione fissava il canonico e pubblicizzava ogni locale configurato come alternativo, inclusi i locali le cui traduzioni non erano ancora state generate Quindi il sito pubblicava pagine di denominazione del cluster hreflang che erano testo inglese sotto un URL spagnolo La terza versione, quella in esecuzione ora, deriva l'insieme alternativo dalla copertura effettiva della traduzione, per pagina.

Questa guida attraversa la configurazione di lavoro: la configurazione locale, il generateMetadata implementazione, la variante sitemap, e i tre errori sopra riportati in modo da poterli saltare Si trova sotto il guida completa di hreflang accanto alla Versione WordPress.

tl; dr: Nell'App Router, hreflang proviene da alternates.languages restituito da generateMetadata, e il canonico deve essere costruito dal locale attivo piuttosto che dichiarato una volta nell'ambito del modulo Costruisci entrambi da una mappa locale, emette l'insieme reciproco completo su ogni pagina del gruppo e includi x-default. Pubblicizza solo locali il cui contenuto esiste realmente, oppure pubblichi un cluster che punta a pagine non tradotte. IL generatore hreflang è utile per verificare l'output renderizzato rispetto a un set di riferimenti convalidato.

Cosa ti dà Next.js fuori dagli schemi?

L'API Metadati supporta direttamente hreflang Ritornando alternates.languages a generateMetadata produce il <link rel="alternate" hreflang> tag nella testa:

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 li trasforma in testa e gestisce il x-default chiave senza involucro speciale, come suo Riferimento API metadati documenti Quello che non fa è decidere quali locali appartengono al set, mantenere il canonico sincronizzato o impedire di mettere l'intero oggetto nell'ambito del modulo dove non può variare Queste sono le parti che devi ottenere correttamente e sono le parti che si rompono.

Si noti anche che metadataBase influisce sugli URL relativi qui Hreflang richiede URL assoluti, quindi entrambi impostati metadataBase e usa i percorsi, o costruisci tu stesso gli URL assoluti Preferisco costruirli esplicitamente da un'origine configurata, perché una distribuzione di anteprima che emette URL di produzione è la sua stessa categoria di problema.

Passo 1: una mappa locale e una sola

Tutto ciò che è a valle lo legge. Un locale ha bisogno di tre fatti: il segmento del percorso, il tag BCP 47 per hreflang e <html lang>, e se è attualmente in spedizione.

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

la code E il hreflang il valore sono campi deliberatamente separati Il segmento del percorso è de perché nessuno vuole /de-DE/pricing nei loro URL, e il valore hreflang è de-DE perché è quello che vuoi annunciare. Conflazionarli significa o urti URL o spogli de nell'annotazione, e nel momento in cui aggiungi pt-BR a fianco pt-PT la fusione smette del tutto di funzionare.

Passo 2: un aiutante che costruisce il set

Una funzione, utilizzata da ogni pagina, quindi la forma non può spostarsi tra i percorsi:

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
}

Due decisioni lì dentro valgono la pena di essere rubate.

Il ritorno anticipato su meno di due località. Una pagina che esiste solo in inglese non dovrebbe emettere alcun hreflang Una singola annotazione autoreferenziale non è sbagliata esattamente, ma è rumore, e la versione di questo codice che lo emetteva faceva sembrare ogni pagina solo in inglese un cluster rotto nei rapporti di scansione.

x-default viene aggiunto dall'aiutante, non dal chiamante. Ogni implementazione che ho visto che lascia il fallback al chiamante finisce con un modello che l'ha dimenticato Piegalo nella cosa che costruisce il set e non può essere dimenticato.

Passaggio 3: collegalo a generaMetadati

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

Il canonico è la linea da fissare Deve essere una funzione di locale, il che significa che non può vivere in una costante modulo-ambito, non può vivere nel layout di root e non può essere condiviso attraverso il segmento locale Ogni pagina del gruppo finisce con un identico languages mappa e un canonico unico per se stesso, che è esattamente la forma hreflang e canonico richiedere.

Poiché la mappa è identica in tutto il gruppo, la reciprocità è soddisfatta strutturalmente piuttosto che dalla disciplina La pagina tedesca elenca tedesco, inglese, francese, giapponese e x-default; così fa la pagina inglese Non esiste una logica per pagina che possa omettere una voce.

Passo 4: pubblicizza solo ciò che esiste

Questo è il passo che la maggior parte delle guide salta, ed è quello che ha prodotto la versione peggiore del nostro setup.

Se le tue traduzioni sono generate in modo asincrono, o un locale è mezzo popolato, l'elenco locale configurato e il contenuto tradotto sono insiemi diversi Pubblicizzare l'elenco configurato significa pubblicare hreflang per le pagine che sono testo in inglese sotto un URL localizzato Google segue l'annotazione, trova l'inglese dove era stato promesso il tedesco e hai prodotto contenuti duplicati in nove locali contemporaneamente.

La correzione è quella di passare una copertura reale piuttosto che la configurazione:

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

Abbinalo a robots: { index: false, follow: true } sulle pagine che esistono in un locale ma non hanno ancora una copia tradotta Rimangono raggiungibili per chiunque arrivi dalla tua navigazione e rimangono fuori dall'indice finché la copia non arriva. Entrambi scendono automaticamente una volta che la copertura si riempie, senza alcuna distribuzione di follow-up.

Passo 5: la variante della mappa del sito

Se preferisci tenere hreflang fuori dalle teste delle pagine, lo stesso aiutante alimenta un percorso della mappa del sito Uno <loc> per contenuto con supplenti da bambini, anziché uno <loc> per locale:

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

la xmlns:xhtml dichiarazione sulla <urlset> è richiesto e facile da dimenticare; senza di esso le voci alternative vengono ignorate Nota la struttura: elencarne una <loc> per locale moltiplica il file per il conteggio locale e mette ogni traduzione in competizione con il proprio canonico Stessa copertura di scoperta, una frazione delle voci.

Scegli un metodo Se emetti sia tag head che voci di sitemap per gli stessi URL, hai due fonti che andranno alla deriva I nostri sono tag head per le pagine e sitemap alternate per il catalogo degli strumenti, che sono insiemi disgiunti.

La locale predefinita ottiene un prefisso URL?

Questa è una decisione di routing che modifica ogni URL nel tuo set hreflang, quindi fallo prima di scrivere l'helper anziché dopo.

Due strategie sono comuni. Prefisso necessario serve la locale predefinita senza prefisso in /pricing e tutto il resto a /de/pricing. Prefissando sempre serve ogni locale sotto un segmento, incluso /en/pricing, con /pricing reindirizzamento. Il middleware Next.js supporta entrambi e next-intl lo espone come a localePrefix impostazione.

Se necessario (/pricing, /de/pricing) Sempre (/en/pricing, /de/pricing)
URL inglesi esistenti Conservato Ognuno diventa un redirect
hreflang per la locale predefinita Punti all'URL non prefissato Punti a /en/...
obiettivo x-default La radice non prefissata, che è anche la en URL Deve scegliere un locale prefissato
Simmetria nel codice localize() ha bisogno di un ramo locale predefinito Nessun ramo; ogni locale è uniforme

Utilizzo il prefisso necessario su un sito esistente, perché riscrivere ogni URL inglese indicizzato per ottenere simmetria è un costo elevato per una piccola vincita di ordine e i reindirizzamenti sulle tue pagine con il traffico più alto non sono gratuiti Su una build greenfield, il prefisso sempre è più pulito: il localize helper perde il suo caso speciale e non c'è mai dubbio se /pricing e /en/pricing sono la stessa pagina.

Ciò che conta per hreflang in ogni caso è la coerenza L'URL nell'annotazione, l'URL nel canonico e l'URL che serve un 200 senza reindirizzare deve essere la stessa stringa L'unica combinazione che si rompe in modo affidabile è sempre-prefissa con hreflang che punta ancora agli URL non prefissati: ogni voce per la locale predefinita nomina quindi un reindirizzamento e il tag di ritorno vive sulla destinazione anziché sull'URL che hai nominato.

Quale biblioteca dovresti usare?

La maggior parte delle configurazioni di App Router i18n finiscono su next-intl oppure next-i18next, e nessuno dei due genera hreflang per te Risolvono il routing e il caricamento dei messaggi; le annotazioni sono ancora tue da emettere Va bene, perché l'helper sopra è di venti righe e lo vuoi comunque sotto il tuo controllo.

Preoccupazione Gestito dalla biblioteca Vostro
Routing locale e middleware certo
Cataloghi dei messaggi e fallback certo
<html lang> di solito Verifica che corrisponda al valore hreflang
alternates.canonical per locale no Costruisci dal locale attivo
alternates.languages no Costruisci dalla mappa locale
x-default no Appendi nell'aiutante

L'unica cosa specifica della libreria da controllare è <html lang>. I dati strutturati e l'hreflang che contraddicono il linguaggio del documento sono peggiori di nessuno e un layout che codifica lang="en" mentre si serve il tedesco è un avanzo sorprendentemente comune.

Come lo verifica?

Costruisci e leggi l'output effettivo, perché il modello non è l'evidenza:

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

Tre controlli su quell'output Il canonico è l'URL che hai recuperato C'è esattamente una voce per locale più una x-default. Ogni href è assoluto e corrisponde all'URL in cui viene servita la pagina, barra finale inclusa.

Quindi prendi un fratello e differenzia i due languages blocchi. Dovrebbero essere identici ai byte; solo il canonico differisce. Se non sono identici, qualcosa nella tua pagina sta costruendo l'insieme dalla posizione corrente piuttosto che dal gruppo, che è il fallimento della reciprocità nel suo travestimento più comune.

Infine, incolla il set nel generatore di tag Hreflang per convalidare i codici stessi Controlla ogni valore rispetto alla forma ISO, contrassegna i sottotag non riconosciuti, cattura i duplicati e avvisa quando un set non ha fallback Esegue lato client, quindi una struttura URL di staging rimane privata Il guida strumenti attraversa il flusso di lavoro e 12 errori hreflang comuni copre cosa cercare una volta che il markup è attivo.

Domande frequenti

Come faccio ad aggiungere i tag hreflang nel router dell'app Next.js?

Restituire un alternates.languages oggetto da generateMetadatamappare ciascun tag BCP 47 sul suo URL assoluto e impostarlo alternates.canonical dalla locale attiva. Next.js rende entrambi nella testa.

Perché il canonico deve essere costruito all'interno di generaMetadata?

Perché deve variare in base alla locale Un modulo-ambito canonico non può, quindi ogni pagina tradotta dichiarerebbe l'URL della locale predefinita come canonico, che rimuove la traduzione dall'indice.

Next-intl genera tag hreflang automaticamente?

No. next-intl gestisce il routing e il caricamento dei messaggi Le annotazioni hreflang e il canonical per-locale sono ancora tue da emettere generateMetadata.

Come faccio ad aggiungere x-default in Next.js?

Aggiungi un 'x-default' chiave per il alternates.languages oggetto che punta al tuo URL di fallback Successivo.js passa la chiave senza una gestione speciale Aggiungila all'interno dell'helper che costruisce il set in modo che nessun modello possa dimenticarlo.

Hreflang dovrebbe entrare in testa o nella mappa del sito in Next.js?

O funziona, ma non entrambi per gli stessi URL Tag head tramite generateMetadata sono più facili da eseguire il debug Un percorso della mappa del sito si adatta a cataloghi di grandi dimensioni e mantiene le teste delle pagine magre; ricorda il xmlns:xhtml dichiarazione sulla <urlset>.

E se un locale viene tradotto solo in parte?

Pubblicizza solo le località il cui contenuto esiste realmente per quella pagina e contrassegna le pagine non tradotte noindex, follow. Pubblicità dell'elenco delle impostazioni locali configurato pubblica un cluster che punta a pagine non tradotte.

Gli URL hreflang in Next.js devono essere assoluti?

Sì. Hreflang richiede URL completamente qualificati. Entrambi impostati metadataBase oppure crea gli URL da un'origine configurata in modo che le distribuzioni di anteprima non emettano URL di produzione.

Come faccio a controllare l'output hreflang renderizzato?

Esegui una build di produzione, recupera due pagine di fratelli e confronta. IL languages i blocchi dovrebbero essere identici ai byte all'interno del gruppo mentre ciascun canonico punta al proprio URL.


Comments

0 comments

0/2000 characters

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