wandres.dev
I18N · internacionalización

SEO multiidioma: hreflang, sitemaps por locale y canonical

Decirle a los buscadores que tus páginas son la misma en distintos idiomas para que sirvan la versión correcta a cada usuario. Las etiquetas hreflang recíprocas con x-default derivadas de getAbsoluteLocaleUrlList, la opción i18n del sitemap que declara las alternativas por idioma, la canónica que se refiere a sí misma en cada locale, y el catálogo de errores clásicos que sabotean el posicionamiento multilingüe.

⏱ 17 min

Has traducido el sitio, has organizado el contenido, has negociado el idioma. Falta un interlocutor al que aún no le has explicado nada: el buscador. Google ve tres URLs —/blog/, /en/blog/, /fr/blog/— y por sí solo no sabe que son la misma página en tres lenguas; puede tomarlas por contenido duplicado, elegir una al azar para todos, o servir la francesa a un anglófono. El SEO multiidioma es el conjunto de señales que deshacen esa ambigüedad: las etiquetas hreflang que declaran las equivalencias, el sitemap que las recopila, la canónica que fija la identidad de cada versión. Bien puestas, cada usuario recibe su idioma en los resultados; mal puestas, saboteas calladamente todo el trabajo anterior.

🎯 Al terminar esta lección sabrás
  • Emitir etiquetas hreflang recíprocas con x-default derivadas de getAbsoluteLocaleUrlList.
  • Declarar las alternativas por idioma en el sitemap con la opción i18n de la integración.
  • Fijar una canónica que se refiera a sí misma en cada locale.
  • Reconocer y evitar los errores clásicos del posicionamiento multilingüe.

hreflang: declarar las equivalencias entre idiomas

La señal central del SEO multilingüe es el conjunto de etiquetas hreflang. Cada versión de una página incluye, en su head, un enlace por cada idioma en que existe, indicando qué URL corresponde a cada lengua. Con ellas, el buscador entiende que las tres URLs son alternativas de un mismo contenido y sirve a cada usuario la que casa con su idioma y región. La pieza que no debe faltar es x-default: la versión a la que recurrir cuando ninguna encaja con el visitante.

---
import { getAbsoluteLocaleUrlList } from 'astro:i18n';

const ruta = Astro.url.pathname.replace(/^\/(en|fr)/, '') || '/';
const alternativas = getAbsoluteLocaleUrlList(ruta);
// las mismas URLs absolutas de esta pagina en cada idioma
---

El material sale de la lección de helpers: getAbsoluteLocaleUrlList devuelve, para una ruta, sus URLs absolutas en todos los idiomas, que es justo el conjunto que las etiquetas hreflang necesitan. Emitirlas es recorrer esa lista, y añadir la x-default apuntando a la versión por defecto. Como todo se deriva de locales, el bloque crece solo cuando añades un idioma: nunca queda a mano una lista de alternativas que se desincronice de los idiomas reales.

{alternativas.map(({ code, url }) => (
  <link rel="alternate" hreflang={code} href={url} />
))}
<link rel="alternate" hreflang="x-default" href={porDefecto} />

La regla de oro del hreflang es la reciprocidad: si la página española apunta a la inglesa como su alternativa, la inglesa tiene que apuntar de vuelta a la española. Un hreflang que no es correspondido, los buscadores lo ignoran. Por eso conviene generarlo desde un componente compartido que reciba la ruta y pinte el bloque completo en todas las versiones a la vez, garantizando por construcción que el conjunto sea simétrico.

El sitemap que habla de idiomas

Las etiquetas hreflang en el head son una vía; el sitemap es la otra, y las dos se complementan. La integración @astrojs/sitemap acepta una opción i18n que, a partir de tus idiomas, añade a cada entrada del mapa las alternativas por idioma en el formato que el estándar de sitemaps define. Así el buscador descubre las equivalencias sin depender solo de rastrear el head de cada página.

// astro.config.mjs
import sitemap from '@astrojs/sitemap';

