< BACK Mapa de papel gastado de Europa en un escritorio de madera con una taza de espresso, luz editorial nublada, grano de película de 35 mm

Next.js i18n en 2026: enrutamiento, hreflang y SEO

En 2023 lancé un sitio de comercio electrónico multilingüe para un cliente basado en Berlín. Doce idiomas, App Router, todo se veía ordenado. Tres meses después sus páginas de productos en alemán se posicionaban en los SERPs del Reino Unido y sus páginas en inglés se servían a usuarios en Múnich. Las etiquetas hreflang estaban técnicamente presentes. Solo que eran incorrectas de una forma que me tomó una tarde vergonzosa desentrañar. Esa experiencia me enseñó más sobre Next.js i18n que cualquier página de documentación.

Este no es un post de "aquí está la documentación oficial resumida". Es lo que he aprendido construyendo sitios multilingües en Seahawk Media, qué problemas afectan repetidamente a la gente, y las decisiones específicas que realmente afectan los posicionamientos en 2026.

---

El App Router Lo Cambió Todo (Y No Todo para Mejor)

El Pages Router tenía soporte i18n integrado. Dejabas un bloque en next.config.js y Next.js manejaba la detección de locale, el enrutamiento por subruta, y el cambio de locale del componente Link casi automáticamente. No era perfecto, pero era opinado, lo que significaba menos riesgos.next.config.js and Next.js handled locale detection, sub-path routing, and Link component locale switching almost automatically. It wasn't perfect, but it was opinionated, which meant fewer footguns.

El App Router eliminó completamente el enrutamiento i18n nativo. Sin clave i18n en next.config.js. Sin detección automática de idioma. Ahora eres responsable de toda la lógica de enrutamiento, y esa responsabilidad es donde la mayoría de los equipos silenciosamente hacen un desastre.i18n key in next.config.js. No automatic locale detection. You are now responsible for the full routing logic, and that responsibility is where most teams quietly make a mess.

El enfoque recomendado en 2026 es un segmento dinámico [locale] en la raíz de tu directorio app/. Algo como app/[locale]/page.tsx. Combina eso con next-intl o i18next a través de react-i18next, un archivo middleware que lee los encabezados Accept-Language y redirige en consecuencia, y tienes una base funcional.[locale] dynamic segment at the root of your app/ directory. Something like app/[locale]/page.tsx . Pair that with next-intl or i18next via react-i18next, a middleware file that reads Accept-Language headers and redirects accordingly, and you have a working foundation.

Por Qué el Middleware Se Entiende Mal

El archivo middleware está haciendo dos trabajos separados que la gente confunde. Primero, detección de idioma: leer el encabezado Accept-Language (o una cookie, o un prefijo de URL) y decidir qué idioma servir. Segundo, redirigir o reescribir la URL para que el usuario termine en /de/produkte en lugar de /produkte con algún estado de idioma oculto.Accept-Language header (or a cookie, or a URL prefix) and deciding which locale to serve. Second, redirecting or rewriting the URL so the user ends up at /de/produkte rather than /produkte with some hidden locale state.

Ambas cosas deben suceder antes de que la página se renderice. Eso suena obvio. Pero he visto equipos ejecutar la detección de idioma dentro de un proveedor de contexto de React, lo que significa que el HTML inicial servido a Googlebot no tiene ninguna señal de idioma. El bot ve una página, la indexa bajo la URL raíz, y de repente tienes un desastre de canonicalización.before the page renders. That sounds obvious. But I've seen teams run locale detection inside a React context provider, which means the initial HTML served to Googlebot has no locale signal at all. The bot sees a page, it indexes it under the root URL, and suddenly you have a canonical mess.

Mantén la resolución de idioma en middleware. Siempre.

---

Sub-ruta vs Sub-dominio: Elige Uno y Comprométete

