Si vous comparez les prix, le contenu ou la disponibilité d’un marché à l’autre, le premier problème est de trouver la même page sur chaque marché. Deviner les modèles d’URL fonctionne sur certains sites et échoue sur la plupart : un site utilise /de/, un autre de.example.com, un autre un paramètre de requête, et un quatrième un domaine distinct par pays.
Beaucoup de sites publient déjà la réponse. L’annotation hreflang, que les sites ajoutent pour les moteurs de recherche, répertorie chaque version linguistique et nationale d’une page et son emplacement. Lisez-la, et une seule requête vous donne l’ensemble complet. Ce guide explique comment cela fonctionne, ce que nous avons trouvé en vérifiant comment les principaux sites l’utilisent, et pourquoi disposer de la bonne URL ne suffit pas toujours pour voir la bonne page.
Points clés à retenir
- Le 1 octobre 2026, 82 des 253 pages d’accueil de sites majeurs que nous avons récupérées (32%) déclaraient des alternatives hreflang, avec une médiane de 17,5 versions chacune et jusqu’à 157.
- Une seule requête sur une page annotée vous donne toutes les URL localisées pour cette page, sans avoir à deviner les modèles d’URL.
- Les annotations ne sont pas toujours propres : 12 des 82 sites utilisaient au moins un code que Google ne prend pas en charge, comme
en-uk,es-419,en_GBavec un tiret bas, ou le pays placé avant la langue. - 86% des alternatives que nous avons vérifiées renvoyaient directement vers la page d’origine, comme l’exige Google. La plupart des autres pointaient vers une URL différente pour la même page, ou redirigeaient ailleurs.
- Certaines versions ne s’ouvrent qu’à partir du bon endroit. La page d’accueil américaine d’un site d’actualités et la vitrine allemande d’une place de marché ont redirigé notre connexion depuis la Roumanie vers ailleurs, et se sont chargées normalement via des sorties aux États-Unis et en Allemagne.
Comment fonctionne hreflang
Une page répertorie ses alternatives dans son <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/" />
Chaque valeur est un code de langue, éventuellement suivi d’un script ou d’une région : de pour l’allemand partout, en-GB pour l’anglais au Royaume-Uni, zh-Hant pour le chinois traditionnel. x-default désigne la version par défaut pour les visiteurs qui ne correspondent à aucune d’entre elles. La documentation de Google établit les règles que la plupart des sites suivent :
- Les langues utilisent les codes ISO 639-1, les scripts ISO 15924 et les régions ISO 3166-1 alpha-2. Une région seule n’est pas valide, et les codes en dehors de ces normes, comme
es-419pour l’espagnol d’Amérique latine, ne sont pas pris en charge. - Les URL doivent être complètes, y compris
https://. - Chaque version doit se lister elle-même ainsi que toutes les autres versions, et si deux pages ne se renvoient pas l’une vers l’autre, les annotations peuvent être ignorées.
- La même information peut être donnée dans le HTML, dans un en-tête HTTP
Link(utile pour les PDF) ou dans un sitemap.
Pour la collecte de données, la troisième règle est celle qui compte. Puisque chaque version est censée lister toutes les autres, n’importe laquelle d’entre elles constitue une carte complète de l’ensemble.
Ce que nous avons trouvé sur les principaux sites
Nous avons récupéré les pages d’accueil des 500 principaux domaines du classement Tranco des sites populaires. 253 sites distincts ont renvoyé une page d’accueil HTML ; le reste était des réseaux de contenu, des hôtes d’API et d’autres domaines sans page d’accueil, ou des doublons d’un site déjà comptabilisé.
| Mesure | Résultat |
|---|---|
| Pages d’accueil avec annotations hreflang | 82 sur 253 (32%) |
| Alternatives par page annotée | médiane 17,5, maximum 157 |
Pages avec un x-default | 63 sur 82 |
Pages d’accueil envoyant hreflang dans un en-tête HTTP Link | 0 |
| Sites avec au moins un code non pris en charge | 12 sur 82 |
| Sites utilisant des URL relatives | 2 |
| Sites listant le même code deux fois | 6 |
Les codes non pris en charge suivaient quelques modèles :
| Modèle | Exemple | Sites |
|---|---|---|
| Pays avant langue, plus libellés régionaux | mx-es, cz-cs, emea_africa-en | 1 (48 codes) |
| Tiret bas au lieu d’un trait d’union | en_GB, de_DE | 1 (25 codes) |
| Code de région qui n’est pas ISO 3166-1 | en-uk, en-eu, sq-xk | 3 |
| Espagnol d’Amérique latine | es-419 | 2 |
| Code de langue qui n’est pas ISO 639-1 | ceb, skr, l’ancien iw pour l’hébreu | 3 |
| Codes inventés | tc, zh-FT, ms-en | 3 |
Certains sites apparaissent dans plusieurs lignes. Le modèle pays-d’abord est le plus trompeur pour un analyseur, car plusieurs de ces codes sont accidentellement valides dans l’ordre inverse : ca est le code du catalan, id de l’indonésien et th du thaï, de sorte que ca-en se lit comme catalan plutôt qu’anglais pour le Canada. Ne déduisez jamais un marché à partir du seul code sans vérifier que cela a du sens.
Liens de retour
Nous avons pris jusqu’à deux alternatives par page d’accueil annotée, soit 155 au total, les avons récupérées, et avons vérifié si chacune renvoyait vers la page d’accueil d’origine.
| Résultat | Alternatives |
|---|---|
| Renvoyaient directement | 133 (86%) |
| Renvoyaient vers une URL différente pour la même page | 9 |
| Redirigeaient ailleurs | 9 |
| Ne listaient réellement pas la page | 4, sur deux sites |
La deuxième ligne est la plus subtile. Sur plusieurs sites, la page d’accueil sur laquelle nous avons atterri se trouvait à une URL tandis que l’ensemble en nommait une autre : / contre /en/, /homepage ou /home.html, ou une URL nue contre une URL avec un paramètre de langue. Les annotations étaient cohérentes ; notre point d’entrée n’était simplement pas l’URL que le site considère comme canonique. Lorsque vous collectez, indexez chaque version par l’URL que l’ensemble lui-même utilise, et non celle sur laquelle vous êtes arrivé par hasard.
La bonne URL ne suffit pas toujours
Les redirections ont été la partie la plus instructive. Nous avons récupéré chaque alternative redirigeante à nouveau, directement et via des sorties Shifter dans le pays cible, avec des en-têtes Accept-Language en anglais et locaux :
| Type de site | Alternative | Depuis la Roumanie | Depuis le pays cible | Déterminé par |
|---|---|---|---|---|
| Site d’actualités | Page d’accueil américaine | Redirigée vers l’édition internationale | Chargée, via une sortie américaine | Localisation |
| Place de marché | Vitrine allemande | Redirigée vers le site mondial | Chargée, via une sortie allemande | Localisation |
| Fournisseur d’appels vidéo | Page d’accueil japonaise | Chargée uniquement avec Accept-Language japonais | Idem, via une sortie japonaise | Langue |
| Éditeur de jeux | Page d’accueil arabe | Redirigée | Redirigée, via une sortie saoudienne | Ni l’un ni l’autre : listée mais inaccessible |
Deux sites se sont déterminés selon la localisation du visiteur, la place de marché quelle que soit la langue, de sorte que leurs propres annotations pointaient vers des pages qu’un visiteur d’ailleurs ne pouvait pas ouvrir. Un site s’est déterminé selon la langue, quelle que soit la localisation. Et un a listé une version qui redirigeait dans toutes les combinaisons que nous avons essayées, ce qui vaut la peine d’être su avant de construire une comparaison de marché sur cette base.
C’est là que hreflang et la localisation de la sortie se rejoignent. Les annotations vous indiquent quelles versions existent et où ; les collecter fidèlement signifie demander chacune comme le ferait un visiteur local, depuis ce pays et avec cette langue, comme indiqué dans faire correspondre géolocalisation, fuseau horaire et paramètres régionaux du proxy. Sinon, vous risquez d’enregistrer la version internationale sous une étiquette allemande.
Le code
Le module ci-dessous lit les alternatives HTML d’une page, signale les codes non pris en charge et les URL relatives, et vérifie les liens de retour, en indiquant pourquoi chaque alternative défaillante échoue :
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
Exécuté sur notre propre page d’accueil :
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: {}
Deux limites méritent d’être connues. Le code ne lit les annotations que dans le HTML ; les sites qui les publient dans un en-tête HTTP ou un sitemap nécessitent que ceux-ci soient lus également, et les sitemaps comme source de découverte couvre le côté sitemap. Et check_return_links récupère chaque alternative qu’on lui fournit, donc sur une page avec 150 versions, échantillonnez-en quelques-unes ou espacez les requêtes.
Mettre cela en pratique
- Commencez par une page par modèle. Un produit, une catégorie et un article partagent généralement un modèle d’annotation, donc une seule requête sur chacun montre comment l’ensemble du site cartographie ses marchés.
- Indexez les versions par les URL propres à l’ensemble. Stockez l’URL que chaque alternative déclare, et traitez les redirections comme des résultats plutôt que comme du bruit.
- Récupérez chaque version comme le ferait un visiteur local. Utilisez une sortie dans le marché concerné et la langue de ce marché, puis vérifiez que vous n’avez pas été redirigé. La localisation a déterminé deux de nos quatre cas de redirection, comme c’est le cas dans quels pays sont le plus souvent géobloqués.
- Validez les codes avant de leur faire confiance. Un code inversé ou inventé peut discrètement assigner une page au mauvais marché.
- Normalisez ce que vous collectez. Une fois que vous disposez de chaque version, les prix, dates et nombres diffèrent toujours dans leur format, comme indiqué dans normaliser les prix, nombres et dates selon les paramètres régionaux, et la comparaison elle-même fait l’objet de détecter la tarification géo-personnalisée.
En résumé
hreflang est ce qui se rapproche le plus sur le web d’un index publié des marchés d’une page. Un tiers des principaux sites le fournit, une seule requête révèle l’ensemble complet, et la plupart de l’ensemble se relie comme il se doit.
C’est une carte, pas une garantie. Les codes peuvent être erronés, l’URL sur laquelle vous atterrissez peut ne pas être celle que l’ensemble utilise, et certaines versions ne s’ouvrent que pour les visiteurs au bon endroit ou avec la bonne langue. Lisez les annotations, validez-les, et récupérez chaque version comme le ferait un visiteur local.
Sources et références
- Google Search Central, Informer Google des versions localisées de votre page.
- Tranco, liste Q2K34, agrégeant les classements de domaines du 1 au 30 septembre 2026.
- Babel, dont les données CLDR fournissent les listes de langues, scripts et régions dans le code.
- Pages d’accueil et alternatives récupérées par Shifter le 1 octobre 2026, directement et via des sorties Shifter, en utilisant le code ci-dessus.