export default defineConfig({
  site: 'https://misitio.com',
  integrations: [
    sitemap({
      i18n: {
        defaultLocale: 'es',
        locales: {
          es: 'es-ES',
          en: 'en-US',
          fr: 'fr-FR',
        },
      },
    }),
  ],
});

El mapa entre código interno y etiqueta de idioma es lo que traduce tu es en el es-ES que los buscadores esperan: un código de idioma con región, más preciso. La integración usa ese mapa para rellenar el campo de enlaces alternativos de cada URL, de modo que el sitemap no solo enumera páginas sino que declara qué traducciones son equivalentes. Es, de nuevo, un artefacto derivado: nace de tu configuración de idiomas, no de una tabla que mantengas aparte, y por eso no puede contradecir a las etiquetas hreflang que emites en el head.

Hay un detalle operativo que agradece atención: la integración solo puede declarar como alternativas las páginas que conoce en el build. Igual que ocurría con el sitemap general, las rutas dinámicas de SSR que nacen en tiempo de petición quedan fuera salvo que las enumeres, y con ellas se quedan fuera sus equivalencias por idioma. Para un blog o una documentación estáticos esto no supone problema; para un catálogo servido bajo demanda, recuerda que las alternativas multilingües de esas URLs tendrás que declararlas por otra vía.

🔀

hreflang reciproco

Cada version enlaza a todas las demas y ellas de vuelta. Sin reciprocidad, se ignora.

🌐

x-default

La version de reserva cuando ningun idioma encaja con el visitante. Nunca debe faltar.

🗺️

sitemap i18n

La opcion i18n de la integracion declara las alternativas por idioma en el propio mapa.

📌

canonical propia

Cada version se declara canonica de si misma, no de la version por defecto.

canonical y los errores clásicos

La etiqueta canónica declara cuál es la dirección oficial de un contenido, y en un sitio multilingüe su regla es contraintuitiva pero firme: cada versión es canónica de sí misma. La francesa se declara canónica en /fr/blog/, la inglesa en /en/blog/. El error más dañino del SEO multiidioma es apuntar la canónica de todas las versiones a la del idioma por defecto: eso le dice al buscador que solo la española es la real y que las demás son duplicados prescindibles, y borra de un plumazo el posicionamiento de cada traducción.

---
import { getAbsoluteLocaleUrl } from 'astro:i18n';
const canon = getAbsoluteLocaleUrl(Astro.currentLocale, ruta);
---
<link rel="canonical" href={canon} />

Con la canónica bien puesta, la relación queda limpia: hreflang dice estas son alternativas equivalentes, y canonical dice y cada una es la oficial de su idioma. Las dos señales colaboran en vez de contradecirse. A partir de aquí, conviene tener presente el catálogo de errores que hunden un sitio multilingüe por lo demás correcto, porque casi todos son silenciosos: no dan un fallo, solo un mal posicionamiento que tarda semanas en notarse.

flowchart TD
PAGE[una version de la pagina] --> HREF[hreflang lista las alternativas]
PAGE --> CAN[canonical apunta a si misma]
HREF --> RECIP[cada idioma enlaza de vuelta]
HREF --> XDEF[x-default para lo no cubierto]
CAN --> SELF[nunca a la version por defecto]
RECIP --> SERP[el buscador sirve el idioma correcto]
XDEF --> SERP
SELF --> SERP
style PAGE fill:#89b4fa,color:#11111b
style HREF fill:#f9e2af,color:#11111b
style SERP fill:#a6e3a1,color:#11111b
⚠️
Los cuatro sabotajes silenciosos del hreflang

Cuatro errores repiten los sitios multilingües, y ninguno da un fallo visible. Primero, hreflang no recíproco: una versión enlaza a otra que no le devuelve el enlace, y el buscador descarta el conjunto. Segundo, URLs relativas en hreflang: el estándar exige absolutas, así que usa getAbsoluteLocaleUrlList, no la variante relativa. Tercero, códigos mal formados: en_US con guion bajo en vez de en-US, o una región inventada. Cuarto, canónica cruzada: apuntar todas las versiones al idioma por defecto. Revisa estos cuatro puntos antes de dar por bueno el SEO, porque su castigo llega tarde y sin avisar.

