Conhecimento

Usando hreflang para Encontrar Cada Versão de Idioma e País de uma Página

O hreflang lista cada versão localizada de uma página. Verificamos como os principais sites o utilizam, o que quebra e por que algumas versões só abrem de dentro de um país.

James Meadow

James Meadow

1 de outubro de 2026 · 11 min de leitura

Se você compara preços, conteúdo ou disponibilidade entre mercados, o primeiro problema é encontrar a mesma página em cada mercado. Adivinhar padrões de URL funciona em alguns sites e falha na maioria: um site usa /de/, outro de.example.com, outro um parâmetro de consulta, e um quarto um domínio separado por país.

Muitos sites já publicam a resposta. A anotação hreflang, que os sites adicionam para mecanismos de busca, lista cada versão de idioma e país de uma página e onde ela está. Leia-a, e uma única busca te dá o conjunto inteiro. Este guia explica como funciona, o que encontramos ao verificar como os principais sites usam isso, e por que ter a URL certa nem sempre é suficiente para ver a página certa.

Principais conclusões

  • Em 1º de outubro de 2026, 82 das 253 homepages de sites principais que buscamos (32%) declararam alternativas hreflang, com uma mediana de 17,5 versões cada e até 157.
  • Uma única busca de uma página anotada te dá cada URL localizada para aquela página, sem adivinhar padrões de URL.
  • As anotações nem sempre são limpas: 12 dos 82 sites usaram pelo menos um código que o Google não suporta, como en-uk, es-419, en_GB com um underscore, ou o país antes do idioma.
  • 86% das alternativas que verificamos apontaram diretamente de volta, como o Google exige. A maioria do restante apontava para uma URL diferente para a mesma página, ou redirecionava.
  • Algumas versões abrem apenas do lugar certo. A homepage dos EUA de um site de notícias e a vitrine alemã de um marketplace redirecionaram nossa conexão da Romênia para outro lugar, e carregaram normalmente através de saídas nos Estados Unidos e na Alemanha.

Como funciona o hreflang

Uma página lista suas alternativas em seu <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 é um código de idioma, opcionalmente seguido de um script ou uma região: de para alemão em qualquer lugar, en-GB para inglês no Reino Unido, zh-Hant para chinês tradicional. x-default nomeia o padrão de reserva para visitantes que não correspondem a nenhum deles. A documentação do Google define as regras que a maioria dos sites segue:

  • Idiomas usam códigos ISO 639-1, scripts ISO 15924 e regiões ISO 3166-1 alpha-2. Uma região sozinha não é válida, e códigos fora desses padrões, como es-419 para espanhol latino-americano, não são suportados.
  • As URLs devem ser totalmente qualificadas, incluindo https://.
  • Cada versão deve listar a si mesma e todas as outras versões, e se duas páginas não apontarem uma para a outra, as anotações podem ser ignoradas.
  • A mesma informação pode ser fornecida no HTML, em um cabeçalho HTTP Link (útil para PDFs) ou em um sitemap.

Para coleta de dados, a terceira regra é a útil. Como cada versão deve listar todas as outras, qualquer uma delas é um mapa completo do conjunto.

O que encontramos nos principais sites

Buscamos as homepages dos 500 principais domínios no ranking Tranco de sites populares. 253 sites distintos retornaram uma homepage HTML; o restante eram redes de conteúdo, hosts de API e outros domínios sem uma, ou duplicatas de um site já contado.

MedidaResultado
Homepages com anotações hreflang82 de 253 (32%)
Alternativas por página anotadamediana 17,5, máximo 157
Páginas com um x-default63 de 82
Homepages enviando hreflang em um cabeçalho HTTP Link0
Sites com pelo menos um código não suportado12 de 82
Sites usando URLs relativas2
Sites listando o mesmo código duas vezes6

Os códigos não suportados se enquadraram em alguns padrões:

PadrãoExemploSites
País antes do idioma, mais rótulos regionaismx-es, cz-cs, emea_africa-en1 (48 códigos)
Underscore em vez de hífenen_GB, de_DE1 (25 códigos)
Código de região que não é ISO 3166-1en-uk, en-eu, sq-xk3
Espanhol latino-americanoes-4192
Código de idioma que não é ISO 639-1ceb, skr, o obsoleto iw para hebraico3
Códigos inventadostc, zh-FT, ms-en3

Alguns sites aparecem em mais de uma linha. O padrão país-primeiro é o mais traiçoeiro para um analisador, porque vários desses códigos são acidentalmente válidos ao contrário: ca é o código para catalão, id para indonésio e th para tailandês, então ca-en é lido como catalão em vez de inglês para o Canadá. Nunca deduza um mercado apenas a partir do código sem verificar se faz sentido.

Pegamos até duas alternativas de cada homepage anotada, 155 no total, buscamos cada uma e verificamos se cada uma listava a homepage de volta.

