Conocimiento

Usar hreflang para encontrar todas las versiones de idioma y país de una página

hreflang enumera todas las versiones localizadas de una página. Comprobamos cómo lo usan los principales sitios, qué falla y por qué algunas versiones solo se abren desde dentro de un país.

James Meadow

James Meadow

1 de octubre de 2026 · 12 min de lectura

Si comparas precios, contenidos o disponibilidad entre mercados, el primer problema es encontrar la misma página en cada mercado. Adivinar patrones de URL funciona en algunos sitios y falla en la mayoría: un sitio usa /de/, otro de.example.com, otro un parámetro de consulta, y un cuarto un dominio independiente por país.

Muchos sitios ya publican la respuesta. La anotación hreflang, que los sitios añaden para los motores de búsqueda, enumera cada versión de idioma y país de una página y dónde se encuentra. Léela, y una sola petición te da el conjunto completo. Esta guía explica cómo funciona, qué encontramos al comprobar cómo la usan los principales sitios, y por qué tener la URL correcta no siempre basta para ver la página correcta.

Conclusiones clave

  • El 1 de octubre de 2026, 82 de las 253 páginas de inicio de los principales sitios que obtuvimos (32%) declaraban alternativas hreflang, con una mediana de 17,5 versiones cada una y hasta 157.
  • Una sola petición a una página anotada te da cada URL localizada de esa página, sin necesidad de adivinar patrones de URL.
  • Las anotaciones no siempre son limpias: 12 de los 82 sitios usaban al menos un código que Google no admite, como en-uk, es-419, en_GB con guion bajo, o el país antes del idioma.
  • El 86% de las alternativas que comprobamos enlazaban directamente de vuelta, como exige Google. La mayoría del resto apuntaba a una URL diferente para la misma página, o redirigía.
  • Algunas versiones solo se abren desde el lugar correcto. La página de inicio de EE. UU. de un sitio de noticias y el escaparate alemán de un marketplace redirigieron nuestra conexión desde Rumanía a otro lugar, y cargaron con normalidad a través de salidas en Estados Unidos y Alemania.

Cómo funciona hreflang

Una página enumera sus alternativas en su <head>:

<link rel="alternate" hreflang="en" href="https://example.com/" />
<link rel="alternate" hreflang="de" href="https://example.com/de/" />
<link rel="alternate" hreflang="pt-BR" href="https://example.com/pt/" />
<link rel="alternate" hreflang="x-default" href="https://example.com/" />

Cada valor es un código de idioma, seguido opcionalmente de un sistema de escritura o una región: de para alemán en cualquier lugar, en-GB para inglés en el Reino Unido, zh-Hant para chino tradicional. x-default designa la versión de respaldo para los visitantes que no coinciden con ninguna de ellas. La documentación de Google establece las reglas que sigue la mayoría de los sitios:

  • Los idiomas usan códigos ISO 639-1, los sistemas de escritura ISO 15924 y las regiones ISO 3166-1 alpha-2. Una región por sí sola no es válida, y los códigos fuera de esos estándares, como es-419 para el español latinoamericano, no son compatibles.
  • Las URL deben estar completamente cualificadas, incluyendo https://.
  • Cada versión debe enumerarse a sí misma y a todas las demás versiones, y si dos páginas no se apuntan mutuamente, las anotaciones pueden ignorarse.
  • La misma información se puede indicar en el HTML, en una cabecera HTTP Link (útil para PDF) o en un sitemap.

Para la recopilación de datos, la tercera regla es la útil. Como se supone que cada versión enumera todas las demás, cualquiera de ellas es un mapa completo del conjunto.

Qué encontramos en los principales sitios

Obtuvimos las páginas de inicio de los 500 dominios principales en el ranking Tranco de sitios populares. 253 sitios distintos devolvieron una página de inicio en HTML; el resto eran redes de contenido, hosts de API y otros dominios sin página de inicio, o duplicados de un sitio ya contado.

MedidaResultado
Páginas de inicio con anotaciones hreflang82 de 253 (32%)
Alternativas por página anotadamediana 17,5, máximo 157
Páginas con un x-default63 de 82
Páginas de inicio que envían hreflang en una cabecera HTTP Link0
Sitios con al menos un código no admitido12 de 82
Sitios que usan URL relativas2
Sitios que enumeran el mismo código dos veces6

Los códigos no admitidos seguían algunos patrones:

PatrónEjemploSitios
País antes del idioma, más etiquetas regionalesmx-es, cz-cs, emea_africa-en1 (48 códigos)
Guion bajo en lugar de guionen_GB, de_DE1 (25 códigos)
Código de región que no es ISO 3166-1en-uk, en-eu, sq-xk3
Español latinoamericanoes-4192
Código de idioma que no es ISO 639-1ceb, skr, el obsoleto iw para hebreo3
Códigos inventadostc, zh-FT, ms-en3