ℹ️
hreflang en el head y en el sitemap no se estorban

Puedes declarar las alternativas por idioma en las dos vías a la vez —etiquetas en el head y campo de enlaces en el sitemap— y es incluso recomendable, porque distintos buscadores privilegian distintas fuentes. Lo que no debes es dejar que se contradigan: como ambas se derivan de tu misma configuración de locales, generándolas con los helpers y la opción i18n del sitemap en lugar de a mano, la coherencia entre las dos queda garantizada por construcción.

💡
Fija el atributo lang del html en cada página

Antes que cualquier hreflang, la señal más elemental y más olvidada es el atributo lang del elemento raíz del documento. Deriva su valor de Astro.currentLocale en tu layout base, con una reserva por si es undefined, y tendrás en toda página una declaración honesta del idioma de su contenido. Es lo primero que leen buscadores y lectores de pantalla, y lo más barato de acertar: una sola línea en el layout cubre el sitio entero.

📝
hreflang usa el código de idioma, no el segmento de la URL

Un matiz que confunde: el valor de un atributo hreflang es un código de idioma —es, en-US, fr— no el segmento que aparece en tu URL. Si usas locales con path personalizado, donde la carpeta se llama espanol pero el código es es, el hreflang debe decir es, jamás espanol. Como getAbsoluteLocaleUrlList te entrega el code junto a la url, tienes ambos a mano y solo debes cuidar de poner cada uno en su sitio: el código en el atributo, la URL en el href.

El SEO multiidioma es un problema de identidad y equivalencia, no de etiquetas

La tentación al llegar aquí es leer este capítulo como una lista de conjuros —pon esta etiqueta, aquel campo— y memorizarlos sin ver el problema que resuelven. Pero debajo de hreflang, canonical y el sitemap hay una sola cuestión conceptual, la misma que atraviesa todo el nivel: cómo se declara que varios recursos distintos son representaciones equivalentes de una misma cosa sin ser el mismo recurso. Un buscador se enfrenta a un problema de identidad genuinamente difícil: ante tres URLs con texto parecido, debe decidir si son duplicados a colapsar en uno, versiones regionales a servir selectivamente, o páginas independientes a indexar por separado. No puede adivinarlo del contenido, porque las tres dicen lo mismo en distintas lenguas. Las señales de este capítulo son, en el fondo, un pequeño lenguaje formal para responderle esa pregunta con precisión: canonical afirma la identidad —esta URL es ella misma, no un eco de otra—; hreflang afirma la equivalencia —y es intercambiable con estas otras según el idioma del lector—; y la reciprocidad es la condición que vuelve creíble la afirmación, porque una equivalencia que solo un lado declara es una hipótesis, no un hecho. Cuando entiendes que estás hablando un lenguaje de identidad y equivalencia, y no rellenando huecos, dejas de cometer los errores clásicos casi solo: la canónica cruzada se revela como una contradicción —afirmar que algo es a la vez él mismo y otro— y el hreflang no recíproco, como una equivalencia a medio afirmar que nadie debería creer. Esta es la forma más alta de dominar una técnica: no recordar sus reglas, sino comprender el problema tan bien que las reglas se vuelven las únicas respuestas posibles. El SEO multilingüe deja entonces de ser una checklist y pasa a ser lo que siempre fue: la tarea de decirle a una máquina, con el rigor que exige, qué es la misma cosa y qué es solo su traducción.

⚔️ Hazte legible para los buscadores en cada idioma
  1. Emite un bloque hreflang recíproco con getAbsoluteLocaleUrlList, incluyendo una etiqueta x-default.
  2. Añade @astrojs/sitemap con la opción i18n y comprueba que el mapa incluye las alternativas por idioma.
  3. Pon una canónica que se refiera a sí misma en cada versión con getAbsoluteLocaleUrl y Astro.currentLocale.
  4. Audita tu sitio contra los cuatro errores clásicos: reciprocidad, URLs absolutas, códigos bien formados y canónica no cruzada.