← Retour au lexique
📖 SEO International

Hreflang

Attribut HTML indiquant aux moteurs de recherche la langue et la région ciblée d'une page, utilisé pour gérer les contenus multilingues.

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.