Base de connaissances

Utiliser hreflang pour trouver chaque version linguistique et nationale d'une page

hreflang répertorie chaque version localisée d'une page. Nous avons vérifié comment les plus grands sites l'utilisent, ce qui ne fonctionne pas, et pourquoi certaines versions ne s'ouvrent que depuis l'intérieur d'un pays.

James Meadow

James Meadow

1 octobre 2026 · 12 min de lecture

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_GB avec 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-419 pour 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é.

MesureRésultat
Pages d’accueil avec annotations hreflang82 sur 253 (32%)
Alternatives par page annotéemédiane 17,5, maximum 157
Pages avec un x-default63 sur 82
Pages d’accueil envoyant hreflang dans un en-tête HTTP Link0
Sites avec au moins un code non pris en charge12 sur 82
Sites utilisant des URL relatives2
Sites listant le même code deux fois6

Les codes non pris en charge suivaient quelques modèles :

ModèleExempleSites
Pays avant langue, plus libellés régionauxmx-es, cz-cs, emea_africa-en1 (48 codes)
Tiret bas au lieu d’un trait d’unionen_GB, de_DE1 (25 codes)
Code de région qui n’est pas ISO 3166-1en-uk, en-eu, sq-xk3
Espagnol d’Amérique latinees-4192
Code de langue qui n’est pas ISO 639-1ceb, skr, l’ancien iw pour l’hébreu3
Codes inventéstc, zh-FT, ms-en3

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ésultatAlternatives
Renvoyaient directement133 (86%)
Renvoyaient vers une URL différente pour la même page9
Redirigeaient ailleurs9
Ne listaient réellement pas la page4, 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 siteAlternativeDepuis la RoumanieDepuis le pays cibleDéterminé par
Site d’actualitésPage d’accueil américaineRedirigée vers l’édition internationaleChargée, via une sortie américaineLocalisation
Place de marchéVitrine allemandeRedirigée vers le site mondialChargée, via une sortie allemandeLocalisation
Fournisseur d’appels vidéoPage d’accueil japonaiseChargée uniquement avec Accept-Language japonaisIdem, via une sortie japonaiseLangue
Éditeur de jeuxPage d’accueil arabeRedirigéeRedirigée, via une sortie saoudienneNi 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.

Prêt à commencer ?

Essayez les proxies résidentiels de Shifter, 205M+ IPs, 195+ pays, à partir de 0,75 $/GB.

Commencer