[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fcUaRXUYN-LKQvwiUg2_BtjWwFW5ACEKybcUJlKCgCjE":3,"$fRoeEVMk5vbSnVLy59it1QwDZSOOCgysJoEmOrxScgU4":23,"$fcTFjTaPPT1sVcnh1wWEGoTWMSaOD4bwmwntkMae6w5s":111},{"_id":4,"slug":5,"__v":6,"author":7,"body":8,"canonical":9,"category":10,"createdAt":11,"date":12,"description":13,"htmlContent":14,"image":15,"imageAlt":15,"readingTime":16,"tags":17,"title":21,"updatedAt":22},"6a34db4aaa6b273b0c724ac7","hreflang-generated-pointent-vers-des-domaines-supprimes",0,"Equipe Seogard","# Hreflang générés vers des domaines supprimés : autopsie d'un nettoyage oublié\n\nMardi 14h. Le Head of SEO d'une marketplace mode européenne ouvre son rapport Search Console hebdomadaire. Le marché français — 12 000 pages produit, 480K clics mensuels — affiche une courbe descendante depuis six semaines. Pas un effondrement brutal. Une érosion lente, −3% par semaine, presque invisible dans le bruit quotidien. Sauf qu'en cumulé, c'est −38% de trafic organique sur le cluster allemand, et −11% sur le .fr. Le site tourne sur Next.js 14 avec App Router, un CMS headless Contentful, et un système de gestion i18n maison. Le marché allemand (shop-marque.de) a été fermé quatre mois plus tôt. Le domaine n'a pas été renouvelé.\n\n## Mercredi 9h12 — Le signal faible devient alarme\n\nL'équipe SEO creuse d'abord les suspects habituels. Core Update ? Rien d'annoncé par Google sur la période. Changement de contenu ? Le dernier déploiement majeur remonte à trois semaines — une refonte des filtres de navigation, sans impact sur les templates produit. Problème d'indexation ? Le rapport \"Pages\" de Search Console montre un chiffre stable de pages indexées pour le .fr.\n\nLe premier vrai indice arrive à 10h35. Un dev frontend, en inspectant une page produit dans Chrome DevTools pour un tout autre sujet, remarque quelque chose dans le `\u003Chead>` :\n\n```html\n\u003Clink rel=\"alternate\" hreflang=\"fr\" href=\"https://www.shop-marque.fr/produit/robe-ete-lin\" />\n\u003Clink rel=\"alternate\" hreflang=\"de\" href=\"https://www.shop-marque.de/produkt/sommerkleid-leinen\" />\n\u003Clink rel=\"alternate\" hreflang=\"en\" href=\"https://www.shop-marque.com/product/summer-linen-dress\" />\n\u003Clink rel=\"alternate\" hreflang=\"x-default\" href=\"https://www.shop-marque.com/product/summer-linen-dress\" />\n```\n\nLa ligne `hreflang=\"de\"` pointe vers `shop-marque.de`. Ce domaine n'existe plus. Le DNS ne résout plus. Le certificat TLS a expiré il y a trois mois. Quiconque — ou quoi que ce soit — qui suit ce lien tombe sur une erreur de connexion.\n\nLe Head of SEO ouvre Screaming Frog, lance un crawl de 500 pages du .fr. Résultat : **100% des pages produit, catégorie et contenu éditorial** contiennent un hreflang vers le .de mort. Sur 12 347 pages crawlées, 12 347 pointent vers un domaine fantôme.\n\nÀ 11h20, l'équipe vérifie l'outil d'inspection d'URL de Search Console. Google voit les hreflang. Google suit les liens. Google tombe sur un domaine mort. Et Google fait exactement ce que la documentation prévoit dans ce cas : il ignore l'ensemble du cluster hreflang de la page. Pas seulement la ligne `de`. Toutes les lignes.\n\nLe CTO rejoint le call à 11h45. La question est simple : comment 12 000 pages génèrent-elles encore un hreflang vers un domaine fermé il y a quatre mois ?\n\nLa réponse est dans le code. Et dans un fichier de configuration que personne n'a touché depuis la fermeture.\n\n## Le bug : un tableau de locales jamais nettoyé\n\nL'architecture i18n du site repose sur un système classique pour Next.js 14 App Router. Un fichier de configuration centralise les locales disponibles :\n\n```typescript\n// lib/i18n/config.ts\nexport const locales = [\n  {\n    code: 'fr',\n    domain: 'www.shop-marque.fr',\n    defaultLocale: false,\n    contentfulLocale: 'fr-FR',\n  },\n  {\n    code: 'de',\n    domain: 'www.shop-marque.de',\n    defaultLocale: false,\n    contentfulLocale: 'de-DE',\n  },\n  {\n    code: 'en',\n    domain: 'www.shop-marque.com',\n    defaultLocale: true,\n    contentfulLocale: 'en-US',\n  },\n] as const;\n\nexport type LocaleCode = (typeof locales)[number]['code'];\n```\n\nCe fichier alimente tout : le routing, les requêtes Contentful, les sitemaps, et — c'est le point critique — la génération des balises hreflang.\n\nLe composant responsable de l'injection des hreflang dans le `\u003Chead>` vit dans le layout racine :\n\n```typescript\n// app/layout.tsx (extrait simplifié)\nimport { locales } from '@/lib/i18n/config';\n\nfunction HreflangTags({ slug, currentLocale }: { slug: string; currentLocale: string }) {\n  return (\n    \u003C>\n      {locales.map((locale) => (\n        \u003Clink\n          key={locale.code}\n          rel=\"alternate\"\n          hrefLang={locale.code}\n          href={`https://${locale.domain}/${slug}`}\n        />\n      ))}\n      \u003Clink\n        rel=\"alternate\"\n        hrefLang=\"x-default\"\n        href={`https://${locales.find(l => l.defaultLocale)!.domain}/${slug}`}\n      />\n    \u003C/>\n  );\n}\n```\n\nLe code itère sur **toutes** les locales du tableau. Aucun filtre. Aucune vérification que le domaine est actif. Aucun flag `enabled: true/false`. Le tableau `locales` est la source de vérité unique, et personne ne l'a modifié quand le marché allemand a été décommissionné.\n\n### Pourquoi personne n'a rien vu\n\nQuatre facteurs ont conspiré :\n\n**1. La fermeture du .de a été traitée comme un projet infrastructure, pas SEO.** Le ticket JIRA \"Fermer shop-marque.de\" listait : couper les paiements, désactiver les comptes clients, arrêter les flux logistiques, ne pas renouveler le domaine. Aucune sous-tâche ne mentionnait les hreflang du .fr ou du .com.\n\n**2. Le composant `HreflangTags` n'avait aucun test.** Pas un seul test unitaire, pas un test e2e vérifiant le contenu du `\u003Chead>`. Le composant avait été écrit une fois, deux ans plus tôt, et jamais retouché. L'équipe testait les pages dans un navigateur — et les hreflang ne sont pas visibles pour un utilisateur humain.\n\n**3. Contentful avait bien été nettoyé.** Les entrées de contenu en `de-DE` avaient été archivées. Les requêtes GraphQL pour la locale `de` retournaient des résultats vides. Mais le composant `HreflangTags` ne consommait pas Contentful — il lisait le tableau statique `locales`. Deux sources de vérité, désynchronisées.\n\n**4. L'impact SEO a été graduel.** Google n'invalide pas immédiatement un cluster hreflang cassé. Il recrawle les pages à son rythme, constate que le lien `de` est mort, et commence à ignorer les signaux hreflang de la page. Sur un site de 12 000 pages avec un crawl budget correct, ce processus prend des semaines. Le temps que l'érosion soit visible dans les métriques, quatre mois se sont écoulés.\n\n### Ce que Googlebot voit vs ce que le développeur voit\n\nLe développeur ouvre la page produit dans Chrome. Il voit une fiche produit qui fonctionne. Les hreflang sont dans le code source, mais ils ne sont pas rendus visuellement. Même en inspectant le `\u003Chead>`, il faut savoir qu'il faut chercher un domaine mort parmi les `\u003Clink>`.\n\nGooglebot, lui, fait exactement ceci :\n\n1. Crawle `https://www.shop-marque.fr/produit/robe-ete-lin`.\n2. Parse le `\u003Chead>`, extrait les hreflang.\n3. Tente de valider le cluster : pour chaque URL hreflang, Google vérifie que la page cible contient un hreflang retour (reciprocal link).\n4. `https://www.shop-marque.de/produkt/sommerkleid-leinen` → DNS failure. Pas de réponse HTTP. Pas de lien retour possible.\n5. Le cluster hreflang est considéré comme **invalide**. Google ignore toutes les annotations hreflang de la page — y compris le lien `fr` ↔ `en` qui, lui, fonctionne parfaitement.\n\nC'est documenté dans la [documentation officielle Google sur les hreflang](https://developers.google.com/search/docs/specialty/international/localized-versions) : \"If the annotations are inconsistent, we may ignore them entirely.\"\n\nLe résultat concret : Google ne sait plus que `shop-marque.fr` et `shop-marque.com` sont des versions localisées du même contenu. Les pages .fr commencent à concurrencer les pages .com dans les SERPs françaises. Le trafic organique se fragmente.\n\n### Vérification à grande échelle avec Screaming Frog\n\nPour quantifier l'étendue du dégât, l'équipe lance un crawl complet avec Screaming Frog, en activant l'extraction custom :\n\n```\nConfiguration > Custom Extraction\nExtraction: XPath\nXPath: //link[@rel='alternate' and @hreflang='de']/@href\n```\n\nExport CSV. 12 347 lignes. Toutes contiennent `https://www.shop-marque.de/...`. Un `curl` rapide confirme que le domaine est mort :\n\n```bash\ncurl -sI https://www.shop-marque.de/produkt/sommerkleid-leinen\n# curl: (6) Could not resolve host: www.shop-marque.de\n```\n\nDNS NXDOMAIN. Le domaine a expiré et n'a pas été racheté. Il est disponible à l'achat chez n'importe quel registrar. Ce qui ouvre un autre risque : si un tiers rachète le domaine et y place du contenu, les hreflang du .fr et du .com pointeront vers un site tiers. Un scénario de détournement classique, rarement anticipé.\n\n## Le fix : supprimer, redéployer, revalider\n\n### Étape 1 — Nettoyer la source de vérité\n\nLe correctif minimal consiste à retirer la locale `de` du tableau. Mais l'équipe prend une décision plus robuste : ajouter un flag `active` pour gérer proprement les futures fermetures de marché.\n\n```typescript\n// lib/i18n/config.ts — version corrigée\nexport const locales = [\n  {\n    code: 'fr',\n    domain: 'www.shop-marque.fr',\n    defaultLocale: false,\n    contentfulLocale: 'fr-FR',\n    active: true,\n  },\n  {\n    code: 'de',\n    domain: 'www.shop-marque.de',\n    defaultLocale: false,\n    contentfulLocale: 'de-DE',\n    active: false, // Marché fermé 2026-02\n  },\n  {\n    code: 'en',\n    domain: 'www.shop-marque.com',\n    defaultLocale: true,\n    contentfulLocale: 'en-US',\n    active: true,\n  },\n] as const;\n\nexport const activeLocales = locales.filter((l) => l.active);\n```\n\n### Étape 2 — Filtrer les hreflang\n\nLe composant `HreflangTags` utilise désormais `activeLocales` :\n\n```typescript\n// app/layout.tsx — composant corrigé\nimport { activeLocales } from '@/lib/i18n/config';\n\nfunction HreflangTags({ slug, currentLocale }: { slug: string; currentLocale: string }) {\n  return (\n    \u003C>\n      {activeLocales.map((locale) => (\n        \u003Clink\n          key={locale.code}\n          rel=\"alternate\"\n          hrefLang={locale.code}\n          href={`https://${locale.domain}/${slug}`}\n        />\n      ))}\n      \u003Clink\n        rel=\"alternate\"\n        hrefLang=\"x-default\"\n        href={`https://${activeLocales.find(l => l.defaultLocale)!.domain}/${slug}`}\n      />\n    \u003C/>\n  );\n}\n```\n\nLe HTML rendu après correctif :\n\n```html\n\u003Clink rel=\"alternate\" hreflang=\"fr\" href=\"https://www.shop-marque.fr/produit/robe-ete-lin\" />\n\u003Clink rel=\"alternate\" hreflang=\"en\" href=\"https://www.shop-marque.com/product/summer-linen-dress\" />\n\u003Clink rel=\"alternate\" hreflang=\"x-default\" href=\"https://www.shop-marque.com/product/summer-linen-dress\" />\n```\n\nPlus de ligne `de`. Le cluster hreflang ne contient plus que des domaines actifs avec des liens réciproques fonctionnels.\n\n### Étape 3 — Vérifier les sitemaps\n\nLe sitemap index référençait encore un `sitemap-de.xml`. Même logique : le générateur de sitemap lisait le même tableau `locales`. Après le fix, le sitemap index ne liste plus que les sitemaps `fr` et `en`. L'ancien `sitemap-de.xml` retourne désormais un 404, ce qui est le comportement attendu — Google cessera de le requêter après quelques tentatives. Un problème similaire de sitemap pointant vers un mauvais domaine avait été documenté dans un [incident Astro avec un domaine inexistant](/blog/astro-site-config-build-local-le-sitemap-entier-pointe-vers-un-domaine-inexistant).\n\n### Étape 4 — Invalider les caches et forcer le recrawl\n\nLe site utilise un CDN avec cache edge de 24h. Un simple redéploiement ne suffit pas — les anciennes pages avec hreflang cassés resteraient servies pendant 24h.\n\n```bash\n# Purge du cache CDN (Cloudflare)\ncurl -X POST \"https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache\" \\\n  -H \"Authorization: Bearer {token}\" \\\n  -H \"Content-Type: application/json\" \\\n  --data '{\"purge_everything\":true}'\n```\n\nEnsuite, soumission du sitemap mis à jour dans Search Console, et demande d'inspection d'URL sur 10 pages stratégiques pour vérifier que Google voit bien le nouveau `\u003Chead>`.\n\n### Étape 5 — Ajouter des tests\n\nL'équipe ajoute un test d'intégration dans la CI qui vérifie que chaque URL hreflang dans le `\u003Chead>` pointe vers un domaine résolvable :\n\n```typescript\n// __tests__/hreflang.test.ts\nimport { activeLocales } from '@/lib/i18n/config';\nimport dns from 'dns/promises';\n\ndescribe('hreflang domains', () => {\n  it.each(activeLocales)('$code domain $domain resolves', async (locale) => {\n    const result = await dns.lookup(locale.domain).catch(() => null);\n    expect(result).not.toBeNull();\n  });\n\n  it('no inactive locale appears in activeLocales', () => {\n    const inactive = activeLocales.filter((l) => !l.active);\n    expect(inactive).toHaveLength(0);\n  });\n});\n```\n\nCe test échouera en CI si quelqu'un ajoute une locale avec un domaine mort, ou si un domaine actif expire sans que l'équipe s'en aperçoive.\n\n### Temps de récupération\n\nLe fix a été déployé un jeudi. Les premiers signes de récupération dans Search Console sont apparus au bout de 8 jours — le temps que Google recrawle une fraction significative des 12 000 pages et revalide les clusters hreflang. La récupération complète du trafic organique a pris 23 jours. Le trafic .fr est revenu à son niveau d'avant l'érosion. Le trafic .com sur les requêtes françaises a cessé de cannibaliser le .fr.\n\nLe domaine `shop-marque.de` a été racheté par l'équipe pour éviter tout détournement futur, même sans intention de le réutiliser. Coût : 9€/an. Assurance contre un risque de réputation potentiellement catastrophique.\n\nL'incident a aussi révélé que les [signaux de langue cassés](/blog/crowdin-auto-translate-injecte-lang-auto-signal-de-langue-casse-sur-tout-le-site) ne se limitent pas aux attributs `lang` — les hreflang sont un vecteur de régression tout aussi silencieux. Et comme pour les [métadonnées SEO qui se désynchronisent entre CMS et framework](/blog/contentful-le-champ-seo-title-non-sync-vers-next-js-fallback-first-h1-genere), la cause racine est toujours la même : deux sources de vérité qui divergent sans que personne ne monitore la divergence.\n\n## Ce qu'on en retient\n\nFermer un marché, c'est un projet business. Nettoyer les artefacts techniques de ce marché — hreflang, sitemaps, redirections, structured data —, c'est un projet technique qui n'apparaît dans aucun ticket JIRA si personne ne le demande explicitement.\n\nTrois règles à graver :\n\n- Chaque locale doit avoir un flag `active`. Supprimer une entrée d'un tableau, c'est perdre la trace de son existence. La désactiver, c'est documenter la décision.\n- Les hreflang doivent être testés en CI. Un test DNS sur chaque domaine cible prend 200ms et évite des semaines de régression silencieuse.\n- Les domaines fermés doivent être conservés. 9€/an, contre le risque qu'un tiers récupère le domaine et hérite de signaux hreflang pointant depuis un site à forte autorité.\n\nUn outil de monitoring continu comme Seogard détecte ce type de hreflang cassé dès le premier crawl post-déploiement — pas quatre mois plus tard, quand la courbe de trafic a déjà décroché.\n```","https://seogard.io/blog/hreflang-generated-pointent-vers-des-domaines-supprimes","i18n","2026-06-19T06:01:46.165Z","2026-06-19","Un marché allemand fermé, des hreflang encore générés vers le .de mort. Récit de l'incident, diagnostic technique et fix complet.","\u003Ch1>Hreflang générés vers des domaines supprimés : autopsie d'un nettoyage oublié\u003C/h1>\n\u003Cp>Mardi 14h. Le Head of SEO d'une marketplace mode européenne ouvre son rapport Search Console hebdomadaire. Le marché français — 12 000 pages produit, 480K clics mensuels — affiche une courbe descendante depuis six semaines. Pas un effondrement brutal. Une érosion lente, −3% par semaine, presque invisible dans le bruit quotidien. Sauf qu'en cumulé, c'est −38% de trafic organique sur le cluster allemand, et −11% sur le .fr. Le site tourne sur Next.js 14 avec App Router, un CMS headless Contentful, et un système de gestion i18n maison. Le marché allemand (shop-marque.de) a été fermé quatre mois plus tôt. Le domaine n'a pas été renouvelé.\u003C/p>\n\u003Ch2>Mercredi 9h12 — Le signal faible devient alarme\u003C/h2>\n\u003Cp>L'équipe SEO creuse d'abord les suspects habituels. Core Update ? Rien d'annoncé par Google sur la période. Changement de contenu ? Le dernier déploiement majeur remonte à trois semaines — une refonte des filtres de navigation, sans impact sur les templates produit. Problème d'indexation ? Le rapport \"Pages\" de Search Console montre un chiffre stable de pages indexées pour le .fr.\u003C/p>\n\u003Cp>Le premier vrai indice arrive à 10h35. Un dev frontend, en inspectant une page produit dans Chrome DevTools pour un tout autre sujet, remarque quelque chose dans le \u003Ccode>&#x3C;head>\u003C/code> :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">link\u003C/span>\u003Cspan style=\"color:#B392F0\"> rel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"alternate\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> hreflang\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"fr\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> href\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"https://www.shop-marque.fr/produit/robe-ete-lin\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">link\u003C/span>\u003Cspan style=\"color:#B392F0\"> rel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"alternate\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> hreflang\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"de\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> href\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"https://www.shop-marque.de/produkt/sommerkleid-leinen\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">link\u003C/span>\u003Cspan style=\"color:#B392F0\"> rel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"alternate\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> hreflang\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"en\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> href\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"https://www.shop-marque.com/product/summer-linen-dress\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">link\u003C/span>\u003Cspan style=\"color:#B392F0\"> rel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"alternate\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> hreflang\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"x-default\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> href\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"https://www.shop-marque.com/product/summer-linen-dress\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>La ligne \u003Ccode>hreflang=\"de\"\u003C/code> pointe vers \u003Ccode>shop-marque.de\u003C/code>. Ce domaine n'existe plus. Le DNS ne résout plus. Le certificat TLS a expiré il y a trois mois. Quiconque — ou quoi que ce soit — qui suit ce lien tombe sur une erreur de connexion.\u003C/p>\n\u003Cp>Le Head of SEO ouvre Screaming Frog, lance un crawl de 500 pages du .fr. Résultat : \u003Cstrong>100% des pages produit, catégorie et contenu éditorial\u003C/strong> contiennent un hreflang vers le .de mort. Sur 12 347 pages crawlées, 12 347 pointent vers un domaine fantôme.\u003C/p>\n\u003Cp>À 11h20, l'équipe vérifie l'outil d'inspection d'URL de Search Console. Google voit les hreflang. Google suit les liens. Google tombe sur un domaine mort. Et Google fait exactement ce que la documentation prévoit dans ce cas : il ignore l'ensemble du cluster hreflang de la page. Pas seulement la ligne \u003Ccode>de\u003C/code>. Toutes les lignes.\u003C/p>\n\u003Cp>Le CTO rejoint le call à 11h45. La question est simple : comment 12 000 pages génèrent-elles encore un hreflang vers un domaine fermé il y a quatre mois ?\u003C/p>\n\u003Cp>La réponse est dans le code. Et dans un fichier de configuration que personne n'a touché depuis la fermeture.\u003C/p>\n\u003Ch2>Le bug : un tableau de locales jamais nettoyé\u003C/h2>\n\u003Cp>L'architecture i18n du site repose sur un système classique pour Next.js 14 App Router. Un fichier de configuration centralise les locales disponibles :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// lib/i18n/config.ts\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> locales\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    code: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'fr'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    domain: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'www.shop-marque.fr'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    defaultLocale: \u003C/span>\u003Cspan style=\"color:#79B8FF\">false\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    contentfulLocale: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'fr-FR'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    code: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'de'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    domain: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'www.shop-marque.de'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    defaultLocale: \u003C/span>\u003Cspan style=\"color:#79B8FF\">false\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    contentfulLocale: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'de-DE'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    code: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'en'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    domain: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'www.shop-marque.com'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    defaultLocale: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    contentfulLocale: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'en-US'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">as\u003C/span>\u003Cspan style=\"color:#F97583\"> const\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> type\u003C/span>\u003Cspan style=\"color:#B392F0\"> LocaleCode\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">typeof\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> locales)[\u003C/span>\u003Cspan style=\"color:#79B8FF\">number\u003C/span>\u003Cspan style=\"color:#E1E4E8\">][\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'code'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">];\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Ce fichier alimente tout : le routing, les requêtes Contentful, les sitemaps, et — c'est le point critique — la génération des balises hreflang.\u003C/p>\n\u003Cp>Le composant responsable de l'injection des hreflang dans le \u003Ccode>&#x3C;head>\u003C/code> vit dans le layout racine :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// app/layout.tsx (extrait simplifié)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { locales } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '@/lib/i18n/config'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">function\u003C/span>\u003Cspan style=\"color:#B392F0\"> HreflangTags\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({ \u003C/span>\u003Cspan style=\"color:#FFAB70\">slug\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">currentLocale\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> }\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#FFAB70\">slug\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; \u003C/span>\u003Cspan style=\"color:#FFAB70\">currentLocale\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> }) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    &#x3C;>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      {\u003C/span>\u003Cspan style=\"color:#FFAB70\">locales\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#FFAB70\">map\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">locale\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        &#x3C;\u003C/span>\u003Cspan style=\"color:#FFAB70\">link\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          key\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{locale.code}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          rel\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"alternate\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          hrefLang\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{locale.code}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          href\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`https://${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">locale\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">domain\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}/${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      ))}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#FFAB70\">link\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        rel\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"alternate\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        hrefLang\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"x-default\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        href\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`https://${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">locales\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#B392F0\">find\u003C/span>\u003Cspan style=\"color:#9ECBFF\">(\u003C/span>\u003Cspan style=\"color:#79B8FF\">l\u003C/span>\u003Cspan style=\"color:#F97583\"> =>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> l\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">defaultLocale\u003C/span>\u003Cspan style=\"color:#9ECBFF\">)\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">domain\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}/${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    &#x3C;/>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le code itère sur \u003Cstrong>toutes\u003C/strong> les locales du tableau. Aucun filtre. Aucune vérification que le domaine est actif. Aucun flag \u003Ccode>enabled: true/false\u003C/code>. Le tableau \u003Ccode>locales\u003C/code> est la source de vérité unique, et personne ne l'a modifié quand le marché allemand a été décommissionné.\u003C/p>\n\u003Ch3>Pourquoi personne n'a rien vu\u003C/h3>\n\u003Cp>Quatre facteurs ont conspiré :\u003C/p>\n\u003Cp>\u003Cstrong>1. La fermeture du .de a été traitée comme un projet infrastructure, pas SEO.\u003C/strong> Le ticket JIRA \"Fermer shop-marque.de\" listait : couper les paiements, désactiver les comptes clients, arrêter les flux logistiques, ne pas renouveler le domaine. Aucune sous-tâche ne mentionnait les hreflang du .fr ou du .com.\u003C/p>\n\u003Cp>\u003Cstrong>2. Le composant \u003Ccode>HreflangTags\u003C/code> n'avait aucun test.\u003C/strong> Pas un seul test unitaire, pas un test e2e vérifiant le contenu du \u003Ccode>&#x3C;head>\u003C/code>. Le composant avait été écrit une fois, deux ans plus tôt, et jamais retouché. L'équipe testait les pages dans un navigateur — et les hreflang ne sont pas visibles pour un utilisateur humain.\u003C/p>\n\u003Cp>\u003Cstrong>3. Contentful avait bien été nettoyé.\u003C/strong> Les entrées de contenu en \u003Ccode>de-DE\u003C/code> avaient été archivées. Les requêtes GraphQL pour la locale \u003Ccode>de\u003C/code> retournaient des résultats vides. Mais le composant \u003Ccode>HreflangTags\u003C/code> ne consommait pas Contentful — il lisait le tableau statique \u003Ccode>locales\u003C/code>. Deux sources de vérité, désynchronisées.\u003C/p>\n\u003Cp>\u003Cstrong>4. L'impact SEO a été graduel.\u003C/strong> Google n'invalide pas immédiatement un cluster hreflang cassé. Il recrawle les pages à son rythme, constate que le lien \u003Ccode>de\u003C/code> est mort, et commence à ignorer les signaux hreflang de la page. Sur un site de 12 000 pages avec un crawl budget correct, ce processus prend des semaines. Le temps que l'érosion soit visible dans les métriques, quatre mois se sont écoulés.\u003C/p>\n\u003Ch3>Ce que Googlebot voit vs ce que le développeur voit\u003C/h3>\n\u003Cp>Le développeur ouvre la page produit dans Chrome. Il voit une fiche produit qui fonctionne. Les hreflang sont dans le code source, mais ils ne sont pas rendus visuellement. Même en inspectant le \u003Ccode>&#x3C;head>\u003C/code>, il faut savoir qu'il faut chercher un domaine mort parmi les \u003Ccode>&#x3C;link>\u003C/code>.\u003C/p>\n\u003Cp>Googlebot, lui, fait exactement ceci :\u003C/p>\n\u003Col>\n\u003Cli>Crawle \u003Ccode>https://www.shop-marque.fr/produit/robe-ete-lin\u003C/code>.\u003C/li>\n\u003Cli>Parse le \u003Ccode>&#x3C;head>\u003C/code>, extrait les hreflang.\u003C/li>\n\u003Cli>Tente de valider le cluster : pour chaque URL hreflang, Google vérifie que la page cible contient un hreflang retour (reciprocal link).\u003C/li>\n\u003Cli>\u003Ccode>https://www.shop-marque.de/produkt/sommerkleid-leinen\u003C/code> → DNS failure. Pas de réponse HTTP. Pas de lien retour possible.\u003C/li>\n\u003Cli>Le cluster hreflang est considéré comme \u003Cstrong>invalide\u003C/strong>. Google ignore toutes les annotations hreflang de la page — y compris le lien \u003Ccode>fr\u003C/code> ↔ \u003Ccode>en\u003C/code> qui, lui, fonctionne parfaitement.\u003C/li>\n\u003C/ol>\n\u003Cp>C'est documenté dans la \u003Ca href=\"https://developers.google.com/search/docs/specialty/international/localized-versions\">documentation officielle Google sur les hreflang\u003C/a> : \"If the annotations are inconsistent, we may ignore them entirely.\"\u003C/p>\n\u003Cp>Le résultat concret : Google ne sait plus que \u003Ccode>shop-marque.fr\u003C/code> et \u003Ccode>shop-marque.com\u003C/code> sont des versions localisées du même contenu. Les pages .fr commencent à concurrencer les pages .com dans les SERPs françaises. Le trafic organique se fragmente.\u003C/p>\n\u003Ch3>Vérification à grande échelle avec Screaming Frog\u003C/h3>\n\u003Cp>Pour quantifier l'étendue du dégât, l'équipe lance un crawl complet avec Screaming Frog, en activant l'extraction custom :\u003C/p>\n\u003Cpre>\u003Ccode>Configuration > Custom Extraction\nExtraction: XPath\nXPath: //link[@rel='alternate' and @hreflang='de']/@href\n\u003C/code>\u003C/pre>\n\u003Cp>Export CSV. 12 347 lignes. Toutes contiennent \u003Ccode>https://www.shop-marque.de/...\u003C/code>. Un \u003Ccode>curl\u003C/code> rapide confirme que le domaine est mort :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -sI\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> https://www.shop-marque.de/produkt/sommerkleid-leinen\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># curl: (6) Could not resolve host: www.shop-marque.de\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>DNS NXDOMAIN. Le domaine a expiré et n'a pas été racheté. Il est disponible à l'achat chez n'importe quel registrar. Ce qui ouvre un autre risque : si un tiers rachète le domaine et y place du contenu, les hreflang du .fr et du .com pointeront vers un site tiers. Un scénario de détournement classique, rarement anticipé.\u003C/p>\n\u003Ch2>Le fix : supprimer, redéployer, revalider\u003C/h2>\n\u003Ch3>Étape 1 — Nettoyer la source de vérité\u003C/h3>\n\u003Cp>Le correctif minimal consiste à retirer la locale \u003Ccode>de\u003C/code> du tableau. Mais l'équipe prend une décision plus robuste : ajouter un flag \u003Ccode>active\u003C/code> pour gérer proprement les futures fermetures de marché.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// lib/i18n/config.ts — version corrigée\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> locales\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    code: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'fr'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    domain: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'www.shop-marque.fr'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    defaultLocale: \u003C/span>\u003Cspan style=\"color:#79B8FF\">false\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    contentfulLocale: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'fr-FR'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    active: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    code: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'de'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    domain: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'www.shop-marque.de'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    defaultLocale: \u003C/span>\u003Cspan style=\"color:#79B8FF\">false\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    contentfulLocale: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'de-DE'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    active: \u003C/span>\u003Cspan style=\"color:#79B8FF\">false\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#6A737D\">// Marché fermé 2026-02\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    code: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'en'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    domain: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'www.shop-marque.com'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    defaultLocale: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    contentfulLocale: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'en-US'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    active: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">as\u003C/span>\u003Cspan style=\"color:#F97583\"> const\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> activeLocales\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> locales.\u003C/span>\u003Cspan style=\"color:#B392F0\">filter\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">l\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> l.active);\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Ch3>Étape 2 — Filtrer les hreflang\u003C/h3>\n\u003Cp>Le composant \u003Ccode>HreflangTags\u003C/code> utilise désormais \u003Ccode>activeLocales\u003C/code> :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// app/layout.tsx — composant corrigé\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { activeLocales } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '@/lib/i18n/config'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">function\u003C/span>\u003Cspan style=\"color:#B392F0\"> HreflangTags\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({ \u003C/span>\u003Cspan style=\"color:#FFAB70\">slug\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">currentLocale\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> }\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#FFAB70\">slug\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; \u003C/span>\u003Cspan style=\"color:#FFAB70\">currentLocale\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> }) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    &#x3C;>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      {\u003C/span>\u003Cspan style=\"color:#FFAB70\">activeLocales\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#FFAB70\">map\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">locale\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        &#x3C;\u003C/span>\u003Cspan style=\"color:#FFAB70\">link\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          key\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{locale.code}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          rel\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"alternate\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          hrefLang\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{locale.code}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          href\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`https://${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">locale\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">domain\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}/${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      ))}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#FFAB70\">link\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        rel\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"alternate\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        hrefLang\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"x-default\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        href\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`https://${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">activeLocales\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#B392F0\">find\u003C/span>\u003Cspan style=\"color:#9ECBFF\">(\u003C/span>\u003Cspan style=\"color:#79B8FF\">l\u003C/span>\u003Cspan style=\"color:#F97583\"> =>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> l\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">defaultLocale\u003C/span>\u003Cspan style=\"color:#9ECBFF\">)\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">domain\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}/${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    &#x3C;/>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le HTML rendu après correctif :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">link\u003C/span>\u003Cspan style=\"color:#B392F0\"> rel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"alternate\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> hreflang\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"fr\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> href\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"https://www.shop-marque.fr/produit/robe-ete-lin\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">link\u003C/span>\u003Cspan style=\"color:#B392F0\"> rel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"alternate\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> hreflang\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"en\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> href\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"https://www.shop-marque.com/product/summer-linen-dress\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">link\u003C/span>\u003Cspan style=\"color:#B392F0\"> rel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"alternate\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> hreflang\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"x-default\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> href\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"https://www.shop-marque.com/product/summer-linen-dress\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Plus de ligne \u003Ccode>de\u003C/code>. Le cluster hreflang ne contient plus que des domaines actifs avec des liens réciproques fonctionnels.\u003C/p>\n\u003Ch3>Étape 3 — Vérifier les sitemaps\u003C/h3>\n\u003Cp>Le sitemap index référençait encore un \u003Ccode>sitemap-de.xml\u003C/code>. Même logique : le générateur de sitemap lisait le même tableau \u003Ccode>locales\u003C/code>. Après le fix, le sitemap index ne liste plus que les sitemaps \u003Ccode>fr\u003C/code> et \u003Ccode>en\u003C/code>. L'ancien \u003Ccode>sitemap-de.xml\u003C/code> retourne désormais un 404, ce qui est le comportement attendu — Google cessera de le requêter après quelques tentatives. Un problème similaire de sitemap pointant vers un mauvais domaine avait été documenté dans un \u003Ca href=\"/blog/astro-site-config-build-local-le-sitemap-entier-pointe-vers-un-domaine-inexistant\">incident Astro avec un domaine inexistant\u003C/a>.\u003C/p>\n\u003Ch3>Étape 4 — Invalider les caches et forcer le recrawl\u003C/h3>\n\u003Cp>Le site utilise un CDN avec cache edge de 24h. Un simple redéploiement ne suffit pas — les anciennes pages avec hreflang cassés resteraient servies pendant 24h.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Purge du cache CDN (Cloudflare)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -X\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> POST\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  -H\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"Authorization: Bearer {token}\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  -H\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"Content-Type: application/json\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  --data\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '{\"purge_everything\":true}'\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Ensuite, soumission du sitemap mis à jour dans Search Console, et demande d'inspection d'URL sur 10 pages stratégiques pour vérifier que Google voit bien le nouveau \u003Ccode>&#x3C;head>\u003C/code>.\u003C/p>\n\u003Ch3>Étape 5 — Ajouter des tests\u003C/h3>\n\u003Cp>L'équipe ajoute un test d'intégration dans la CI qui vérifie que chaque URL hreflang dans le \u003Ccode>&#x3C;head>\u003C/code> pointe vers un domaine résolvable :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// __tests__/hreflang.test.ts\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { activeLocales } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '@/lib/i18n/config'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> dns \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'dns/promises'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">describe\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'hreflang domains'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, () \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  it.\u003C/span>\u003Cspan style=\"color:#B392F0\">each\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(activeLocales)(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'$code domain $domain resolves'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#F97583\">async\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#FFAB70\">locale\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> result\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> dns.\u003C/span>\u003Cspan style=\"color:#B392F0\">lookup\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(locale.domain).\u003C/span>\u003Cspan style=\"color:#B392F0\">catch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(() \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#79B8FF\"> null\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(result).not.\u003C/span>\u003Cspan style=\"color:#B392F0\">toBeNull\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  it\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'no inactive locale appears in activeLocales'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, () \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> inactive\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> activeLocales.\u003C/span>\u003Cspan style=\"color:#B392F0\">filter\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">l\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#F97583\"> !\u003C/span>\u003Cspan style=\"color:#E1E4E8\">l.active);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(inactive).\u003C/span>\u003Cspan style=\"color:#B392F0\">toHaveLength\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">});\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Ce test échouera en CI si quelqu'un ajoute une locale avec un domaine mort, ou si un domaine actif expire sans que l'équipe s'en aperçoive.\u003C/p>\n\u003Ch3>Temps de récupération\u003C/h3>\n\u003Cp>Le fix a été déployé un jeudi. Les premiers signes de récupération dans Search Console sont apparus au bout de 8 jours — le temps que Google recrawle une fraction significative des 12 000 pages et revalide les clusters hreflang. La récupération complète du trafic organique a pris 23 jours. Le trafic .fr est revenu à son niveau d'avant l'érosion. Le trafic .com sur les requêtes françaises a cessé de cannibaliser le .fr.\u003C/p>\n\u003Cp>Le domaine \u003Ccode>shop-marque.de\u003C/code> a été racheté par l'équipe pour éviter tout détournement futur, même sans intention de le réutiliser. Coût : 9€/an. Assurance contre un risque de réputation potentiellement catastrophique.\u003C/p>\n\u003Cp>L'incident a aussi révélé que les \u003Ca href=\"/blog/crowdin-auto-translate-injecte-lang-auto-signal-de-langue-casse-sur-tout-le-site\">signaux de langue cassés\u003C/a> ne se limitent pas aux attributs \u003Ccode>lang\u003C/code> — les hreflang sont un vecteur de régression tout aussi silencieux. Et comme pour les \u003Ca href=\"/blog/contentful-le-champ-seo-title-non-sync-vers-next-js-fallback-first-h1-genere\">métadonnées SEO qui se désynchronisent entre CMS et framework\u003C/a>, la cause racine est toujours la même : deux sources de vérité qui divergent sans que personne ne monitore la divergence.\u003C/p>\n\u003Ch2>Ce qu'on en retient\u003C/h2>\n\u003Cp>Fermer un marché, c'est un projet business. Nettoyer les artefacts techniques de ce marché — hreflang, sitemaps, redirections, structured data —, c'est un projet technique qui n'apparaît dans aucun ticket JIRA si personne ne le demande explicitement.\u003C/p>\n\u003Cp>Trois règles à graver :\u003C/p>\n\u003Cul>\n\u003Cli>Chaque locale doit avoir un flag \u003Ccode>active\u003C/code>. Supprimer une entrée d'un tableau, c'est perdre la trace de son existence. La désactiver, c'est documenter la décision.\u003C/li>\n\u003Cli>Les hreflang doivent être testés en CI. Un test DNS sur chaque domaine cible prend 200ms et évite des semaines de régression silencieuse.\u003C/li>\n\u003Cli>Les domaines fermés doivent être conservés. 9€/an, contre le risque qu'un tiers récupère le domaine et hérite de signaux hreflang pointant depuis un site à forte autorité.\u003C/li>\n\u003C/ul>\n\u003Cp>Un outil de monitoring continu comme Seogard détecte ce type de hreflang cassé dès le premier crawl post-déploiement — pas quatre mois plus tard, quand la courbe de trafic a déjà décroché.\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,12,[18,10,19,20],"hreflang","cleanup","domains","Hreflang vers domaines supprimés : −38% de trafic DE en 6 semaines","Fri Jun 19 2026 06:01:46 GMT+0000 (Coordinated Universal Time)",[24,37,52,67,82,97],{"_id":25,"slug":26,"__v":6,"author":7,"canonical":27,"category":10,"createdAt":28,"date":29,"description":30,"image":15,"imageAlt":15,"readingTime":16,"tags":31,"title":35,"updatedAt":36},"6a3389c9aa6b273b0c5c95c6","crowdin-auto-translate-injecte-lang-auto-signal-de-langue-casse-sur-tout-le-site","https://seogard.io/blog/crowdin-auto-translate-injecte-lang-auto-signal-de-langue-casse-sur-tout-le-site","2026-06-18T06:01:45.023Z","2026-06-18","Crowdin auto-translate injecte lang=\\\"auto\\\" sur tout le site. Google confond le marché cible. Récit, diagnostic et fix complet.",[32,10,33,34],"crowdin","lang","translation","Crowdin lang=\\\"auto\\\" : signal de langue cassé, −34 % trafic","Thu Jun 18 2026 06:01:45 GMT+0000 (Coordinated Universal Time)",{"_id":38,"slug":39,"__v":6,"author":7,"canonical":40,"category":41,"createdAt":42,"date":29,"description":43,"image":15,"imageAlt":15,"readingTime":16,"tags":44,"title":50,"updatedAt":51},"6a34169aaa6b273b0ccfca84","for-site-moves-specify-all-domain-variants-with-google-s-change-of-address-tool","https://seogard.io/blog/for-site-moves-specify-all-domain-variants-with-google-s-change-of-address-tool","Actualités SEO","2026-06-18T16:02:34.751Z","Google clarifie sa doc sur les site moves : chaque variante de domaine doit être déclarée dans le Change of Address tool. Guide technique complet.",[45,46,47,48,49],"site move","domain variants","Change of Address","Search Console","migration SEO","Change of Address : déclarer toutes les variantes de domaine","Thu Jun 18 2026 16:02:34 GMT+0000 (Coordinated Universal Time)",{"_id":53,"slug":54,"__v":6,"author":7,"canonical":55,"category":41,"createdAt":56,"date":57,"description":58,"image":15,"imageAlt":15,"readingTime":16,"tags":59,"title":65,"updatedAt":66},"6a3238a5aa6b273b0c4e69eb","bing-rolls-out-ai-citation-share-in-webmaster-tools-via-sejournal-mattgsouthern","https://seogard.io/blog/bing-rolls-out-ai-citation-share-in-webmaster-tools-via-sejournal-mattgsouthern","2026-06-17T06:03:17.202Z","2026-06-17","Analyse technique du nouveau Citation Share dans Bing Webmaster Tools. Métriques AI, impact sur le trafic, et stratégies d'optimisation concrètes.",[60,61,62,63,64],"bing","citation share","webmaster tools","AI search","SEO technique","Bing AI Citation Share : ce que ça change pour le SEO technique","Wed Jun 17 2026 06:03:17 GMT+0000 (Coordinated Universal Time)",{"_id":68,"slug":69,"__v":6,"author":7,"canonical":70,"category":71,"createdAt":72,"date":57,"description":73,"image":15,"imageAlt":15,"readingTime":74,"tags":75,"title":80,"updatedAt":81},"6a32c4e8aa6b273b0cbed17b","storyblok-redirections-en-custom-field-oubliees-au-passage-nouveau-plan","https://seogard.io/blog/storyblok-redirections-en-custom-field-oubliees-au-passage-nouveau-plan","Headless","2026-06-17T16:01:44.888Z","Un site e-commerce perd 1 200 redirections stockées en custom field Storyblok lors d'un upgrade de plan. Récit, diagnostic et fix complet.",11,[76,77,78,79],"storyblok","redirects","migration","cms","Storyblok : redirections custom perdues après changement de plan","Wed Jun 17 2026 16:01:44 GMT+0000 (Coordinated Universal Time)",{"_id":83,"slug":84,"__v":6,"author":7,"canonical":85,"category":71,"createdAt":86,"date":87,"description":88,"image":15,"imageAlt":15,"readingTime":74,"tags":89,"title":95,"updatedAt":96},"6a30e6c9aa6b273b0c3f8791","sanity-preview-mode-actif-en-prod-indexation-de-drafts-non-publies","https://seogard.io/blog/sanity-preview-mode-actif-en-prod-indexation-de-drafts-non-publies","2026-06-16T06:01:45.128Z","2026-06-16","Un site Next.js + Sanity garde la preview API key en production. Googlebot indexe 340 drafts non publiés. Récit, diagnostic et fix complet.",[90,91,92,93,94],"sanity","preview","drafts","indexation","next.js","Sanity preview mode en prod : drafts indexés par Google","Tue Jun 16 2026 06:01:45 GMT+0000 (Coordinated Universal Time)",{"_id":98,"slug":99,"__v":6,"author":7,"canonical":100,"category":71,"createdAt":101,"date":87,"description":102,"image":15,"imageAlt":15,"readingTime":16,"tags":103,"title":109,"updatedAt":110},"6a317369aa6b273b0cb03aa2","strapi-public-role-bloque-l-api-lecture-ssr-void-googlebot-voit-page-blanche","https://seogard.io/blog/strapi-public-role-bloque-l-api-lecture-ssr-void-googlebot-voit-page-blanche","2026-06-16T16:01:45.497Z","Un seed Strapi écrase le rôle Public. L'API renvoie 403, le SSR sert du vide. Récit complet : diagnostic, fix, récupération SEO en 19 jours.",[104,105,106,107,108],"strapi","permissions","ssr","api","headless-cms","Strapi public role 403 : SSR vide, Googlebot indexe du blanc","Tue Jun 16 2026 16:01:45 GMT+0000 (Coordinated Universal Time)",{"categories":112},[113,116,119,123,127,130,134,137,140,144,148,151,154,158,161,164,167,170,172,176],{"category":41,"slug":114,"count":115},"actualites-seo",170,{"category":117,"slug":78,"count":118},"Migration",18,{"category":120,"slug":121,"count":122},"Rendering","rendering",9,{"category":124,"slug":125,"count":126},"Framework","framework",8,{"category":128,"slug":129,"count":126},"Performance","performance",{"category":131,"slug":132,"count":133},"Meta Tags","meta-tags",7,{"category":135,"slug":136,"count":133},"Crawl","crawl",{"category":138,"slug":139,"count":133},"SEO Technique","seo-technique",{"category":141,"slug":142,"count":143},"Architecture","architecture",6,{"category":145,"slug":146,"count":147},"JavaScript SEO","javascript-seo",5,{"category":149,"slug":150,"count":147},"Monitoring","monitoring",{"category":152,"slug":153,"count":147},"Structured Data","structured-data",{"category":155,"slug":156,"count":157},"Refonte","refonte",4,{"category":159,"slug":160,"count":157},"Outils","outils",{"category":162,"slug":163,"count":157},"Redirections","redirections",{"category":165,"slug":166,"count":157},"E-commerce","e-commerce",{"category":168,"slug":169,"count":157},"Avancé","avance",{"category":71,"slug":171,"count":157},"headless",{"category":173,"slug":174,"count":175},"Contenu","contenu",3,{"category":177,"slug":178,"count":175},"IA & SEO","ia-seo"]