El debate eterno. /en/about vs en.yoursite.com/about. Para la mayoría de los proyectos recomiendo enrutamiento de sub-rutas, y aquí está el porqué: concentra tu autoridad de dominio. Cada enlace que apunta a tu .com beneficia todos los idiomas. Con sub-dominios efectivamente estás ejecutando sitios separados a los ojos de Google, lo que significa que necesitas construir autoridad para cada uno independientemente./en/about vs en.yoursite.com/about . For most projects I recommend sub-path routing, and here's why: it concentrates your domain authority. Every link pointing at your .com benefits all locales. With sub-domains you're effectively running separate sites in Google's eyes, which means you need to build authority for each one independently.

Hay una excepción. Si estás en un mercado donde el TLD local importa enormemente para la confianza (.de en Alemania, .fr en Francia), un TLD de código de país supera tanto las sub-rutas como los sub-dominios para la credibilidad local percibida. Eso no es una afirmación de SEO, es una afirmación de conversión. He visto pruebas A/B donde .de vs yourbrand.com/de movieron métricas de confianza significativamente incluso cuando los rankings eran idénticos..de in Germany, .fr in France), a country-code TLD beats both sub-paths and sub-domains for perceived local credibility. That's not an SEO claim, it's a conversion claim. I've seen A/B tests where .de vs yourbrand.com/de moved trust metrics meaningfully even when rankings were identical.

Pero no puedes ejecutar una configuración de ccTLD dentro de una sola aplicación Next.js fácilmente. Entonces, para el 99% de los proyectos: sub-rutas, una aplicación, un dominio.

---

hreflang: La Etiqueta Que Te Miente

Aquí es donde veo el daño más grave. hreflang no es conceptualmente difícil. Le dice a los motores de búsqueda: "esta página tiene equivalentes en otros idiomas, aquí están." En la práctica, la implementación tiene tres modos de falla específicos que sigo encontrando.

Modo de Falla 1: Las Etiquetas No Son Recíprocas

Cada página en tu conjunto hreflang debe hacer referencia a todas las demás páginas, incluida ella misma. Si /en/about tiene un hreflang que apunta a /de/about pero /de/about no tiene un hreflang que apunte de vuelta a /en/about, Google ignora el conjunto completo. No penaliza. Ignora. Como si las etiquetas no existieran./en/about has an hreflang pointing to /de/about but /de/about does not have an hreflang pointing back to /en/about, Google ignores the whole set. Not penalises. Ignores. As if the tags don't exist.

Seahawk tuvo un proyecto de fintech el año pasado donde el 40% de sus etiquetas hreflang no eran recíprocas porque dos desarrolladores las habían implementado independientemente en componentes diferentes sin coordinarse. La corrección tomó una hora. La recuperación de posicionamiento tomó dos meses.

Modo de Falla 2: La Etiqueta x-default Falta o Está Mal Colocada

x-default es para páginas que no apuntan a un idioma o región específicos. Típicamente tu raíz / o una página selectora de idioma. Algunos equipos ponen x-default en su página en inglés, lo cual está bien si el inglés es genuinamente tu fallback para todas las locales sin coincidencia. Pero si sirves una página diferente a usuarios cuyo idioma no soportas, x-default debería apuntar allí. is for pages that don't target a specific language or region. Typically your root / or a language-selector page. Some teams put x-default on their English page, which is fine if English is genuinely your fallback for all unmatched locales. But if you serve a different page to users whose language you don't support, x-default should point there.

He visto x-default omitido completamente. Google no lanza un error. Simplemente toma su propia decisión sobre qué versión mostrar para consultas sin coincidencia, y esa decisión frecuentemente es incorrecta.x-default omitted entirely. Google doesn't throw an error. It just makes its own decision about which version to show for unmatched queries, and that decision is often wrong.

Modo de fallo 3: Barras diagonales finales y conflictos canónicos