ResultadoAlternativas
Apontou diretamente de volta133 (86%)
Apontou de volta para uma URL diferente para a mesma página9
Redirecionou para outro lugar9
Genuinamente não listou a página4, em dois sites

A segunda linha é a mais sutil. Em vários sites, a homepage onde chegamos vivia em uma URL enquanto o conjunto nomeava outra: / contra /en/, /homepage ou /home.html, ou uma URL simples contra uma com um parâmetro de idioma. As anotações eram consistentes; nosso ponto de entrada simplesmente não era a URL que o site considera canônica. Ao coletar, chave cada versão pela URL que o próprio conjunto usa, não pela qual você acabou chegando.

A URL certa nem sempre é suficiente

Os redirecionamentos foram a parte mais instrutiva. Buscamos cada alternativa que redirecionava novamente, diretamente e através de saídas Shifter no país alvo, com cabeçalhos Accept-Language em inglês e locais:

Tipo de siteAlternativaDa RomêniaDo país alvoDecidido por
Site de notíciasHomepage dos EUARedirecionou para a edição internacionalCarregou, via uma saída dos EUALocalização
MarketplaceVitrine alemãRedirecionou para o site globalCarregou, via uma saída alemãLocalização
Fornecedor de videochamadasHomepage japonesaCarregou apenas com Accept-Language japonêsO mesmo, via uma saída japonesaIdioma
Editora de jogosHomepage árabeRedirecionouRedirecionou, via uma saída sauditaNenhum: listado mas inacessível

Dois sites decidiram pela localização do visitante, o marketplace independentemente do idioma, então suas próprias anotações apontavam para páginas que um visitante de outro lugar não poderia abrir. Um decidiu pelo idioma, independentemente da localização. E um listou uma versão que redirecionava em toda combinação que tentamos, o que vale a pena saber antes de construir uma comparação de mercado sobre ela.

É aqui que hreflang e localização de saída se encontram. As anotações dizem quais versões existem e onde; coletá-las fielmente significa solicitar cada uma da forma como um visitante local faria, daquele país e com aquele idioma, como abordado em combinando geo, fuso horário e localidade do proxy. Caso contrário, você corre o risco de registrar a versão internacional sob um rótulo alemão.

O código

O módulo abaixo lê as alternativas HTML de uma página, sinaliza códigos não suportados e URLs relativas, e verifica links de retorno, relatando por que cada alternativa com falha falha:

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

Executado contra nossa própria homepage:

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: {}

Dois limites valem a pena conhecer. O código lê apenas anotações no HTML; sites que publicam em um cabeçalho HTTP ou um sitemap precisam que aquilo também seja lido, e sitemaps como fonte de descoberta cobre o lado do sitemap. E check_return_links busca cada alternativa que recebe, então em uma página com 150 versões, amostre algumas ou ritme as solicitações.

Colocando em prática

  • Comece de uma página por modelo. Um produto, uma categoria e uma página de artigo geralmente compartilham um padrão de anotação, então uma busca de cada uma mostra como o site inteiro mapeia seus mercados.
  • Chave as versões pelas próprias URLs do conjunto. Armazene a URL que cada alternativa declara, e trate redirecionamentos como descobertas em vez de ruído.
  • Busque cada versão como um visitante local. Use uma saída no mercado e o idioma daquele mercado, então verifique que você não foi redirecionado. A localização decidiu dois de nossos quatro casos de redirecionamento, como acontece em quais países sofrem mais bloqueio geográfico.
  • Valide os códigos antes de confiar neles. Um código invertido ou inventado pode atribuir silenciosamente uma página ao mercado errado.
  • Normalize o que você coleta. Uma vez que você tenha cada versão, preços, datas e números ainda diferem em formato, como abordado em normalizando preços, números e datas entre localidades, e a própria comparação é o tema de detectando precificação geo-personalizada.

Conclusão

O hreflang é a coisa mais próxima que a web tem de um índice publicado dos mercados de uma página. Um terço dos principais sites o fornece, uma única busca revela o conjunto inteiro, e a maior parte do conjunto se conecta entre si como deveria.

É um mapa, não uma garantia. Os códigos podem estar errados, a URL onde você chega pode não ser aquela que o conjunto usa, e algumas versões abrem apenas para visitantes no lugar certo ou com o idioma certo. Leia as anotações, valide-as e busque cada versão da forma como um visitante local faria.

Fontes e referências

  • Google Search Central, Tell Google about localized versions of your page.
  • Tranco, list Q2K34, agregando rankings de domínios de 1 a 30 de setembro de 2026.
  • Babel, cujos dados CLDR fornecem as listas de idioma, script e região no código.
  • Homepages e alternativas buscadas pela Shifter em 1º de outubro de 2026, diretamente e através de saídas Shifter, usando o código acima.

Pronto para começar?

Experimente os proxies residenciais da Shifter, mais de 205M IPs, mais de 195 países, a partir de $ 0,75/GB.

Começar