Toolz se ejecuta en el enrutador de aplicaciones Next.js en nueve idiomas y la configuración de hreflang pasó por tres versiones incorrectas antes de que saliera bien. El primero puso un módulo-alcance alternates objeto en el diseño, que no puede variar según la configuración regional, por lo que cada página traducida anunció la URL en inglés como canónica. Ese es el error que elimina las traducciones: le dice a Google /es/pricing es un duplicado de /pricing y no debe indexarse, lo que deshace silenciosamente el trabajo de traducción mientras cada página aún se representa correctamente.
La segunda versión arregló lo canónico y anunció cada configuración regional como alternativa, incluidas las ubicaciones cuyas traducciones aún no se habían generado. Entonces, el sitio publicó un grupo de hreflang nombrando páginas que eran texto en inglés bajo una URL en español. La tercera versión, la que se está ejecutando ahora, deriva el conjunto alternativo de la cobertura de traducción real, por página.
Esta guía recorre la configuración de trabajo: la configuración local, la generateMetadata implementación, la variante del mapa del sitio y los tres errores anteriores para que puedas omitirlos. Se encuentra debajo del guía completa de hreflang junto al Versión de WordPress.
TL;Dr: En App Router, proviene de hreflang
alternates.languagesdevuelto porgenerateMetadata, y lo canónico debe construirse a partir de la configuración regional activa en lugar de declararse una vez en el alcance del módulo. Construya ambos a partir de un mapa de configuración regional, emita el conjunto recíproco completo en cada página del grupo e incluyax-default. Sólo anuncie lugares cuyo contenido exista genuinamente, o publique un clúster que apunte a páginas no traducidas. El generador hreflang es útil para comparar la salida renderizada con un conjunto de referencia validado.
¿qué te ofrece Next.js de fábrica?
La API de metadatos admite hreflang directamente. Regresando alternates.languages de generateMetadata produce el <link rel="alternate" hreflang> etiquetas en la cabeza:
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 los representa en la cabeza y los maneja x-default llave sin carcasa especial, como su Referencia de API de metadatos documentos. Lo que no hace es decidir qué ubicaciones pertenecen al conjunto, mantener el canónico sincronizado o evitar que coloques todo el objeto en el alcance del módulo donde no puede variar. Esas son las partes que tienes que corregir y son las partes que se rompen.
Tenga en cuenta también eso metadataBase afecta las URL relativas aquí. Hreflang requiere URL absolutas, por lo que cualquiera de los dos se establece metadataBase y utilice rutas o cree URL absolutas usted mismo. Prefiero construirlas explícitamente desde un origen configurado, porque una implementación de vista previa que emite URL de producción es su propia categoría de problema.
Paso 1: un mapa local y solo uno
Todo lo que se lee aguas abajo de esto. Un lugar necesita tres datos: el segmento de ruta, la etiqueta BCP 47 para hreflang y <html lang>, y si se está enviando actualmente.
// 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}`
}
los code y el hreflang los valores son campos deliberadamente separados. El segmento de ruta es de porque nadie quiere /de-DE/pricing en sus URL, y el valor hreflang es de-DE porque eso es lo que quieres anunciar. Combinarlos significa URL feas o simples de en la anotación y en el momento en que agregas pt-BR al lado pt-PT la combinación deja de funcionar en absoluto.
Paso 2: un ayudante que construye el conjunto
Una función, utilizada por cada página, para que la forma no se desvíe entre rutas:
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
}
Vale la pena robar dos decisiones allí.
El regreso anticipado en menos de dos localidades. Una página que existe sólo en inglés no debería emitir ningún hreflang. Una única anotación autorreferencial no está exactamente equivocada, pero es ruido, y la versión de este código que la emitió hizo que cada página solo en inglés pareciera un grupo roto en los informes de rastreo.
x-default lo añade el ayudante, no la persona que llama. Cada implementación que he visto que deja el respaldo a la persona que llama termina con una plantilla que la olvidó. Dóblalo en lo que construye el conjunto y no se puede olvidar.
Paso 3: conéctelo para generar metadatos
// 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])) }
: {}),
},
}
}
Lo canónico es la línea a la que mirar. Debe ser una función de locale, lo que significa que no puede vivir en una constante de alcance de módulo, no puede vivir en el diseño raíz y no se puede compartir en todo el segmento local. Cada página del grupo termina con una idéntica languages mapa y un canónico único en sí mismo, que es exactamente la forma hreflang y canónico requerir.
Debido a que el mapa es idéntico en todo el grupo, la reciprocidad se satisface estructuralmente más que por disciplina. La página alemana enumera alemán, inglés, francés, japonés y x-default; también lo hace la página en inglés. No existe una lógica por página que pueda omitir una entrada.
Paso 4: anuncie únicamente lo que existe
Este es el paso que la mayoría de las guías omiten y es el que produjo la peor versión de nuestra configuración.
Si sus traducciones se generan de forma asincrónica o una configuración regional está medio poblada, la lista de configuración regional configurada y el contenido traducido son conjuntos diferentes. Publicitar la lista configurada significa publicar hreflang para páginas que son texto en inglés bajo una URL localizada. Google sigue la anotación, encuentra el inglés donde se prometió el alemán y ha fabricado contenido duplicado en nueve ubicaciones a la vez.
La solución es pasar cobertura real en lugar de la configuración:
// 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)
Combina eso con robots: { index: false, follow: true } en páginas que existen en una configuración regional pero que aún no tienen una copia traducida. Siguen siendo accesibles para cualquiera que llegue desde su propia navegación y permanecen fuera del índice hasta que llega la copia. Ambos desaparecen automáticamente una vez que se completa la cobertura, sin implementación de seguimiento.
Paso 5: la variante del mapa del sitio
Si prefiere mantener hreflang fuera de los encabezados de página, el mismo ayudante proporciona una ruta de mapa del sitio. Uno <loc> por contenido con suplentes cuando eran niños, en lugar de 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>`
los xmlns:xhtml declaración en <urlset> es obligatorio y fácil de olvidar; sin él se ignoran las entradas alternativas. Tenga en cuenta la estructura: enumerar uno <loc> por configuración regional multiplica el archivo por el recuento de configuración regional y pone cada traducción en competencia con su propio canónico. Misma cobertura de descubrimiento, una fracción de las entradas.
Elija un método. Si emite etiquetas principales y entradas de mapas de sitio para las mismas URL, tendrá dos fuentes que se desviarán. Las nuestras son etiquetas principales para páginas y alternativas de mapas de sitio para el catálogo de herramientas, que son conjuntos disjuntos.
¿la configuración regional predeterminada obtiene un prefijo de URL?
Esta es una decisión de enrutamiento que cambia cada URL de su conjunto hreflang, así que hágalo antes de escribir el ayudante en lugar de después.
Dos estrategias son comunes. Prefijo según sea necesario sirve la configuración regional predeterminada sin prefijo en /pricing y todo lo demás en /de/pricing. Siempre prefijando sirve a todas las ubicaciones bajo un segmento, incluido /en/pricing, con /pricing redireccionamiento. El middleware Next.js admite ambos y next-intl lo expone como localePrefix configuración.
Așa nevoie (/pricing, /de/pricing) |
Siempre (/en/pricing, /de/pricing) |
|
|---|---|---|
| Url en inglés existentes | Conservado | Cada uno se convierte en una redirección |
| hreflang para la configuración regional predeterminada | Puntos en la URL sin prefijo | Puntos en /en/... |
| objetivo x-predeterminado | La raíz sin prefijo, que también es la en URL |
Debe elegir una ubicación con prefijo |
| Simetría en código | localize() necesita una sucursal local predeterminada |
Sin rama; cada lugar es uniforme |
Utilizo prefijos según sea necesario en un sitio existente, porque reescribir cada URL en inglés indexada para obtener simetría es un gran costo para una pequeña ganancia de orden y las redirecciones en sus páginas de mayor tráfico no son gratuitas. En una compilación totalmente nueva, siempre el prefijo es más limpio: el localize helper pierde su caso especial y nunca hay duda sobre si /pricing y /en/pricing son la misma página.
Lo que importa para hreflang de cualquier manera es la coherencia. La URL en la anotación, la URL en el canónico y la URL que sirve a 200 sin redirigir deben ser la misma cadena. La única combinación que se rompe de manera confiable es siempre prefijar con hreflang todavía apuntando a las URL sin prefijo: cada entrada para la configuración regional predeterminada nombra una redirección y la etiqueta de retorno vive en el destino en lugar de en la URL que nombró.
¿Qué biblioteca deberías utilizar?
La mayoría de las configuraciones de App Router i18n terminan activadas next-intl investigaciones operacionales next-i18next, y ninguno genera hreflang para ti. Resuelven el enrutamiento y la carga de mensajes; las anotaciones siguen siendo tuyas para emitir. Está bien, porque el ayudante de arriba tiene veinte líneas y lo quieres bajo tu propio control de todos modos.
| Preocupación | Manejado por la biblioteca | Tuyo |
|---|---|---|
| Enrutamiento local y middleware | sí | |
| Catálogos de mensajes y alternativas | sí | |
<html lang> |
por lo general | Verifique que coincida con el valor de hreflang |
alternates.canonical por localidad |
prohibido | Construya desde la configuración regional activa |
alternates.languages |
prohibido | Construya desde el mapa local |
x-default |
prohibido | Agregar en el ayudante |
Lo único específico de la biblioteca que hay que comprobar es <html lang>. Los datos estructurados y el hreflang que contradicen el lenguaje del documento son peores que ninguno, y un diseño que codifica lang="en" mientras se sirve alemán es un sobrante sorprendentemente común.
¿Cómo lo verificaciones?
Construya y lea el resultado real, porque la plantilla no es la evidencia:
next build && next start
curl -s http://localhost:3000/de/preise | grep -E 'rel="(canonical|alternate)"'
Tres comprobaciones de ese resultado. La canónica es la URL que obtuvo. Hay exactamente una entrada por configuración regional más una x-default. Fiecare href es absoluto y coincide con la URL en la que se envía esa página, incluida la barra diagonal final.
Luego busca un hermano y difícalo languages bloques. Deberían ser idénticos en bytes; sólo lo canónico difiere. Si no son idénticos, algo en su página está construyendo el conjunto a partir de la configuración regional actual en lugar de del grupo, que es el fracaso de reciprocidad en su disfraz más común.
Finalmente, pegue el conjunto en el generador de etiquetas Hreflang para validar los códigos ellos mismos. Comprueba cada valor con la forma ISO, marca subetiquetas no reconocidas, detecta duplicados y advierte cuando un conjunto no tiene respaldo. Se ejecuta en el lado del cliente, por lo que una estructura de URL de preparación permanece privada. El guía de herramientas camina a través del flujo de trabajo y 12 errores comunes de hreflang cubre qué buscar una vez que el marcado esté activo.
Preguntas frecuentes
¿cómo agrego etiquetas hreflang en el enrutador de aplicaciones Next.js?
Devuelve un alternates.languages objeto de generateMetadata, asignando cada etiqueta BCP 47 a su URL absoluta y establezca alternates.canonical desde la configuración regional activa. Next.js representa ambos en la cabeza.
¿por qué el canónico debe construirse dentro de generateMetadata?
Porque tiene que variar según la configuración regional. Un módulo-alcance canónico no puede, por lo que cada página traducida declararía la URL de la ubicación predeterminada como canónica, lo que elimina la traducción del índice.
¿next-intl genera etiquetas hreflang automáticamente?
No. next-intl maneja el enrutamiento y la carga de mensajes. Las anotaciones hreflang y el canónico por localidad siguen siendo suyos para emitir generateMetadata.
¿cómo agrego x-default en Next.js?
Agregue un 'x-default' clave para el alternates.languages objeto que apunta a su URL alternativa. Next.js pasa la clave sin un manejo especial. Agréguelo dentro del ayudante que construye el conjunto para que ninguna plantilla pueda olvidarlo.
¿debería hreflang ir a la cabeza o al mapa del sitio en Next.js?
Cualquiera funciona, pero no ambos, para las mismas URL. Etiquetas de encabezado vía generateMetadata son más fáciles de depurar. Una ruta de mapa del sitio se adapta a catálogos grandes y mantiene los encabezados de las páginas ajustados; recuerda el xmlns:xhtml declaración en <urlset>.
¿qué pasa si un lugar se traduce sólo parcialmente?
Anuncie sólo los lugares cuyo contenido realmente exista para esa página y marque las páginas no traducidas noindex, follow. Al anunciar la lista local configurada, se publica un clúster que apunta a páginas no traducidas.
¿las URL de hreflang en Next.js deben ser absolutas?
Sí. Hreflang requiere URL completamente calificadas. Cualquiera de los dos establecidos metadataBase o cree las URL desde un origen configurado para que las implementaciones de vista previa no emitan URL de producción.
¿cómo verifico la salida de hreflang renderizada?
Ejecute una compilación de producción, busque dos páginas de hermanos y compárelas. El languages los bloques deben tener bytes idénticos en todo el grupo, mientras que cada canónico apunta a su propia URL.