Tu etiqueta hreflang dice https://yoursite.com/de/ueber-uns . La etiqueta canónica en la misma página dice https://yoursite.com/de/ueber-uns/ (nota la barra diagonal final). Google trata esos como URLs diferentes. Tu canónica y hreflang ahora apuntan a cosas distintas, y has creado un conflicto de señal que suprime silenciosamente los rankings sin ningún error visible en Search Console.https://yoursite.com/de/ueber-uns . Your canonical tag on the same page says https://yoursite.com/de/ueber-uns/ (note the trailing slash). Google treats those as different URLs. Your canonical and hreflang are now pointing at different things, and you've created a signal conflict that quietly suppresses rankings without any visible error in Search Console.

Establece trailingSlash en next.config.js y sé consistente en todas partes. Verifica. Realmente verifica ejecutando curl en tus propias URLs y leyendo el resultado.trailingSlash in next.config.js and be consistent everywhere. Check it. Actually check it by curling your own URLs and reading the output.

La documentación de Google Search Central sobre hreflang realmente vale la pena leerla completa. Es uno de esos raros documentos oficiales que es lo suficientemente específico para ser útil.Google Search Central documentation on hreflang is genuinely worth reading in full. It's one of those rare official docs that's specific enough to be useful.

---

Generando hreflang a escala en Next.js

Para un sitio de 5 idiomas podrías escribir etiquetas hreflang a mano. Para 12 locales en 3,000 páginas de productos, necesitas un enfoque sistemático.

Esto es lo que uso:

  1. Define tu lista de locales una sola vez, en un único archivo de configuración. Algo como locales.config.ts exportando un array: ['en', 'de', 'fr', 'es', 'nl'...].locales.config.ts exporting an array: ['en', 'de', 'fr', 'es', 'nl'...].
  2. Construye una función utilitaria getHreflangAlternates(pathname: string) que tome una ruta agnóstica de locale (/about ) y devuelva un array de objetos { hreflang, href } para cada locale.getHreflangAlternates(pathname: string) that takes a locale-agnostic path (/about ) and returns an array of { hreflang, href } objects for every locale.
  3. Llama esa función en tu root layout.tsx e inyecta los resultados mediante la API metadata de Next.js usando el campo alternates.languages. Esto se añadió en Next.js 13.3 y genera automáticamente las etiquetas <link rel="alternate" hreflang="..."> correctas en <head>.layout.tsx and inject the results via Next.js's metadata API using the alternates.languages field. This was added in Next.js 13.3 and it generates the correct <link rel="alternate" hreflang="..."> tags in <head> automatically.
  4. Para páginas con contenido dinámico (posts de blog, páginas de productos) donde no todos los idiomas tienen traducción, pasa un array supportedLocales a tu utilidad y genera etiquetas solo para los idiomas donde el contenido realmente existe. Una etiqueta hreflang que apunte a una página que devuelve 404 es peor que no tener etiqueta hreflang.supportedLocales array to your utility and only generate tags for locales where the content actually exists. A hreflang tag pointing to a page that returns 404 is worse than no hreflang tag.

El enfoque alternates.languages en la API metadata es la forma más limpia que he encontrado para manejar esto. Sin manipulación de <Head> personalizada, sin paquetes de terceros para las etiquetas en sí.alternates.languages approach in the metadata API is the cleanest way I've found to handle this. No custom <Head> juggling, no third-party packages for the tags themselves.

---

Detección de Idioma Sin Ser Molesto

Hay una trampa UX aquí que también tiene implicaciones SEO. La redirección automática agresiva basada en el idioma del navegador es mala para ambos.

Si un usuario en Alemania visita yoursite.com/en/article y tu middleware lo redirige forzadamente a /de/artikel sin preguntar, acabas de romper su navegación deseada. También has roto potencialmente el compartir enlaces: alguien comparte la URL en inglés, el destinatario en Francia es redirigido al francés, y el contenido que se suponía que debería ver podría no existir en francés aún.yoursite.com/en/article and your middleware hard-redirects them to /de/artikel without asking, you've just broken their intended navigation. You've also potentially broken link sharing: someone shares the English URL, the recipient in France gets bounced to French, and the content they were meant to see might not exist in French yet.