Algunos sitios aparecen en más de una fila. El patrón de país primero es el más traicionero para un analizador, porque varios de esos códigos son válidos accidentalmente al revés: ca es el código del catalán, id del indonesio y th del tailandés, así que ca-en se interpreta como catalán en lugar de inglés para Canadá. Nunca deduzcas un mercado a partir solo del código sin comprobar que tiene sentido.

Enlaces de retorno

Tomamos hasta dos alternativas de cada página de inicio anotada, 155 en total, las obtuvimos y comprobamos si cada una enumeraba la página de inicio de vuelta.

ResultadoAlternativas
Enlazaba directamente de vuelta133 (86%)
Enlazaba de vuelta a una URL diferente para la misma página9
Redirigía a otro sitio9
Realmente no enumeraba la página4, en dos sitios

La segunda fila es la sutil. En varios sitios, la página de inicio a la que llegamos residía en una URL mientras que el conjunto nombraba otra: / frente a /en/, /homepage o /home.html, o una URL sin parámetros frente a una con un parámetro de idioma. Las anotaciones eran coherentes; nuestro punto de entrada simplemente no era la URL que el sitio considera canónica. Cuando recopiles, clasifica cada versión por la URL que usa el propio conjunto, no por la que casualmente visitaste.

La URL correcta no siempre basta

Las redirecciones fueron la parte más instructiva. Volvimos a obtener cada alternativa que redirigía, directamente y a través de salidas de Shifter en el país de destino, con cabeceras Accept-Language en inglés y locales:

Tipo de sitioAlternativaDesde RumaníaDesde el país de destinoDecidido por
Sitio de noticiasPágina de inicio de EE. UU.Redirigió a la edición internacionalCargó, vía una salida de EE. UU.Ubicación
MarketplaceEscaparate alemánRedirigió al sitio globalCargó, vía una salida alemanaUbicación
Proveedor de videollamadasPágina de inicio en japonésCargó solo con Accept-Language en japonésIgual, vía una salida japonesaIdioma
Editora de videojuegosPágina de inicio en árabeRedirigióRedirigió, vía una salida saudíNinguno: listado pero inalcanzable

Dos sitios decidían según la ubicación del visitante, el marketplace sin importar el idioma, de modo que sus propias anotaciones apuntaban a páginas que un visitante de otro lugar no podía abrir. Uno decidía según el idioma, sin importar la ubicación. Y uno enumeraba una versión que redirigía en todas las combinaciones que probamos, algo que conviene saber antes de construir una comparación de mercados sobre ella.

Aquí es donde se encuentran hreflang y la ubicación de salida. Las anotaciones te dicen qué versiones existen y dónde; recopilarlas fielmente implica solicitar cada una como lo haría un visitante local, desde ese país y con ese idioma, como se trata en cómo hacer coincidir la geolocalización, la zona horaria y el idioma del proxy. De lo contrario, corres el riesgo de registrar la versión internacional bajo una etiqueta alemana.

El código

El siguiente módulo lee las alternativas HTML de una página, señala códigos no admitidos y URL relativas, y comprueba los enlaces de retorno, informando por qué falla cada alternativa fallida:

from html.parser import HTMLParser
from urllib.parse import urljoin, urlsplit, urlunsplit

import requests
from babel import Locale

# Google accepts ISO 639-1 languages, ISO 15924 scripts and ISO 3166-1 alpha-2 regions.
LANGUAGES = {code for code in Locale("en").languages if len(code) == 2}
NOT_ISO_3166 = {"AC", "CP", "CQ", "DG", "EA", "EU", "EZ", "IC", "QO", "TA", "UN", "XA", "XB", "XK", "ZZ"}
REGIONS = {code for code in Locale("en").territories if code.isalpha()} - NOT_ISO_3166
SCRIPTS = set(Locale("en").scripts)


class AlternateLinks(HTMLParser):
    """Collect <link rel="alternate" hreflang="..."> elements from a page's markup."""

    def __init__(self):
        super().__init__()
        self.links = []

    def handle_starttag(self, tag, attrs):
        a = dict(attrs)
        if tag == "link" and "alternate" in (a.get("rel") or "").lower().split() and a.get("hreflang") and a.get("href"):
            self.links.append((a["hreflang"].strip(), a["href"].strip()))


def valid_hreflang(code):
    """True for x-default, a language, or language-region / language-script(-region) codes."""
    if code.lower() == "x-default":
        return True
    parts = code.split("-")
    if parts[0].lower() not in LANGUAGES:
        return False
    rest = parts[1:]
    if rest and rest[0].title() in SCRIPTS:
        rest = rest[1:]
    if rest and rest[0].upper() in REGIONS:
        rest = rest[1:]
    return not rest


