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_GBcom 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-419para 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.
| Medida | Resultado |
|---|---|
| Homepages com anotações hreflang | 82 de 253 (32%) |
| Alternativas por página anotada | mediana 17,5, máximo 157 |
Páginas com um x-default | 63 de 82 |
Homepages enviando hreflang em um cabeçalho HTTP Link | 0 |
| Sites com pelo menos um código não suportado | 12 de 82 |
| Sites usando URLs relativas | 2 |
| Sites listando o mesmo código duas vezes | 6 |
Os códigos não suportados se enquadraram em alguns padrões:
| Padrão | Exemplo | Sites |
|---|---|---|
| País antes do idioma, mais rótulos regionais | mx-es, cz-cs, emea_africa-en | 1 (48 códigos) |
| Underscore em vez de hífen | en_GB, de_DE | 1 (25 códigos) |
| Código de região que não é ISO 3166-1 | en-uk, en-eu, sq-xk | 3 |
| Espanhol latino-americano | es-419 | 2 |
| Código de idioma que não é ISO 639-1 | ceb, skr, o obsoleto iw para hebraico | 3 |
| Códigos inventados | tc, zh-FT, ms-en | 3 |
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.
Links de retorno
Pegamos até duas alternativas de cada homepage anotada, 155 no total, buscamos cada uma e verificamos se cada uma listava a homepage de volta.
| Resultado | Alternativas |
|---|---|
| Apontou diretamente de volta | 133 (86%) |
| Apontou de volta para uma URL diferente para a mesma página | 9 |
| Redirecionou para outro lugar | 9 |
| Genuinamente não listou a página | 4, 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 site | Alternativa | Da Romênia | Do país alvo | Decidido por |
|---|---|---|---|---|
| Site de notícias | Homepage dos EUA | Redirecionou para a edição internacional | Carregou, via uma saída dos EUA | Localização |
| Marketplace | Vitrine alemã | Redirecionou para o site global | Carregou, via uma saída alemã | Localização |
| Fornecedor de videochamadas | Homepage japonesa | Carregou apenas com Accept-Language japonês | O mesmo, via uma saída japonesa | Idioma |
| Editora de jogos | Homepage árabe | Redirecionou | Redirecionou, via uma saída saudita | Nenhum: 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.