Mi regla: detecta, sugiere, no fuerces. Usa el header Accept-Language para establecer un idioma sensato por defecto en la primera visita (almacenado en una cookie), pero permite que los usuarios lo sobrescriban manualmente y respeta esa decisión en cada visita posterior. La cookie tiene prioridad sobre el header.detect, suggest, don't force. Use the Accept-Language header to set a sensible default on first visit (stored in a cookie), but let users override it manually and honour that override on every subsequent visit. The cookie takes priority over the header.

Para SEO específicamente: Googlebot no envía headers Accept-Language significativos. Rastreará tus URLs tal cual. Así que asegúrate de que cada URL de idioma sea directamente accesible sin redirección, y que tu sitemap incluya explícitamente todas las variantes de idioma.Accept-Language headers. It will crawl your URLs as-is. So make sure every locale URL is directly accessible without redirection, and that your sitemap includes all locale variants explicitly.

La documentación del middleware next-intl cubre bien esta estrategia de detección, y es donde dirigiría a cualquiera que empiece un proyecto nuevo.next-intl middleware documentation covers this detection strategy well, and it's where I'd send anyone starting a new project.

---

Mapas de sitio para sitios multilingües

Un único sitemap.xml que incluya todas las variantes de locale funciona bien para sitios más pequeños. Para sitios grandes (decenas de miles de URLs en múltiples locales), lo divido: un índice de sitemap en /sitemap.xml, luego archivos de sitemap separados por locale en /sitemaps/en.xml, /sitemaps/de.xml, y así sucesivamente.sitemap.xml that includes every locale variant is fine for smaller sites. For large sites (tens of thousands of URLs across multiple locales) I split it: one sitemap index at /sitemap.xml , then separate sitemap files per locale at /sitemaps/en.xml , /sitemaps/de.xml, and so on.

Cada entrada de URL debe incluir etiquetas <xhtml:link rel="alternate"> que reflejen tu configuración de hreflang. Esto es redundante con las etiquetas en la página, pero ayuda a Googlebot a conectar los puntos en páginas que aún no ha rastreado.<xhtml:link rel="alternate"> tags mirroring your hreflang setup. This is redundant with the in-page tags but it helps Googlebot connect the dots on pages it hasn't crawled yet.

En Next.js 14+ puedes generar sitemaps dinámicamente mediante app/sitemap.ts. El tipo de retorno soporta alternates.languages en la misma forma que la API de metadata. Conéctalo a la misma utilidad getHreflangAlternates que construiste antes y obtienes consistencia de forma gratuita.app/sitemap.ts . The return type supports alternates.languages in the same shape as the metadata API. Wire it up to the same getHreflangAlternates utility you built earlier and you get consistency for free.

Un número concreto: en un proyecto con 8 locales y 4,500 productos, dividir sitemaps redujo el tiempo promedio de rastreo por locale aproximadamente un 30% según las estadísticas de rastreo de GSC. No es espectacular, pero es significativo.

---

Librerías de traducción: lo que realmente uso

Hay tres que vale la pena conocer. next-intl es mi predeterminada para proyectos con App Router. Tiene soporte de primera clase para componentes de servidor, maneja pluralización correctamente, y la API es limpia. La he usado en alrededor de 40 proyectos desde mediados de 2023.next-intl is my default for App Router projects. It has first-class support for server components, handles pluralisation properly, and the API is clean. I've used it on about 40 projects since mid-2023.

react-i18next está probada en batalla y tiene un ecosistema enorme, pero su modelo mental fue construido para componentes de cliente y agregar soporte de componentes de servidor mediante el core de i18next se siente como algo añadido en comparación con el enfoque nativo de next-intl.i18next core feels bolted-on compared to next-intl's native approach.

Lingui vale la pena considerarlo si tu flujo de traducción involucra traductores profesionales en lugar de un CMS. Su herramienta de extracción es superior. Pero para la mayoría de proyectos de agencia donde el contenido viene de Contentful o Sanity, next-intl es más simple.

