Définition
L’attribut hreflang est une balise HTML qui indique aux moteurs de recherche la langue et optionnellement la région géographique ciblée par une page web. Cet élément crucial du SEO international permet d’éviter les problèmes de contenu dupliqué entre versions linguistiques et assure que les utilisateurs voient la version appropriée dans les résultats de recherche selon leur langue et localisation.
Syntaxe et implémentation
Format de base
<!-- Dans le <head> de la page -->
<link rel="alternate" hreflang="fr" href="https://example.com/fr/" />
<link rel="alternate" hreflang="en" href="https://example.com/en/" />
<link rel="alternate" hreflang="es" href="https://example.com/es/" />
<!-- Avec ciblage régional -->
<link rel="alternate" hreflang="fr-FR" href="https://example.com/fr-fr/" />
<link rel="alternate" hreflang="fr-CA" href="https://example.com/fr-ca/" />
<link rel="alternate" hreflang="en-US" href="https://example.com/en-us/" />
<link rel="alternate" hreflang="en-GB" href="https://example.com/en-gb/" />
<!-- X-default pour version par défaut -->
<link rel="alternate" hreflang="x-default" href="https://example.com/" />
Codes langue et région
# Validation codes hreflang
def validate_hreflang_codes(hreflang_value):
"""
Valide format codes langue/région ISO
"""
import re
# Pattern: langue (ISO 639-1) + région optionnelle (ISO 3166-1 Alpha 2)
pattern = r'^([a-z]{2})(-[A-Z]{2})?$|^x-default$'
if not re.match(pattern, hreflang_value):
return False, "Format invalide"
# Codes langue valides (ISO 639-1)
valid_languages = [
'en', 'fr', 'es', 'de', 'it', 'pt', 'nl', 'ru', 'ja', 'zh',
'ar', 'hi', 'ko', 'tr', 'pl', 'sv', 'no', 'da', 'fi', 'cs'
]
# Codes région valides (ISO 3166-1)
valid_regions = [
'US', 'GB', 'CA', 'AU', 'FR', 'DE', 'ES', 'IT', 'BR', 'MX',
'JP', 'CN', 'IN', 'RU', 'NL', 'BE', 'CH', 'AT', 'SE', 'NO'
]
if hreflang_value == 'x-default':
return True, "Valid x-default"
parts = hreflang_value.split('-')
language = parts[0]
region = parts[1] if len(parts) > 1 else None
if language not in valid_languages:
return False, f"Code langue '{language}' non reconnu"
if region and region not in valid_regions:
return False, f"Code région '{region}' non reconnu"
return True, "Code hreflang valide"
Implémentation à grande échelle
Sitemap XML
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<!-- Page avec alternatives hreflang -->
<url>
<loc>https://example.com/en/products/</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/products/"/>
<xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr/produits/"/>
<xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/productos/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/products/"/>
</url>
<url>
<loc>https://example.com/fr/produits/</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/products/"/>
<xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr/produits/"/>
<xhtml:link rel="alternate" hreflang="es" href="https://example.com/es/productos/"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/products/"/>
</url>
</urlset>
HTTP Headers
# Implementation hreflang via headers HTTP
def add_hreflang_headers(response, current_lang, alternatives):
"""
Ajoute hreflang dans headers HTTP
"""
# Format: Link: <URL>; rel="alternate"; hreflang="lang"
link_headers = []
for lang, url in alternatives.items():
link_header = f'<{url}>; rel="alternate"; hreflang="{lang}"'
link_headers.append(link_header)
# Joindre tous les headers Link
response.headers['Link'] = ', '.join(link_headers)
# Exemple résultat:
# Link: <https://example.com/fr/>; rel="alternate"; hreflang="fr",
# <https://example.com/en/>; rel="alternate"; hreflang="en"
return response
Patterns d’implémentation
Structure multilingue
// Gestion URLs multilingues
const hreflangPatterns = {
subdomain: {
pattern: 'https://{lang}.example.com/{path}',
examples: [
'https://fr.example.com/produits',
'https://en.example.com/products',
'https://es.example.com/productos'
],
pros: 'Séparation claire, CDN par région',
cons: 'Gestion DNS complexe'
},
subdirectory: {
pattern: 'https://example.com/{lang}/{path}',
examples: [
'https://example.com/fr/produits',
'https://example.com/en/products',
'https://example.com/es/productos'
],
pros: 'Simple à maintenir, un seul domaine',
cons: 'Structure URL plus longue'
},
tld: {
pattern: 'https://example.{tld}/{path}',
examples: [
'https://example.fr/produits',
'https://example.co.uk/products',
'https://example.es/productos'
],
pros: 'Signal géo fort, trust local',
cons: 'Coût domaines, maintenance'
},
parameter: {
pattern: 'https://example.com/{path}?lang={lang}',
examples: [
'https://example.com/products?lang=fr',
'https://example.com/products?lang=en'
],
pros: 'Très simple',
cons: 'Non recommandé SEO, URLs moins clean'
}
};
Génération automatique
# Générateur hreflang automatique
class HreflangGenerator:
def __init__(self, base_urls, url_mappings):
self.base_urls = base_urls # {'en': 'example.com/en', 'fr': 'example.com/fr'}
self.url_mappings = url_mappings # Correspondances URLs entre langues
def generate_hreflang_tags(self, current_url, current_lang):
"""
Génère tags hreflang pour une page
"""
hreflang_tags = []
# Trouver groupe URLs équivalentes
url_group = self.find_url_group(current_url)
if not url_group:
return []
# Générer tag pour chaque langue
for lang, url in url_group.items():
tag = f'<link rel="alternate" hreflang="{lang}" href="{url}" />'
hreflang_tags.append(tag)
# Ajouter x-default si défini
if 'x-default' in url_group:
tag = f'<link rel="alternate" hreflang="x-default" href="{url_group["x-default"]}" />'
hreflang_tags.append(tag)
return hreflang_tags
def find_url_group(self, url):
"""
Trouve toutes variantes linguistiques d'une URL
"""
# Logique pour matcher URLs équivalentes
for group in self.url_mappings:
if url in group.values():
return group
return None
def validate_implementation(self):
"""
Valide cohérence implémentation
"""
errors = []
for group in self.url_mappings:
# Vérifier bidirectionnalité
for lang1, url1 in group.items():
for lang2, url2 in group.items():
if not self.check_bidirectional(url1, url2):
errors.append(f"Lien manquant: {url1} -> {url2}")
# Vérifier auto-référence
for lang, url in group.items():
if not self.has_self_reference(url, lang):
errors.append(f"Auto-référence manquante: {url}")
return errors
Erreurs courantes
Validation et debug
// Détection erreurs hreflang
const hreflangValidator = {
checkCommonErrors: function(hreflangData) {
const errors = [];
const warnings = [];
// Erreur 1: Tags non bidirectionnels
hreflangData.forEach(page => {
page.hreflangTags.forEach(tag => {
const targetPage = hreflangData.find(p => p.url === tag.href);
if (!targetPage) {
errors.push({
type: 'MISSING_PAGE',
message: `Page ${tag.href} référencée mais non trouvée`
});
} else {
const returnTag = targetPage.hreflangTags.find(
t => t.href === page.url && t.hreflang === page.lang
);
if (!returnTag) {
errors.push({
type: 'NOT_BIDIRECTIONAL',
message: `${page.url} pointe vers ${tag.href} mais pas l'inverse`
});
}
}
});
});
// Erreur 2: Langue-région incohérente
const langRegionPairs = {
'en-FR': 'Anglais en France peu commun',
'es-JP': 'Espagnol au Japon peu commun',
'fr-US': 'Vérifier si vraiment nécessaire'
};
hreflangData.forEach(page => {
page.hreflangTags.forEach(tag => {
if (langRegionPairs[tag.hreflang]) {
warnings.push({
type: 'UNUSUAL_COMBINATION',
url: page.url,
hreflang: tag.hreflang,
message: langRegionPairs[tag.hreflang]
});
}
});
});
// Erreur 3: URLs relatives
hreflangData.forEach(page => {
page.hreflangTags.forEach(tag => {
if (!tag.href.startsWith('http')) {
errors.push({
type: 'RELATIVE_URL',
message: `URL relative utilisée: ${tag.href}`
});
}
});
});
// Erreur 4: Self-reference manquante
hreflangData.forEach(page => {
const selfRef = page.hreflangTags.find(
tag => tag.href === page.url && tag.hreflang === page.lang
);
if (!selfRef) {
errors.push({
type: 'MISSING_SELF_REFERENCE',
message: `${page.url} n'a pas d'auto-référence hreflang="${page.lang}"`
});
}
});
return { errors, warnings };
}
};
Cas d’usage complexes
Sites e-commerce internationaux
# Gestion hreflang e-commerce
class EcommerceHreflang:
def __init__(self):
self.config = {
'markets': {
'fr-FR': {'currency': 'EUR', 'domain': 'fr.shop.com'},
'fr-BE': {'currency': 'EUR', 'domain': 'be.shop.com/fr'},
'nl-BE': {'currency': 'EUR', 'domain': 'be.shop.com/nl'},
'en-US': {'currency': 'USD', 'domain': 'us.shop.com'},
'en-CA': {'currency': 'CAD', 'domain': 'ca.shop.com/en'},
'fr-CA': {'currency': 'CAD', 'domain': 'ca.shop.com/fr'}
}
}
def handle_product_availability(self, product_id):
"""
Gère hreflang selon disponibilité produit
"""
available_markets = self.check_product_availability(product_id)
hreflang_set = []
for market, config in self.config['markets'].items():
if market in available_markets:
# Produit disponible
url = f"https://{config['domain']}/products/{product_id}"
hreflang_set.append({
'hreflang': market,
'href': url
})
else:
# Produit non disponible - pointer vers catégorie
category_url = f"https://{config['domain']}/category/similar-products"
hreflang_set.append({
'hreflang': market,
'href': category_url,
'note': 'Product not available in this market'
})
return hreflang_set
def handle_regional_content(self, content_type):
"""
Contenu spécifique par région
"""
if content_type == 'legal':
# Pages légales différentes par pays
return {
'fr-FR': '/fr/mentions-legales',
'en-US': '/en/terms-of-service',
'en-GB': '/en-gb/terms-and-conditions'
}
elif content_type == 'shipping':
# Info livraison par région
return {
'fr-FR': '/fr/livraison-france',
'fr-BE': '/fr/livraison-belgique',
'en-US': '/en/shipping-usa'
}
Monitoring et maintenance
// Monitoring santé hreflang
const hreflangMonitoring = {
async checkImplementationHealth(domain) {
const issues = {
broken_links: [],
missing_annotations: [],
conflicts: [],
coverage_gaps: []
};
// Crawler toutes pages avec hreflang
const pages = await crawlDomain(domain);
for (const page of pages) {
// Vérifier chaque lien hreflang
for (const hreflangTag of page.hreflangTags) {
const response = await fetch(hreflangTag.href);
if (response.status !== 200) {
issues.broken_links.push({
source: page.url,
target: hreflangTag.href,
status: response.status
});
}
}
// Détecter conflits
const duplicates = page.hreflangTags.filter(
(tag, index, self) =>
index !== self.findIndex(t => t.hreflang === tag.hreflang)
);
if (duplicates.length > 0) {
issues.conflicts.push({
url: page.url,
duplicated_langs: duplicates.map(d => d.hreflang)
});
}
}
return issues;
},
generateReport(issues) {
return {
summary: {
total_issues: Object.values(issues).flat().length,
critical: issues.broken_links.length,
warnings: issues.missing_annotations.length
},
recommendations: this.generateRecommendations(issues),
export_date: new Date().toISOString()
};
}
};
L’implémentation correcte du hreflang est essentielle pour le SEO international, permettant de servir le bon contenu aux bons utilisateurs tout en évitant les problèmes de duplication.