def hreflang_map(url, session=None, timeout=20):
    """Fetch a page and return its declared alternates as {hreflang: absolute URL}, plus any problems found."""
    session = session or requests.Session()
    response = session.get(url, timeout=timeout)
    parser = AlternateLinks()
    parser.feed(response.text)  # link tags are normally ASCII, so the decoding choice rarely matters here
    alternates, problems = {}, []
    for code, href in parser.links:
        if not href.startswith(("http://", "https://")):
            problems.append(f"relative URL for {code}: {href}")
        if not valid_hreflang(code):
            problems.append(f"invalid code: {code}")
        alternates[code] = urljoin(response.url, href)
    return response.url, alternates, problems


def normalise(url):
    """Compare URLs without default ports, host case or a trailing slash getting in the way."""
    parts = urlsplit(url)
    port = f":{parts.port}" if parts.port and parts.port not in (80, 443) else ""
    return urlunsplit((parts.scheme.lower(), (parts.hostname or "") + port, parts.path.rstrip("/") or "/", parts.query, ""))


def check_return_links(url, alternates, session=None, timeout=20):
    """Fetch each alternate and report why it breaks the return-link rule. An empty result means all link back."""
    session = session or requests.Session()
    target = normalise(url)
    problems = {}
    for code, alternate in alternates.items():
        if normalise(alternate) == target:
            continue
        final, back, _ = hreflang_map(alternate, session, timeout)
        if normalise(final) != normalise(alternate):
            problems[code] = f"redirects to {final}"
        elif not back:
            problems[code] = "has no hreflang annotations"
        elif target not in {normalise(u) for u in back.values()}:
            problems[code] = "does not link back"
    return problems

Ejecutado contra nuestra propia página de inicio:

import requests

from hreflang import hreflang_map, check_return_links

session = requests.Session()
session.headers["User-Agent"] = "ExampleCollector/1.0 (+https://example.com/bot)"

url, alternates, problems = hreflang_map("https://shifter.io/", session)
for code, alternate in sorted(alternates.items()):
    print(code, alternate)
print("problems:", problems)
print("return-link problems:", check_return_links(url, alternates, session))
de https://shifter.io/de
en https://shifter.io/
es https://shifter.io/es
fr https://shifter.io/fr
ja https://shifter.io/ja
ko https://shifter.io/ko
pt-BR https://shifter.io/pt
x-default https://shifter.io/
zh-Hans https://shifter.io/cn
problems: []
return-link problems: {}

Vale la pena conocer dos límites. El código lee las anotaciones solo en el HTML; los sitios que las publican en una cabecera HTTP o en un sitemap necesitan que estas también se lean, y los sitemaps como fuente de descubrimiento cubre el lado del sitemap. Y check_return_links obtiene cada alternativa que se le proporciona, así que en una página con 150 versiones, conviene muestrear algunas o espaciar las peticiones.

Ponerlo en práctica

  • Empieza por una página por plantilla. Un producto, una categoría y una página de artículo suelen compartir un patrón de anotación, así que una petición a cada una muestra cómo mapea el sitio completo sus mercados.
  • Clasifica las versiones por las URL propias del conjunto. Almacena la URL que declara cada alternativa, y trata las redirecciones como hallazgos en lugar de ruido.
  • Obtén cada versión como un visitante local. Usa una salida en el mercado y el idioma de ese mercado, y luego comprueba que no fuiste redirigido. La ubicación decidió dos de nuestros cuatro casos de redirección, como ocurre en qué países sufren más bloqueo geográfico.
  • Valida los códigos antes de confiar en ellos. Un código invertido o inventado puede asignar silenciosamente una página al mercado equivocado.
  • Normaliza lo que recopilas. Una vez que tienes cada versión, los precios, fechas y números todavía difieren en formato, como se trata en normalizar precios, números y fechas entre locales, y la comparación en sí es el tema de detectar precios geopersonalizados.

Conclusión

hreflang es lo más parecido que tiene la web a un índice publicado de los mercados de una página. Un tercio de los principales sitios lo proporciona, una sola petición revela el conjunto completo, y la mayor parte del conjunto enlaza entre sí como debería.

Es un mapa, no una garantía. Los códigos pueden estar equivocados, la URL a la que llegas puede no ser la que usa el conjunto, y algunas versiones solo se abren para visitantes en el lugar correcto o con el idioma correcto. Lee las anotaciones, valídalas, y obtén cada versión como lo haría un visitante local.

Fuentes y referencias

  • Google Search Central, Informa a Google sobre las versiones localizadas de tu página.
  • Tranco, lista Q2K34, que agrega clasificaciones de dominios del 1 al 30 de septiembre de 2026.
  • Babel, cuyos datos CLDR proporcionan las listas de idiomas, sistemas de escritura y regiones en el código.
  • Páginas de inicio y alternativas obtenidas por Shifter el 1 de octubre de 2026, directamente y a través de salidas de Shifter, usando el código anterior.

¿Listo para empezar?

Prueba los proxies residenciales de Shifter, más de 205M IPs, más de 195 países, desde 0,75 $/GB.

Comenzar