Lo que evitaría: construir el tuyo propio. Lo hice una vez en 2020 para un proyecto donde el cliente insistía en reutilizar directamente sus archivos de traducción PHP existentes. Funcionó, apenas, y pasé dos años manteniendo algo que next-intl ahora maneja en una única llamada useTranslations().useTranslations() call.

---

FAQ

¿Soporta Next.js App Router el bloque i18n antiguo de `next.config.js`?

No. La clave de configuración i18n era específica del Pages Router. Si estás migrando a App Router, necesitas implementar el enrutamiento a través de un segmento dinámico [locale] y un archivo middleware. El bloque de configuración antiguo se ignora silenciosamente en proyectos de App Router.i18n configuration key was specific to the Pages Router. If you're migrating to App Router, you need to implement routing via a [locale] dynamic segment and a middleware file. The old config block is silently ignored in App Router projects.

¿Debo usar `lang` en la ruta URL o confiar en subdominios?

Para la mayoría de proyectos, `lang` en la ruta URL (enrutamiento por subruta como /de/) es la opción correcta. Los subdominios fragmentan tu autoridad de dominio y requieren configuración DNS separada. Los ccTLDs solo valen la complejidad si las señales de confianza local están moviendo genuinamente las tasas de conversión en tus mercados específicos.lang in the URL path (sub-path routing like /de/) is the right call. Subdomains fragment your domain authority and require separate DNS configuration. ccTLDs are only worth the complexity if local trust signals are genuinely moving conversion rates in your specific markets.

¿Cómo maneja Googlebot el contenido de locales renderizado con JavaScript?

Mal, a veces. Si tu detección de locale se ejecuta puramente del lado del cliente en un contexto React y el HTML inicial no tiene marcador de locale, Googlebot puede indexar el estado previo a la hidratación. Siempre resuelve el locale en middleware y asegúrate de que el atributo `lang` correcto esté en el elemento <html> en el HTML renderizado del servidor. Verifica esto viendo el código fuente, no usando las herramientas del desarrollador del navegador (que muestran el estado post-hidratación).lang attribute is on the <html> element in the server-rendered HTML. Check this by viewing source, not by using browser devtools (which show post-hydration state).

¿Cuál es el valor `x-default` correcto para hreflang en un sitio sin página de selector de idiomas?

Apúntalo a la página de inicio de tu locale principal, típicamente /en/. Le señala a Google que el inglés es tu opción de fallback para usuarios cuyo idioma no soportas. No es una penalización hacerlo, es solo una señal de preferencia suave que Google puede o no considerar./en/. It signals to Google that English is your fallback for users whose language you don't support. It's not a penalty to do this, it's just a soft preference signal Google may or may not honour.

¿Puedo verificar mi implementación de hreflang sin una herramienta SEO de pago?

Sí. La herramienta de Inspección de URLs de Google Search Console te mostrará los tags hreflang detectados en cualquier URL indexada. No validará la reciprocidad automáticamente, pero puedes verificar manualmente algunas páginas representativas. Para validación en masa, Screaming Frog (gratuito hasta 500 URLs) maneja verificaciones de reciprocidad en su pestaña hreflang.Google Search Console's URL Inspection tool will show you detected hreflang tags for any indexed URL. It won't validate reciprocity automatically, but you can manually check a few representative pages. For bulk validation, Screaming Frog (free up to 500 URLs) handles reciprocity checks in its hreflang tab.

---

Hacer Next.js multilingüe correctamente es genuinamente un trabajo poco glamoroso. Son archivos de configuración, consistencia de URLs, y asegurarte de que dos desarrolladores no hayan implementado lo mismo dos veces en componentes diferentes. Pero los sitios donde me he tomado el tiempo de hacerlo bien tienen rankings que se sostienen entre regiones, y los que corté esquinas me han enseñado las lecciones que he escrito aquí.

El cliente de Berlín, por cierto, eventualmente logró que sus páginas en alemán rankeen en Alemania. Nos tomó un ciclo de re-crawl completo y mucho café.

< BACK