Toolz draait op de Next.js App Router in negen talen, en de hreflang setup ging door drie verkeerde versies voordat het ging rechts De eerste zet een module-scope alternates object in de lay-out, dat niet per landinstelling kan verschillen, dus elke vertaalde pagina kondigde de Engelse URL aan als canoniek. Dat is de bug die vertalingen verwijdert: het vertelt Google /es/pricing is een duplicaat van /pricing en mag niet worden geïndexeerd, waardoor het vertaalwerk stilletjes ongedaan wordt gemaakt terwijl elke pagina nog correct wordt weergegeven.
De tweede versie repareerde de canonieke en adverteerde elke geconfigureerde landinstelling als alternatief, inclusief locaties waarvan de vertalingen nog niet waren gegenereerd. Dus publiceerde de site een hreflang-cluster met een naamgeving van pagina's die Engelse tekst waren onder een Spaanse URL. De derde versie, die nu wordt uitgevoerd, leidt de alternatieve set af uit de daadwerkelijke vertaaldekking, per pagina.
Deze gids loopt door de werkopstelling: de locale config, de generateMetadata implementatie, de sitemap variant, en de drie fouten hierboven zodat je ze kunt overslaan Het zit onder de complete hreflang gids naast de WordPress-versie.
tl;dr: In de App Router komt hreflang van
alternates.languagesgeretourneerd doorgenerateMetadata, en de canonieke moet worden opgebouwd uit de actieve locale in plaats van één keer te worden gedeclareerd op module scope Bouw beide op vanuit één locale map, zend de volledige wederkerige set uit op elke pagina in de groep en voeg deze toex-default. Alleen reclame maken voor lokalen waarvan de inhoud echt bestaat, of u publiceert een cluster dat naar onvertaalde pagina's verwijst De hreflang generator is handig voor het controleren van de weergegeven uitvoer aan de hand van een gevalideerde referentieset.
Wat geeft Next.js je out of the box?
De Metadata API ondersteunt hreflang direct Terugkeren alternates.languages ten gevolge van generateMetadata produceert de <link rel="alternate" hreflang> tags in het hoofd:
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 geeft die in het hoofd en hanteert de x-default sleutel zonder speciale behuizing, zoals Metadata API-referentie documenten. wat het niet doet, is beslissen welke lokalen in de set thuishoren, de canonieke synchronisatie behouden of voorkomen dat u het hele object in de moduleomvang plaatst waar het niet kan variëren. Dat zijn de onderdelen die u goed moet krijgen, en het zijn de onderdelen die kapot gaan.
Let ook op dat metadataBase beïnvloedt hier relatieve URL's Hreflang vereist absolute URL's, dus beide ingesteld metadataBase en gebruik paden, of bouw zelf absolute URL's Ik bouw ze liever expliciet vanuit een geconfigureerde oorsprong, omdat een preview-implementatie die productie-URL's uitzendt zijn eigen categorie van problemen is.
Stap 1: één landkaart en slechts één
Alles stroomafwaarts leest hieruit Een locale heeft drie feiten nodig: het routesegment, de BCP 47 tag voor hreflang en <html lang>, en of het momenteel wordt verzonden.
// 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}`
}
de code en de hreflang waarde zijn bewust gescheiden velden Het trajectsegment is de omdat niemand wil /de-DE/pricing in hun URL's, en de hreflang-waarde is de-DE want dat is wat je wilt aankondigen Ze samenvoegen betekent ofwel lelijke URL's ofwel een kale de in de annotatie, en het moment dat je toevoegt pt-BR naast pt-PT de samensmelting werkt helemaal niet meer.
Stap 2: een helper die de set bouwt
Eén functie, die door elke pagina wordt gebruikt, zodat de vorm niet tussen routes kan afdrijven:
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
}
Twee beslissingen daarin zijn het stelen waard.
De vroege terugkeer op minder dan twee locaties. Een pagina die alleen in het Engels bestaat, zou helemaal geen hreflang moeten uitzenden. Eén enkele zelfreferentiële annotatie is niet precies verkeerd, maar het is ruis, en de versie van deze code die deze heeft uitgezonden, zorgde ervoor dat elke pagina die alleen in het Engels beschikbaar was, eruitzag als een gebroken cluster in crawlrapporten.
x-default wordt toegevoegd door de helper, niet door de beller. Elke implementatie die ik heb gezien en die de terugval aan de beller overlaat, eindigt met één sjabloon die het is vergeten. Vouw het op in het ding dat de set bouwt en het kan niet worden vergeten.
Stap 3: draad het 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])) }
: {}),
},
}
}
De canonieke is de lijn om naar te staren. Het moet een functie zijn van locale, wat betekent dat het niet in een module-scope-constante kan leven, niet in de root-indeling kan leven en niet over het landsegment kan worden gedeeld. Elke pagina in de groep eindigt met een identiek languages kaart en een canonieke uniek voor zichzelf, wat precies de vorm is hreflang en canoniek vereisen.
Omdat de kaart identiek is over de groep heen, wordt aan wederkerigheid eerder structureel dan door discipline voldaan De Duitse pagina vermeldt Duits, Engels, Frans, Japans, en x-standaard; de Engelse pagina ook Er is geen logica per pagina die een vermelding kan weglaten.
Stap 4: adverteer alleen wat bestaat
Dit is de stap die de meeste gidsen overslaan, en het is degene die de slechtste versie van onze opstelling heeft opgeleverd.
Als uw vertalingen asynchroon worden gegenereerd, of een landinstelling half gevuld is, zijn de geconfigureerde landinstellingenlijst en de vertaalde inhoud verschillende sets. Het adverteren van de geconfigureerde lijst betekent het publiceren van hreflang voor pagina's die Engelse tekst zijn onder een gelokaliseerde URL. Google volgt de annotatie, vindt Engels waar Duits was beloofd en u hebt dubbele inhoud op negen locaties tegelijk vervaardigd.
De oplossing is om echte dekking door te geven in plaats van de configuratie:
// 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)
Koppel dat met robots: { index: false, follow: true } op pagina's die in een locale bestaan maar nog geen vertaalde kopie hebben Ze blijven bereikbaar voor iedereen die vanuit je eigen navigatie aankomt, en ze blijven uit de index totdat de kopie landt Beide vallen automatisch weg zodra de dekking is ingevuld, zonder dat er een vervolg wordt geïmplementeerd.
Stap 5: de sitemapvariant
Als je liever hreflang uit paginahoofden houdt, voert dezelfde helper een sitemaproute. Een <loc> per stuk inhoud met plaatsvervangers als kinderen, in plaats van één <loc> per locatie:
// 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>`
de xmlns:xhtml verklaring op <urlset> is vereist en gemakkelijk te vergeten; zonder dit worden de alternatieve vermeldingen genegeerd Let op de structuur: er één vermelden <loc> per locale vermenigvuldigt het bestand met de locale count en plaatst elke vertaling in concurrentie met zijn eigen canonical Zelfde discovery dekking, een fractie van de inzendingen.
Kies één methode Als u zowel koptags als sitemapvermeldingen voor dezelfde URL's uitzendt, heeft u twee bronnen die zullen afdrijven. Onze zijn koptags voor pagina's en sitemap-alternaten voor de gereedschapscatalogus, dit zijn onsamenhangende sets.
Krijgt de standaard locale een URL prefix?
Dit is een routeringsbeslissing die elke URL in uw hreflang-set wijzigt, dus maak deze voordat u de helper schrijft in plaats van erna.
Twee strategieën zijn gebruikelijk. Indien nodig voorvoegsel serveert de standaardlocatie zonder voorvoegsel op /pricing en al het andere bij /de/pricing. Altijd voorvoegsel bedient elke locatie onder een segment, inclusief /en/pricing, met /pricing omleiden Next.js middleware ondersteunt beide, en next-intl stelt het bloot als een localePrefix instelling.
Indien nodig (/pricing, /de/pricing) |
Altijd (/en/pricing, /de/pricing) |
|
|---|---|---|
| Bestaande Engelse URL's | Bewaard | Iedereen wordt een omleiding |
| hreflang voor de standaard locale | Punten op de niet-voorgepaarde URL | Punten bij /en/... |
| x-standaard doel | De niet-voorgevoegde wortel, die ook de en url |
Moet één vooraf ingestelde locale kiezen |
| Symmetrie in code | localize() heeft een standaard-locale branch nodig |
Geen tak; elke locatie is uniform |
Ik gebruik de voorvoegsels die nodig zijn op een bestaande site, omdat het herschrijven van elke geïndexeerde Engelse URL om symmetrie te verkrijgen hoge kosten met zich meebrengt voor een kleine netheidswinst, en omleidingen op uw pagina's met het hoogste verkeer niet gratis zijn. Op een greenfield-build is het altijd vooraf instellen schoner: de localize helper verliest zijn speciale geval en er is nooit een vraag of /pricing en /en/pricing zijn dezelfde pagina.
Wat voor hreflang hoe dan ook belangrijk is, is consistentie. De URL in de annotatie, de URL in de canonical en de URL die een 200 bedient zonder omleiding moet dezelfde string zijn. De enige combinatie die betrouwbaar breekt, is altijd een voorvoegsel, waarbij hreflang nog steeds naar de niet-voorafgestelde URL's wijst: elke invoer voor de standaardlocatie noemt vervolgens een omleiding, en de retourtag leeft op de bestemming in plaats van op de URL die u hebt genoemd.
Welke bibliotheek moet je gebruiken?
De meeste App Router i18n-instellingen eindigen op next-intl of next-i18next, en geen van beide genereert hreflang voor je Ze lossen routering en bericht laden op; de annotaties zijn nog steeds van jou om uit te zenden Dat is prima, want de helper hierboven is twintig regels en je wilt hem sowieso onder eigen controle hebben.
| Concern | Afgehandeld door de bibliotheek | Yours |
|---|---|---|
| Locale routing en middleware | ja | |
| Berichtencatalogi en terugval | ja | |
<html lang> |
doorgaans | Controleer of het overeenkomt met de hreflangwaarde |
alternates.canonical per locale |
heel weinig | Bouw op vanuit de actieve locatie |
alternates.languages |
heel weinig | Bouw op basis van de lokale kaart |
x-default |
heel weinig | In de helper toevoegen |
Het enige bibliotheekspecifieke ding dat u moet controleren is <html lang>. Gestructureerde gegevens en hreflang die de documenttaal tegenspreken zijn slechter dan geen, en een lay-out die hardcodes bevat lang="en" het serveren van Duits is een verrassend veel voorkomende rest.
Hoe verifieer je het?
Bouw en lees de daadwerkelijke output, want de template is niet het bewijs
next build && next start
curl -s http://localhost:3000/de/preise | grep -E 'rel="(canonical|alternate)"'
Drie controles op die uitvoer De canonical is de URL die u hebt opgehaald Er is precies één invoer per locale plus één x-default. Elk href is absoluut en komt overeen met de URL waarop de pagina wordt weergegeven, inclusief trailing slash.
Haal dan een broer of zus en verspreid de twee languages blokken.ze zouden byte-identiek moeten zijn; alleen de canonieke verschilt Als ze niet identiek zijn, is iets op je pagina het opbouwen van de set van de huidige locale in plaats van van de groep, wat de wederkerigheidsfout is in zijn meest voorkomende vermomming.
Plak ten slotte de set in de hreflang tag generator om de codes zelf te valideren Het controleert elke waarde aan de hand van de ISO-vorm, vlaggen niet-herkende subtags, vangsten duplicaten, en waarschuwt wanneer een set geen fallback heeft Het draait client-side, dus een staging URL structuur blijft privé De gereedschap gids door de workflow loopt, en 12 veel voorkomende hreflang fouten dekt waar u op moet letten zodra de markup live is.
Veelgestelde vragen
Hoe voeg ik hreflang tags toe in de Next.js App Router?
Return an alternates.languages object van generateMetadata, waarbij elke BCP 47-tag wordt toegewezen aan de absolute URL en ingesteld alternates.canonical van de actieve locale Next.js renders beide in het hoofd.
Waarom moet de canonieke structuur worden gebouwd in generateMetadata?
Omdat het per land moet variëren. Een module-scope canonical kan dat niet, dus elke vertaalde pagina zou de standaard-lokale URL canoniek verklaren, waardoor de vertaling uit de index wordt verwijderd.
Genereert next-intl automatisch hreflang-tags?
Nr. next-intl verwerkt routering en het laden van berichten De hreflang-annotaties en de canonieke per-locale zijn nog steeds van jou om uit te zenden generateMetadata.
Hoe voeg ik x-default toe in Next.js?
Voeg een 'x-default' sleutel tot de alternates.languages object dat naar uw fallback-URL wijst Next.js geeft de sleutel door zonder speciale afhandeling Voeg deze toe aan de helper die de set bouwt, zodat geen enkele sjabloon deze kan vergeten.
Moet hreflang in het hoofd gaan of de sitemap in Next.js?
Of werkt, maar niet allebei voor dezelfde URL's Hoofdtags via generateMetadata zijn makkelijker te debuggen Een sitemap route past bij grote catalogi en houdt paginahoofden lean; onthoud de xmlns:xhtml verklaring op <urlset>.
Wat als een locale slechts gedeeltelijk vertaald wordt?
Adverteer alleen de locaties waarvan de inhoud echt bestaat voor die pagina, en markeer de onvertaalde pagina's noindex, follow. Door reclame te maken voor de geconfigureerde localelijst wordt een cluster gepubliceerd dat naar niet-vertaalde pagina's verwijst.
Moeten hreflang URL's in Next.js absoluut zijn?
Ja. Hreflang vereist volledig gekwalificeerde URL's. Beide ingesteld metadataBase of bouw de URL's op vanuit een geconfigureerde oorsprong, zodat voorbeeldimplementaties geen productie-URL's uitzenden.
Hoe controleer ik de weergegeven hreflang-uitvoer?
Voer een productiebuild uit, haal twee pagina's van broers en zussen op en vergelijk. De languages blokken moeten byte-identiek zijn voor de hele groep, terwijl elke canonieke verbinding op zijn eigen URL wijst.



