[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fxrfEIsaqfvDedVuXdGrc3TaryXyR8BzRl7OylkoatKY":3,"$f-plgjIuA5nhQ7mDO_O7AV-L4Qcq7mE0LJ63_3ndIFOo":23,"$fcTFjTaPPT1sVcnh1wWEGoTWMSaOD4bwmwntkMae6w5s":109},{"_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},"6a22f2ebaa6b273b0cc9fc68","mode-sombre-prefers-color-scheme-injecte-un-meta-noindex-par-erreur",0,"Equipe Seogard","# Mode sombre : quand prefers-color-scheme injecte un meta noindex par erreur\n\nJeudi 16h40. Un développeur frontend merge une PR intitulée \"feat: dark mode support — prefers-color-scheme\". Trois reviewers ont approuvé. Le pipeline CI passe. Le Lighthouse score reste à 96. Le site — une marketplace B2B française de 8 400 pages — affiche un mode sombre impeccable. Dix-huit jours plus tard, Search Console signale que 1 247 pages portent une directive `noindex`. Personne n'a touché aux meta robots depuis six mois.\n\n## Lundi 9h12 — L'alerte que personne ne comprend\n\nLe lead SEO ouvre Search Console le lundi matin. Routine hebdomadaire. L'onglet \"Pages\" affiche un pic dans la catégorie \"Exclue — Page avec balise 'noindex'\". Le compteur est passé de 14 (les pages légitimement exclues — CGV, politique de confidentialité, pages de compte) à 1 261.\n\nPremier réflexe : vérifier si un déploiement a touché le fichier `robots.txt` ou les composants de `\u003Chead>`. Git blame sur les trois dernières semaines. Rien. Le `robots.txt` n'a pas bougé depuis quatre mois. Le composant `\u003CMetaTags>` non plus.\n\n9h38 — Le lead SEO ouvre une page produit dans Chrome, `View Source`. Pas de `noindex`. Il inspecte le DOM avec DevTools. Pas de `noindex` non plus. Il vérifie cinq pages. Dix pages. Aucune trace.\n\n9h52 — Hypothèse : faux positif de Search Console, un bug d'indexation côté Google. L'équipe a déjà vécu ça sur des sous-domaines staging. Le CTO propose d'attendre 48 heures.\n\nMercredi 11h — Le compteur monte à 1 247 pages exclues. Le trafic organique a perdu 31 % sur les fiches produit en deux semaines. −74K clics sur la période. L'attente n'est plus une option.\n\n13h15 — Un dev senior lance Screaming Frog sur un échantillon de 500 URLs. Configuration par défaut : user-agent Googlebot, rendering JavaScript activé. Résultat : 0 page avec `noindex`. Screaming Frog ne voit rien.\n\n13h40 — Le lead SEO utilise l'outil d'inspection d'URL de Search Console sur une fiche produit spécifique. Le rendu HTML live affiche clairement :\n\n```html\n\u003Cmeta name=\"robots\" content=\"noindex\">\n```\n\nLe tag est là. Dans le `\u003Chead>`. Mais invisible dans le navigateur classique, invisible dans Screaming Frog, invisible dans `curl`. Le lead SEO prend une capture d'écran, la poste dans le canal Slack `#incident-seo`. Silence pendant dix secondes. Puis le CTO : \"Comment c'est possible ?\"\n\n14h02 — L'équipe comprend que le bug ne se manifeste que dans un contexte de rendu bien précis. Pas un bug mineur. Un fantôme dans le `\u003Chead>`.\n\n## Le bug : un CSS-in-JS qui écrit du HTML dans une media query\n\nLe site utilise React 18 avec Next.js 14 (App Router) et Styled Components v6 pour le styling. La PR \"dark mode\" a introduit un composant `ThemeProvider` qui injecte des styles globaux conditionnels via `createGlobalStyle`.\n\nVoici le code mergé (simplifié mais fidèle à la structure réelle) :\n\n```tsx\n// theme/GlobalStyles.tsx\nimport { createGlobalStyle } from 'styled-components';\n\nexport const GlobalStyles = createGlobalStyle`\n  body {\n    background-color: ${({ theme }) => theme.colors.background};\n    color: ${({ theme }) => theme.colors.text};\n  }\n\n  @media (prefers-color-scheme: dark) {\n    body {\n      background-color: #1a1a2e;\n      color: #e0e0e0;\n    }\n    ${({ theme }) => theme.darkModeMetaOverride || ''}\n  }\n`;\n```\n\nLe problème est dans `theme.darkModeMetaOverride`. Ce champ a été ajouté au thème par un autre développeur — celui qui travaillait sur l'intégration d'un outil tiers d'analytics. Il voulait pouvoir injecter dynamiquement des fragments dans le `\u003Chead>` depuis la configuration du thème. Son intention : permettre à l'équipe marketing de désactiver l'indexation de certaines variantes de pages A/B testées en mode sombre.\n\nVoici ce qu'il a ajouté dans la configuration du thème :\n\n```tsx\n// theme/config.ts\nexport const darkTheme = {\n  colors: {\n    background: '#1a1a2e',\n    text: '#e0e0e0',\n    primary: '#6c63ff',\n  },\n  darkModeMetaOverride: '\u003Cmeta name=\"robots\" content=\"noindex\">',\n};\n\nexport const lightTheme = {\n  colors: {\n    background: '#ffffff',\n    text: '#1a1a2e',\n    primary: '#6c63ff',\n  },\n  darkModeMetaOverride: '',\n};\n```\n\nDans le thème clair, `darkModeMetaOverride` est une chaîne vide. Dans le thème sombre, c'est une balise `\u003Cmeta>` brute.\n\nLe développeur pensait que cette chaîne serait injectée dans le `\u003Chead>` via un composant React dédié, comme `next/head` ou un `Helmet`. Mais elle finissait dans `createGlobalStyle` — un template literal qui produit une balise `\u003Cstyle>`. Le contenu brut `\u003Cmeta name=\"robots\" content=\"noindex\">` se retrouvait donc à l'intérieur d'un bloc `\u003Cstyle>`.\n\n### Ce que voit un navigateur\n\nLe navigateur parse le HTML. Il rencontre la balise `\u003Cstyle>`. À l'intérieur, il trouve du CSS valide puis un fragment `\u003Cmeta ...>`. Le parser CSS ignore silencieusement ce fragment invalide. Le navigateur ne crée aucun nœud DOM pour ce `\u003Cmeta>`. Résultat : aucun `noindex` visible dans l'inspecteur DOM, aucun effet sur la page.\n\n### Ce que voit Googlebot\n\nGooglebot utilise un rendering basé sur Chrome headless (WRS — Web Rendering Service), mais il effectue aussi un parsing du HTML brut avant le rendu JavaScript complet. Lors de cette phase de pre-parsing, le HTML brut côté serveur (SSR) contient ceci :\n\n```html\n\u003Chead>\n  \u003Cmeta charset=\"utf-8\">\n  \u003Ctitle>Produit XYZ — Marketplace\u003C/title>\n  \u003Cmeta name=\"description\" content=\"...\">\n  \u003Cstyle data-styled=\"active\">\n    body{background-color:#ffffff;color:#1a1a2e;}\n    @media (prefers-color-scheme: dark){\n      body{background-color:#1a1a2e;color:#e0e0e0;}\n      \u003Cmeta name=\"robots\" content=\"noindex\">\n    }\n  \u003C/style>\n\u003C/head>\n```\n\nLe parser HTML de Googlebot — conforme à la [spécification HTML5](https://html.spec.whatwg.org/multipage/parsing.html) — traite le contenu de `\u003Cstyle>` comme du texte brut (*raw text element*). Mais lors du pre-processing des directives d'indexation, Google extrait les balises `\u003Cmeta name=\"robots\">` par pattern matching sur le HTML brut **avant** le parsing DOM complet. Le fragment `\u003Cmeta name=\"robots\" content=\"noindex\">` est détecté et appliqué, même s'il est techniquement à l'intérieur d'une balise `\u003Cstyle>`.\n\nCe comportement est documenté de manière oblique dans la [documentation Google sur les directives robots](https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag) : Google recommande de placer les meta robots dans le `\u003Chead>`, mais le crawler scanne l'intégralité du document pour détecter ces directives.\n\n### Pourquoi les tests n'ont rien détecté\n\nQuatre raisons :\n\n1. **Le navigateur masque le bug.** Chrome, Firefox, Safari — tous ignorent les balises HTML invalides à l'intérieur de `\u003Cstyle>`. Le DOM résultant ne contient pas de `\u003Cmeta noindex>`. Inspecter la page dans DevTools ne montre rien.\n\n2. **Screaming Frog en mode rendering utilise un headless Chrome standard.** Il parse le DOM final, pas le HTML brut SSR. Même résultat : pas de `noindex` visible.\n\n3. **Le thème clair est le défaut SSR.** Le serveur Next.js rend la page avec `lightTheme` par défaut. `darkModeMetaOverride` est une chaîne vide en thème clair. Mais `createGlobalStyle` de Styled Components injecte **les deux blocs media query** dans le HTML SSR — le bloc `prefers-color-scheme: dark` inclut la chaîne du `darkTheme`, pas du `lightTheme`. La résolution du thème pour les media queries CSS se fait au niveau du template literal, pas au niveau du state React.\n\n4. **Les tests unitaires et e2e ne vérifient pas le HTML brut SSR.** Les tests Cypress et Jest/RTL interrogent le DOM. Le `\u003Cmeta>` fantôme n'existe pas dans le DOM.\n\nPour reproduire le problème, il fallait inspecter le HTML brut servi par le serveur :\n\n```bash\ncurl -s -H \"User-Agent: Googlebot\" https://marketplace.example.com/produit/xyz \\\n  | grep -i \"noindex\"\n```\n\nRésultat :\n\n```\n      \u003Cmeta name=\"robots\" content=\"noindex\">\n```\n\nPrésent. En plein milieu d'un bloc `\u003Cstyle>`. Un `curl` simple suffit. Mais personne n'avait pensé à chercher un `noindex` dans une feuille de style.\n\nLa divergence entre ce que voit le développeur et ce que voit Googlebot est exactement le type de piège décrit dans le récit d'une [migration Next.js Pages Router vers App Router où les metadata étaient ignorées sur les pages client](/blog/migration-next-js-pages-router-vers-app-router-les-metadata-ignorees-sur-les-pages-client). Le SSR produit un HTML que personne ne lit — et c'est précisément celui que Google lit en premier.\n\n## Le fix : supprimer, valider, purger\n\n### Étape 1 — Supprimer le vecteur d'injection\n\nLe correctif immédiat : retirer `darkModeMetaOverride` du thème et du `createGlobalStyle`.\n\n```tsx\n// theme/GlobalStyles.tsx — APRÈS CORRECTION\nimport { createGlobalStyle } from 'styled-components';\n\nexport const GlobalStyles = createGlobalStyle`\n  body {\n    background-color: ${({ theme }) => theme.colors.background};\n    color: ${({ theme }) => theme.colors.text};\n  }\n\n  @media (prefers-color-scheme: dark) {\n    body {\n      background-color: #1a1a2e;\n      color: #e0e0e0;\n    }\n  }\n`;\n```\n\n```tsx\n// theme/config.ts — APRÈS CORRECTION\nexport const darkTheme = {\n  colors: {\n    background: '#1a1a2e',\n    text: '#e0e0e0',\n    primary: '#6c63ff',\n  },\n  // darkModeMetaOverride supprimé\n};\n```\n\nSi l'équipe marketing a besoin de contrôler l'indexation de variantes A/B, cela doit passer par un composant React dédié dans le `\u003Chead>`, jamais par une interpolation dans un template CSS. Le récit d'un [A/B test header servant un noindex à 50 % du trafic pendant 9 jours](/blog/a-b-test-header-la-variante-b-sert-un-noindex-a-50-du-trafic-pendant-9-jours) illustre exactement ce même anti-pattern dans un autre contexte.\n\n### Étape 2 — Valider le HTML SSR avant déploiement\n\nAjout d'un test d'intégration dans la CI qui vérifie le HTML brut SSR :\n\n```ts\n// __tests__/ssr-noindex-guard.test.ts\nimport { describe, it, expect } from 'vitest';\n\ndescribe('SSR HTML safety checks', () => {\n  it('should not contain noindex in style tags', async () => {\n    const res = await fetch('http://localhost:3000/produit/xyz');\n    const html = await res.text();\n\n    // Extraire le contenu de toutes les balises \u003Cstyle>\n    const styleBlocks = html.match(/\u003Cstyle[^>]*>([\\s\\S]*?)\u003C\\/style>/gi) || [];\n\n    for (const block of styleBlocks) {\n      expect(block.toLowerCase()).not.toContain('noindex');\n      expect(block.toLowerCase()).not.toContain('\u003Cmeta');\n    }\n  });\n\n  it('should not contain any meta tag outside of \u003Chead>', async () => {\n    const res = await fetch('http://localhost:3000/produit/xyz');\n    const html = await res.text();\n\n    const headEnd = html.indexOf('\u003C/head>');\n    const bodyContent = html.slice(headEnd);\n    const strayMetas = bodyContent.match(/\u003Cmeta\\s+name=[\"']robots[\"'][^>]*>/gi) || [];\n\n    expect(strayMetas).toHaveLength(0);\n  });\n});\n```\n\nCe test tourne sur chaque PR via un serveur Next.js éphémère dans le pipeline CI (GitHub Actions). Il attrape toute injection accidentelle de balises HTML dans des blocs `\u003Cstyle>`, `\u003Cscript>`, ou dans le `\u003Cbody>`.\n\n### Étape 3 — Forcer le re-crawl\n\nAprès déploiement du fix :\n\n1. Soumission d'un sitemap mis à jour dans Search Console avec `lastmod` actualisé sur toutes les fiches produit.\n2. Utilisation de l'API d'indexation Google pour soumettre les 50 pages à plus fort trafic en priorité.\n3. Invalidation du cache CDN (Vercel, dans ce cas) pour garantir que Googlebot reçoit le HTML corrigé dès le prochain passage.\n\n```bash\n# Invalider le cache Vercel sur les routes produit\nnpx vercel --force --scope=marketplace-prod \\\n  && curl -X PURGE \"https://marketplace.example.com/produit/*\"\n```\n\n### Temps de récupération\n\n- **J+3** : les 50 pages soumises via l'API d'indexation repassent en `index, follow` dans Search Console.\n- **J+7** : 60 % des 1 247 pages exclues sont réindexées.\n- **J+14** : 95 % des pages récupèrent leur statut indexé. Les 5 % restantes sont des fiches produit à faible PageRank, crawlées moins souvent.\n- **J+21** : le trafic organique revient à son niveau d'avant l'incident. Total de l'impact estimé : −112K clics sur 21 jours.\n\nLe schéma de récupération est cohérent avec ce qui a été observé lors d'[une migration Nuxt 2 vers Nuxt 3 où 200 pages sont restées en fallback layout pendant 6 semaines](/blog/migration-nuxt-2-vers-nuxt-3-200-pages-en-fallback-layout-pendant-6-semaines) : les pages à forte autorité reviennent vite, les pages orphelines traînent.\n\n### Gardes-fous ajoutés post-incident\n\n1. **Lint rule custom** : un plugin ESLint qui interdit les chaînes contenant `\u003Cmeta` ou `\u003Clink` dans les fichiers de configuration de thème et dans les template literals de `createGlobalStyle`, `css`, et `styled`.\n2. **Alerte Search Console** : un script cron quotidien qui requête l'API Search Console et déclenche une alerte Slack si le compteur de pages `noindex` dépasse le seuil connu (14 pages légitimes + marge de 5).\n3. **Revue SSR obligatoire** : toute PR touchant le `\u003Chead>`, le thème, ou les styles globaux doit inclure un `curl` du HTML SSR dans la description de PR. Pas de merge sans cette preuve.\n\nLe [récit du design system dont le composant heading rendait des `\u003Cdiv>` au lieu de `\u003Ch1>`](/blog/design-system-composant-heading-qui-rend-div-selon-la-prop-as-mal-configuree) montre le même angle mort : le composant fait ce qu'on lui demande, mais ce qu'on lui demande n'est pas ce que Google attend.\n\n## Ce qu'on en retient\n\nLe CSS-in-JS est puissant. Les template literals acceptent n'importe quelle chaîne. C'est leur force — et leur surface d'attaque SEO. Une interpolation de thème dans un `createGlobalStyle` peut injecter du HTML brut dans une balise `\u003Cstyle>`. Le navigateur l'ignore. Googlebot ne l'ignore pas.\n\nLe diagnostic a pris 18 jours parce que personne n'inspecte le HTML SSR brut. Les DevTools montrent le DOM, pas le source. Screaming Frog rend le JavaScript, pas le pré-parse. Seul un `curl` — ou un monitoring continu type Seogard qui compare le HTML SSR aux directives d'indexation effectives — aurait détecté la divergence en quelques minutes au lieu de trois semaines.\n\nRègle simple : aucune donnée de configuration ne devrait contenir de balise HTML. Si un champ de thème peut finir dans un template literal CSS, il finira dans le HTML servi. Et si Google le voit, Google l'applique.\n```","https://seogard.io/blog/mode-sombre-prefers-color-scheme-injecte-un-meta-noindex-par-erreur","Refonte","2026-06-05T16:01:47.389Z","2026-06-05","Un CSS-in-JS mal configuré injecte un meta noindex via prefers-color-scheme. Récit de l'incident, diagnostic technique, et correctif complet.","\u003Ch1>Mode sombre : quand prefers-color-scheme injecte un meta noindex par erreur\u003C/h1>\n\u003Cp>Jeudi 16h40. Un développeur frontend merge une PR intitulée \"feat: dark mode support — prefers-color-scheme\". Trois reviewers ont approuvé. Le pipeline CI passe. Le Lighthouse score reste à 96. Le site — une marketplace B2B française de 8 400 pages — affiche un mode sombre impeccable. Dix-huit jours plus tard, Search Console signale que 1 247 pages portent une directive \u003Ccode>noindex\u003C/code>. Personne n'a touché aux meta robots depuis six mois.\u003C/p>\n\u003Ch2>Lundi 9h12 — L'alerte que personne ne comprend\u003C/h2>\n\u003Cp>Le lead SEO ouvre Search Console le lundi matin. Routine hebdomadaire. L'onglet \"Pages\" affiche un pic dans la catégorie \"Exclue — Page avec balise 'noindex'\". Le compteur est passé de 14 (les pages légitimement exclues — CGV, politique de confidentialité, pages de compte) à 1 261.\u003C/p>\n\u003Cp>Premier réflexe : vérifier si un déploiement a touché le fichier \u003Ccode>robots.txt\u003C/code> ou les composants de \u003Ccode>&#x3C;head>\u003C/code>. Git blame sur les trois dernières semaines. Rien. Le \u003Ccode>robots.txt\u003C/code> n'a pas bougé depuis quatre mois. Le composant \u003Ccode>&#x3C;MetaTags>\u003C/code> non plus.\u003C/p>\n\u003Cp>9h38 — Le lead SEO ouvre une page produit dans Chrome, \u003Ccode>View Source\u003C/code>. Pas de \u003Ccode>noindex\u003C/code>. Il inspecte le DOM avec DevTools. Pas de \u003Ccode>noindex\u003C/code> non plus. Il vérifie cinq pages. Dix pages. Aucune trace.\u003C/p>\n\u003Cp>9h52 — Hypothèse : faux positif de Search Console, un bug d'indexation côté Google. L'équipe a déjà vécu ça sur des sous-domaines staging. Le CTO propose d'attendre 48 heures.\u003C/p>\n\u003Cp>Mercredi 11h — Le compteur monte à 1 247 pages exclues. Le trafic organique a perdu 31 % sur les fiches produit en deux semaines. −74K clics sur la période. L'attente n'est plus une option.\u003C/p>\n\u003Cp>13h15 — Un dev senior lance Screaming Frog sur un échantillon de 500 URLs. Configuration par défaut : user-agent Googlebot, rendering JavaScript activé. Résultat : 0 page avec \u003Ccode>noindex\u003C/code>. Screaming Frog ne voit rien.\u003C/p>\n\u003Cp>13h40 — Le lead SEO utilise l'outil d'inspection d'URL de Search Console sur une fiche produit spécifique. Le rendu HTML live affiche clairement :\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\">meta\u003C/span>\u003Cspan style=\"color:#B392F0\"> name\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"robots\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> content\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"noindex\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le tag est là. Dans le \u003Ccode>&#x3C;head>\u003C/code>. Mais invisible dans le navigateur classique, invisible dans Screaming Frog, invisible dans \u003Ccode>curl\u003C/code>. Le lead SEO prend une capture d'écran, la poste dans le canal Slack \u003Ccode>#incident-seo\u003C/code>. Silence pendant dix secondes. Puis le CTO : \"Comment c'est possible ?\"\u003C/p>\n\u003Cp>14h02 — L'équipe comprend que le bug ne se manifeste que dans un contexte de rendu bien précis. Pas un bug mineur. Un fantôme dans le \u003Ccode>&#x3C;head>\u003C/code>.\u003C/p>\n\u003Ch2>Le bug : un CSS-in-JS qui écrit du HTML dans une media query\u003C/h2>\n\u003Cp>Le site utilise React 18 avec Next.js 14 (App Router) et Styled Components v6 pour le styling. La PR \"dark mode\" a introduit un composant \u003Ccode>ThemeProvider\u003C/code> qui injecte des styles globaux conditionnels via \u003Ccode>createGlobalStyle\u003C/code>.\u003C/p>\n\u003Cp>Voici le code mergé (simplifié mais fidèle à la structure réelle) :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// theme/GlobalStyles.tsx\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { createGlobalStyle } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'styled-components'\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\"> GlobalStyles\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> createGlobalStyle\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  body {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    background-color: ${\u003C/span>\u003Cspan style=\"color:#9ECBFF\">({ \u003C/span>\u003Cspan style=\"color:#79B8FF\">theme\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> }) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> theme\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">colors\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">background\u003C/span>\u003Cspan style=\"color:#9ECBFF\">};\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    color: ${\u003C/span>\u003Cspan style=\"color:#9ECBFF\">({ \u003C/span>\u003Cspan style=\"color:#79B8FF\">theme\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> }) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> theme\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">colors\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">text\u003C/span>\u003Cspan style=\"color:#9ECBFF\">};\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  @media (prefers-color-scheme: dark) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    body {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">      background-color: #1a1a2e;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">      color: #e0e0e0;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    ${\u003C/span>\u003Cspan style=\"color:#9ECBFF\">({ \u003C/span>\u003Cspan style=\"color:#79B8FF\">theme\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> }) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> theme\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">darkModeMetaOverride\u003C/span>\u003Cspan style=\"color:#F97583\"> ||\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> ''}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le problème est dans \u003Ccode>theme.darkModeMetaOverride\u003C/code>. Ce champ a été ajouté au thème par un autre développeur — celui qui travaillait sur l'intégration d'un outil tiers d'analytics. Il voulait pouvoir injecter dynamiquement des fragments dans le \u003Ccode>&#x3C;head>\u003C/code> depuis la configuration du thème. Son intention : permettre à l'équipe marketing de désactiver l'indexation de certaines variantes de pages A/B testées en mode sombre.\u003C/p>\n\u003Cp>Voici ce qu'il a ajouté dans la configuration du thème :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// theme/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\"> darkTheme\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  colors: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    background: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'#1a1a2e'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    text: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'#e0e0e0'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    primary: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'#6c63ff'\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\">  darkModeMetaOverride: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'&#x3C;meta name=\"robots\" content=\"noindex\">'\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:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> lightTheme\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  colors: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    background: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'#ffffff'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    text: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'#1a1a2e'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    primary: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'#6c63ff'\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\">  darkModeMetaOverride: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">''\u003C/span>\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>Dans le thème clair, \u003Ccode>darkModeMetaOverride\u003C/code> est une chaîne vide. Dans le thème sombre, c'est une balise \u003Ccode>&#x3C;meta>\u003C/code> brute.\u003C/p>\n\u003Cp>Le développeur pensait que cette chaîne serait injectée dans le \u003Ccode>&#x3C;head>\u003C/code> via un composant React dédié, comme \u003Ccode>next/head\u003C/code> ou un \u003Ccode>Helmet\u003C/code>. Mais elle finissait dans \u003Ccode>createGlobalStyle\u003C/code> — un template literal qui produit une balise \u003Ccode>&#x3C;style>\u003C/code>. Le contenu brut \u003Ccode>&#x3C;meta name=\"robots\" content=\"noindex\">\u003C/code> se retrouvait donc à l'intérieur d'un bloc \u003Ccode>&#x3C;style>\u003C/code>.\u003C/p>\n\u003Ch3>Ce que voit un navigateur\u003C/h3>\n\u003Cp>Le navigateur parse le HTML. Il rencontre la balise \u003Ccode>&#x3C;style>\u003C/code>. À l'intérieur, il trouve du CSS valide puis un fragment \u003Ccode>&#x3C;meta ...>\u003C/code>. Le parser CSS ignore silencieusement ce fragment invalide. Le navigateur ne crée aucun nœud DOM pour ce \u003Ccode>&#x3C;meta>\u003C/code>. Résultat : aucun \u003Ccode>noindex\u003C/code> visible dans l'inspecteur DOM, aucun effet sur la page.\u003C/p>\n\u003Ch3>Ce que voit Googlebot\u003C/h3>\n\u003Cp>Googlebot utilise un rendering basé sur Chrome headless (WRS — Web Rendering Service), mais il effectue aussi un parsing du HTML brut avant le rendu JavaScript complet. Lors de cette phase de pre-parsing, le HTML brut côté serveur (SSR) contient ceci :\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\">head\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\">meta\u003C/span>\u003Cspan style=\"color:#B392F0\"> charset\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"utf-8\"\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\">title\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Produit XYZ — Marketplace&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">title\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\">meta\u003C/span>\u003Cspan style=\"color:#B392F0\"> name\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"description\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> content\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"...\"\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\">style\u003C/span>\u003Cspan style=\"color:#B392F0\"> data-styled\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"active\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">    body\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{\u003C/span>\u003Cspan style=\"color:#79B8FF\">background-color\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\">#ffffff\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003Cspan style=\"color:#79B8FF\">color\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\">#1a1a2e\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    @media\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (prefers-color-scheme: dark){\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">      body\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{\u003C/span>\u003Cspan style=\"color:#79B8FF\">background-color\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\">#1a1a2e\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003Cspan style=\"color:#79B8FF\">color\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\">#e0e0e0\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\">meta\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> name=\"robots\" content=\"noindex\"\u003C/span>\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:#E1E4E8\">  &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">style\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">head\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le parser HTML de Googlebot — conforme à la \u003Ca href=\"https://html.spec.whatwg.org/multipage/parsing.html\">spécification HTML5\u003C/a> — traite le contenu de \u003Ccode>&#x3C;style>\u003C/code> comme du texte brut (\u003Cem>raw text element\u003C/em>). Mais lors du pre-processing des directives d'indexation, Google extrait les balises \u003Ccode>&#x3C;meta name=\"robots\">\u003C/code> par pattern matching sur le HTML brut \u003Cstrong>avant\u003C/strong> le parsing DOM complet. Le fragment \u003Ccode>&#x3C;meta name=\"robots\" content=\"noindex\">\u003C/code> est détecté et appliqué, même s'il est techniquement à l'intérieur d'une balise \u003Ccode>&#x3C;style>\u003C/code>.\u003C/p>\n\u003Cp>Ce comportement est documenté de manière oblique dans la \u003Ca href=\"https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag\">documentation Google sur les directives robots\u003C/a> : Google recommande de placer les meta robots dans le \u003Ccode>&#x3C;head>\u003C/code>, mais le crawler scanne l'intégralité du document pour détecter ces directives.\u003C/p>\n\u003Ch3>Pourquoi les tests n'ont rien détecté\u003C/h3>\n\u003Cp>Quatre raisons :\u003C/p>\n\u003Col>\n\u003Cli>\n\u003Cp>\u003Cstrong>Le navigateur masque le bug.\u003C/strong> Chrome, Firefox, Safari — tous ignorent les balises HTML invalides à l'intérieur de \u003Ccode>&#x3C;style>\u003C/code>. Le DOM résultant ne contient pas de \u003Ccode>&#x3C;meta noindex>\u003C/code>. Inspecter la page dans DevTools ne montre rien.\u003C/p>\n\u003C/li>\n\u003Cli>\n\u003Cp>\u003Cstrong>Screaming Frog en mode rendering utilise un headless Chrome standard.\u003C/strong> Il parse le DOM final, pas le HTML brut SSR. Même résultat : pas de \u003Ccode>noindex\u003C/code> visible.\u003C/p>\n\u003C/li>\n\u003Cli>\n\u003Cp>\u003Cstrong>Le thème clair est le défaut SSR.\u003C/strong> Le serveur Next.js rend la page avec \u003Ccode>lightTheme\u003C/code> par défaut. \u003Ccode>darkModeMetaOverride\u003C/code> est une chaîne vide en thème clair. Mais \u003Ccode>createGlobalStyle\u003C/code> de Styled Components injecte \u003Cstrong>les deux blocs media query\u003C/strong> dans le HTML SSR — le bloc \u003Ccode>prefers-color-scheme: dark\u003C/code> inclut la chaîne du \u003Ccode>darkTheme\u003C/code>, pas du \u003Ccode>lightTheme\u003C/code>. La résolution du thème pour les media queries CSS se fait au niveau du template literal, pas au niveau du state React.\u003C/p>\n\u003C/li>\n\u003Cli>\n\u003Cp>\u003Cstrong>Les tests unitaires et e2e ne vérifient pas le HTML brut SSR.\u003C/strong> Les tests Cypress et Jest/RTL interrogent le DOM. Le \u003Ccode>&#x3C;meta>\u003C/code> fantôme n'existe pas dans le DOM.\u003C/p>\n\u003C/li>\n\u003C/ol>\n\u003Cp>Pour reproduire le problème, il fallait inspecter le HTML brut servi par le serveur :\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\"> -s\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -H\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"User-Agent: Googlebot\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> https://marketplace.example.com/produit/xyz\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  |\u003C/span>\u003Cspan style=\"color:#B392F0\"> grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -i\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"noindex\"\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Résultat :\u003C/p>\n\u003Cpre>\u003Ccode>      &#x3C;meta name=\"robots\" content=\"noindex\">\n\u003C/code>\u003C/pre>\n\u003Cp>Présent. En plein milieu d'un bloc \u003Ccode>&#x3C;style>\u003C/code>. Un \u003Ccode>curl\u003C/code> simple suffit. Mais personne n'avait pensé à chercher un \u003Ccode>noindex\u003C/code> dans une feuille de style.\u003C/p>\n\u003Cp>La divergence entre ce que voit le développeur et ce que voit Googlebot est exactement le type de piège décrit dans le récit d'une \u003Ca href=\"/blog/migration-next-js-pages-router-vers-app-router-les-metadata-ignorees-sur-les-pages-client\">migration Next.js Pages Router vers App Router où les metadata étaient ignorées sur les pages client\u003C/a>. Le SSR produit un HTML que personne ne lit — et c'est précisément celui que Google lit en premier.\u003C/p>\n\u003Ch2>Le fix : supprimer, valider, purger\u003C/h2>\n\u003Ch3>Étape 1 — Supprimer le vecteur d'injection\u003C/h3>\n\u003Cp>Le correctif immédiat : retirer \u003Ccode>darkModeMetaOverride\u003C/code> du thème et du \u003Ccode>createGlobalStyle\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\">// theme/GlobalStyles.tsx — APRÈS CORRECTION\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { createGlobalStyle } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'styled-components'\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\"> GlobalStyles\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> createGlobalStyle\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  body {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    background-color: ${\u003C/span>\u003Cspan style=\"color:#9ECBFF\">({ \u003C/span>\u003Cspan style=\"color:#79B8FF\">theme\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> }) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> theme\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">colors\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">background\u003C/span>\u003Cspan style=\"color:#9ECBFF\">};\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    color: ${\u003C/span>\u003Cspan style=\"color:#9ECBFF\">({ \u003C/span>\u003Cspan style=\"color:#79B8FF\">theme\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> }) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> theme\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">colors\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">text\u003C/span>\u003Cspan style=\"color:#9ECBFF\">};\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  @media (prefers-color-scheme: dark) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    body {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">      background-color: #1a1a2e;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">      color: #e0e0e0;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// theme/config.ts — APRÈS CORRECTION\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\"> darkTheme\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  colors: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    background: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'#1a1a2e'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    text: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'#e0e0e0'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    primary: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'#6c63ff'\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:#6A737D\">  // darkModeMetaOverride supprimé\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">};\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Si l'équipe marketing a besoin de contrôler l'indexation de variantes A/B, cela doit passer par un composant React dédié dans le \u003Ccode>&#x3C;head>\u003C/code>, jamais par une interpolation dans un template CSS. Le récit d'un \u003Ca href=\"/blog/a-b-test-header-la-variante-b-sert-un-noindex-a-50-du-trafic-pendant-9-jours\">A/B test header servant un noindex à 50 % du trafic pendant 9 jours\u003C/a> illustre exactement ce même anti-pattern dans un autre contexte.\u003C/p>\n\u003Ch3>Étape 2 — Valider le HTML SSR avant déploiement\u003C/h3>\n\u003Cp>Ajout d'un test d'intégration dans la CI qui vérifie le HTML brut SSR :\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__/ssr-noindex-guard.test.ts\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { describe, it, expect } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'vitest'\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\">'SSR HTML safety checks'\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:#B392F0\">  it\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'should not contain noindex in style tags'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#F97583\">async\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\"> res\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'http://localhost:3000/produit/xyz'\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\"> html\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> res.\u003C/span>\u003Cspan style=\"color:#B392F0\">text\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Extraire le contenu de toutes les balises &#x3C;style>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> styleBlocks\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> html.\u003C/span>\u003Cspan style=\"color:#B392F0\">match\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\u003C/span>\u003Cspan style=\"color:#DBEDFF\">&#x3C;style\u003C/span>\u003Cspan style=\"color:#79B8FF\">[\u003C/span>\u003Cspan style=\"color:#F97583\">^\u003C/span>\u003Cspan style=\"color:#79B8FF\">>]\u003C/span>\u003Cspan style=\"color:#F97583\">*\u003C/span>\u003Cspan style=\"color:#DBEDFF\">>(\u003C/span>\u003Cspan style=\"color:#79B8FF\">[\\s\\S]\u003C/span>\u003Cspan style=\"color:#F97583\">*?\u003C/span>\u003Cspan style=\"color:#DBEDFF\">)&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D;font-weight:bold\">\\/\u003C/span>\u003Cspan style=\"color:#DBEDFF\">style>\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\u003C/span>\u003Cspan style=\"color:#F97583\">gi\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\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> block\u003C/span>\u003Cspan style=\"color:#F97583\"> of\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> styleBlocks) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">      expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(block.\u003C/span>\u003Cspan style=\"color:#B392F0\">toLowerCase\u003C/span>\u003Cspan style=\"color:#E1E4E8\">()).not.\u003C/span>\u003Cspan style=\"color:#B392F0\">toContain\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'noindex'\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\">(block.\u003C/span>\u003Cspan style=\"color:#B392F0\">toLowerCase\u003C/span>\u003Cspan style=\"color:#E1E4E8\">()).not.\u003C/span>\u003Cspan style=\"color:#B392F0\">toContain\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'&#x3C;meta'\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\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  it\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'should not contain any meta tag outside of &#x3C;head>'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#F97583\">async\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\"> res\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'http://localhost:3000/produit/xyz'\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\"> html\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> res.\u003C/span>\u003Cspan style=\"color:#B392F0\">text\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> headEnd\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> html.\u003C/span>\u003Cspan style=\"color:#B392F0\">indexOf\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'&#x3C;/head>'\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\"> bodyContent\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> html.\u003C/span>\u003Cspan style=\"color:#B392F0\">slice\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(headEnd);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> strayMetas\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> bodyContent.\u003C/span>\u003Cspan style=\"color:#B392F0\">match\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\u003C/span>\u003Cspan style=\"color:#DBEDFF\">&#x3C;meta\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\s\u003C/span>\u003Cspan style=\"color:#F97583\">+\u003C/span>\u003Cspan style=\"color:#DBEDFF\">name=\u003C/span>\u003Cspan style=\"color:#79B8FF\">[\"']\u003C/span>\u003Cspan style=\"color:#DBEDFF\">robots\u003C/span>\u003Cspan style=\"color:#79B8FF\">[\"'][\u003C/span>\u003Cspan style=\"color:#F97583\">^\u003C/span>\u003Cspan style=\"color:#79B8FF\">>]\u003C/span>\u003Cspan style=\"color:#F97583\">*\u003C/span>\u003Cspan style=\"color:#DBEDFF\">>\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\u003C/span>\u003Cspan style=\"color:#F97583\">gi\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\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(strayMetas).\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 tourne sur chaque PR via un serveur Next.js éphémère dans le pipeline CI (GitHub Actions). Il attrape toute injection accidentelle de balises HTML dans des blocs \u003Ccode>&#x3C;style>\u003C/code>, \u003Ccode>&#x3C;script>\u003C/code>, ou dans le \u003Ccode>&#x3C;body>\u003C/code>.\u003C/p>\n\u003Ch3>Étape 3 — Forcer le re-crawl\u003C/h3>\n\u003Cp>Après déploiement du fix :\u003C/p>\n\u003Col>\n\u003Cli>Soumission d'un sitemap mis à jour dans Search Console avec \u003Ccode>lastmod\u003C/code> actualisé sur toutes les fiches produit.\u003C/li>\n\u003Cli>Utilisation de l'API d'indexation Google pour soumettre les 50 pages à plus fort trafic en priorité.\u003C/li>\n\u003Cli>Invalidation du cache CDN (Vercel, dans ce cas) pour garantir que Googlebot reçoit le HTML corrigé dès le prochain passage.\u003C/li>\n\u003C/ol>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Invalider le cache Vercel sur les routes produit\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">npx\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> vercel\u003C/span>\u003Cspan style=\"color:#79B8FF\"> --force\u003C/span>\u003Cspan style=\"color:#79B8FF\"> --scope=marketplace-prod\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x26;&#x26; \u003C/span>\u003Cspan style=\"color:#B392F0\">curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -X\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> PURGE\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"https://marketplace.example.com/produit/*\"\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Ch3>Temps de récupération\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Cstrong>J+3\u003C/strong> : les 50 pages soumises via l'API d'indexation repassent en \u003Ccode>index, follow\u003C/code> dans Search Console.\u003C/li>\n\u003Cli>\u003Cstrong>J+7\u003C/strong> : 60 % des 1 247 pages exclues sont réindexées.\u003C/li>\n\u003Cli>\u003Cstrong>J+14\u003C/strong> : 95 % des pages récupèrent leur statut indexé. Les 5 % restantes sont des fiches produit à faible PageRank, crawlées moins souvent.\u003C/li>\n\u003Cli>\u003Cstrong>J+21\u003C/strong> : le trafic organique revient à son niveau d'avant l'incident. Total de l'impact estimé : −112K clics sur 21 jours.\u003C/li>\n\u003C/ul>\n\u003Cp>Le schéma de récupération est cohérent avec ce qui a été observé lors d'\u003Ca href=\"/blog/migration-nuxt-2-vers-nuxt-3-200-pages-en-fallback-layout-pendant-6-semaines\">une migration Nuxt 2 vers Nuxt 3 où 200 pages sont restées en fallback layout pendant 6 semaines\u003C/a> : les pages à forte autorité reviennent vite, les pages orphelines traînent.\u003C/p>\n\u003Ch3>Gardes-fous ajoutés post-incident\u003C/h3>\n\u003Col>\n\u003Cli>\u003Cstrong>Lint rule custom\u003C/strong> : un plugin ESLint qui interdit les chaînes contenant \u003Ccode>&#x3C;meta\u003C/code> ou \u003Ccode>&#x3C;link\u003C/code> dans les fichiers de configuration de thème et dans les template literals de \u003Ccode>createGlobalStyle\u003C/code>, \u003Ccode>css\u003C/code>, et \u003Ccode>styled\u003C/code>.\u003C/li>\n\u003Cli>\u003Cstrong>Alerte Search Console\u003C/strong> : un script cron quotidien qui requête l'API Search Console et déclenche une alerte Slack si le compteur de pages \u003Ccode>noindex\u003C/code> dépasse le seuil connu (14 pages légitimes + marge de 5).\u003C/li>\n\u003Cli>\u003Cstrong>Revue SSR obligatoire\u003C/strong> : toute PR touchant le \u003Ccode>&#x3C;head>\u003C/code>, le thème, ou les styles globaux doit inclure un \u003Ccode>curl\u003C/code> du HTML SSR dans la description de PR. Pas de merge sans cette preuve.\u003C/li>\n\u003C/ol>\n\u003Cp>Le \u003Ca href=\"/blog/design-system-composant-heading-qui-rend-div-selon-la-prop-as-mal-configuree\">récit du design system dont le composant heading rendait des \u003Ccode>&#x3C;div>\u003C/code> au lieu de \u003Ccode>&#x3C;h1>\u003C/code>\u003C/a> montre le même angle mort : le composant fait ce qu'on lui demande, mais ce qu'on lui demande n'est pas ce que Google attend.\u003C/p>\n\u003Ch2>Ce qu'on en retient\u003C/h2>\n\u003Cp>Le CSS-in-JS est puissant. Les template literals acceptent n'importe quelle chaîne. C'est leur force — et leur surface d'attaque SEO. Une interpolation de thème dans un \u003Ccode>createGlobalStyle\u003C/code> peut injecter du HTML brut dans une balise \u003Ccode>&#x3C;style>\u003C/code>. Le navigateur l'ignore. Googlebot ne l'ignore pas.\u003C/p>\n\u003Cp>Le diagnostic a pris 18 jours parce que personne n'inspecte le HTML SSR brut. Les DevTools montrent le DOM, pas le source. Screaming Frog rend le JavaScript, pas le pré-parse. Seul un \u003Ccode>curl\u003C/code> — ou un monitoring continu type Seogard qui compare le HTML SSR aux directives d'indexation effectives — aurait détecté la divergence en quelques minutes au lieu de trois semaines.\u003C/p>\n\u003Cp>Règle simple : aucune donnée de configuration ne devrait contenir de balise HTML. Si un champ de thème peut finir dans un template literal CSS, il finira dans le HTML servi. Et si Google le voit, Google l'applique.\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,12,[18,19,20],"dark mode","css in js","noindex","Dark mode CSS-in-JS injecte un noindex : récit et fix","Fri Jun 05 2026 16:01:47 GMT+0000 (Coordinated Universal Time)",[24,39,53,66,81,95],{"_id":25,"slug":26,"__v":6,"author":7,"canonical":27,"category":10,"createdAt":28,"date":29,"description":30,"image":15,"imageAlt":15,"readingTime":31,"tags":32,"title":37,"updatedAt":38},"6a2114ceaa6b273b0c3f5edb","design-system-composant-heading-qui-rend-div-selon-la-prop-as-mal-configuree","https://seogard.io/blog/design-system-composant-heading-qui-rend-div-selon-la-prop-as-mal-configuree","2026-06-04T06:01:50.617Z","2026-06-04","Un composant Heading React mal configuré rend des div au lieu de h1-h6. Récit de l'incident, diagnostic du diff, fix et récupération SEO.",11,[33,34,35,36],"design system","heading","semantic","react","Design system React : un Heading en div détruit la sémantique de 1 200 pages","Thu Jun 04 2026 06:01:50 GMT+0000 (Coordinated Universal Time)",{"_id":40,"slug":41,"__v":6,"author":7,"canonical":42,"category":10,"createdAt":43,"date":44,"description":45,"image":15,"imageAlt":15,"readingTime":16,"tags":46,"title":51,"updatedAt":52},"6a1fc353aa6b273b0c28a952","design-mobile-first-h1-en-display-none-sur-desktop-invisible-pour-l-index-mobile-first","https://seogard.io/blog/design-mobile-first-h1-en-display-none-sur-desktop-invisible-pour-l-index-mobile-first","2026-06-03T06:01:55.600Z","2026-06-03","Un H1 masqué par CSS responsive disparaît de l'index mobile-first de Google. Récit de l'incident, diagnostic technique et correctif sémantique.",[47,48,49,50],"mobile first","h1","display none","responsive","H1 display:none en mobile-first : −34 % de trafic organique","Wed Jun 03 2026 06:01:55 GMT+0000 (Coordinated Universal Time)",{"_id":54,"slug":55,"__v":6,"author":7,"canonical":56,"category":10,"createdAt":57,"date":58,"description":59,"image":15,"imageAlt":15,"readingTime":16,"tags":60,"title":64,"updatedAt":65},"6a1dace0aa6b273b0c6f01ee","refonte-header-le-h1-remplace-par-un-div-title-par-le-design-system","https://seogard.io/blog/refonte-header-le-h1-remplace-par-un-div-title-par-le-design-system","2026-06-01T16:01:36.649Z","2026-06-01","Un composant générique remplace silencieusement le H1 par un div sur 800 pages. Récit du bug, diagnostic technique et fix complet.",[33,48,61,62,63],"refonte","composant","régression SEO","Design system : un div remplace le H1 sur 800 pages","Mon Jun 01 2026 16:01:36 GMT+0000 (Coordinated Universal Time)",{"_id":67,"slug":68,"__v":6,"author":7,"canonical":69,"category":70,"createdAt":71,"date":12,"description":72,"image":15,"imageAlt":15,"readingTime":16,"tags":73,"title":79,"updatedAt":80},"6a226691aa6b273b0c562a67","google-s-may-core-update-favored-pages-that-match-intent-via-sejournal-mattgsouthern","https://seogard.io/blog/google-s-may-core-update-favored-pages-that-match-intent-via-sejournal-mattgsouthern","Actualités SEO","2026-06-05T06:02:57.864Z","Analyse technique du May 2025 Core Update de Google : comment l'alignement intent/contenu et les signaux techniques déterminent les gagnants et perdants.",[74,75,76,77,78],"core update","search intent","SISTRIX","SEO technique","google algorithm","May 2025 Core Update : intent matching et signaux techniques","Fri Jun 05 2026 06:02:57 GMT+0000 (Coordinated Universal Time)",{"_id":82,"slug":83,"__v":6,"author":7,"canonical":84,"category":85,"createdAt":86,"date":29,"description":87,"image":15,"imageAlt":15,"readingTime":31,"tags":88,"title":93,"updatedAt":94},"6a21a15daa6b273b0cb36647","lazy-load-du-hero-h1-dans-une-section-v-if-invisible-au-fetch-http-brut","https://seogard.io/blog/lazy-load-du-hero-h1-dans-une-section-v-if-invisible-au-fetch-http-brut","Rendering","2026-06-04T16:01:33.508Z","Un hero section en v-if masque le H1 au SSR. Récit d'une régression silencieuse sur 320 pages, diagnostic technique et fix Nuxt complet.",[89,90,91,48,92],"lazy load","hero","ssr","vue","Lazy-load du hero Vue : H1 invisible pour Google","Thu Jun 04 2026 16:01:33 GMT+0000 (Coordinated Universal Time)",{"_id":96,"slug":97,"__v":6,"author":7,"canonical":98,"category":70,"createdAt":99,"date":44,"description":100,"image":15,"imageAlt":15,"readingTime":16,"tags":101,"title":107,"updatedAt":108},"6a205036aa6b273b0c9cf532","google-tests-dedicated-ai-search-reports-in-search-console-via-sejournal-mattgsouthern","https://seogard.io/blog/google-tests-dedicated-ai-search-reports-in-search-console-via-sejournal-mattgsouthern","2026-06-03T16:03:02.029Z","Google teste des rapports dédiés AI Search dans Search Console. Analyse technique des données, impacts SEO et stratégies d'adaptation pour les sites 500+ pages.",[102,103,104,105,106],"google","search console","AI search","AI overviews","reports","AI Search Reports dans Search Console : analyse technique","Wed Jun 03 2026 16:03:02 GMT+0000 (Coordinated Universal Time)",{"categories":110},[111,114,118,121,125,128,131,134,138,142,145,148,150,153,156,159,162,166],{"category":70,"slug":112,"count":113},"actualites-seo",161,{"category":115,"slug":116,"count":117},"Migration","migration",18,{"category":85,"slug":119,"count":120},"rendering",8,{"category":122,"slug":123,"count":124},"Performance","performance",7,{"category":126,"slug":127,"count":124},"SEO Technique","seo-technique",{"category":129,"slug":130,"count":124},"Crawl","crawl",{"category":132,"slug":133,"count":124},"Meta Tags","meta-tags",{"category":135,"slug":136,"count":137},"Architecture","architecture",6,{"category":139,"slug":140,"count":141},"JavaScript SEO","javascript-seo",5,{"category":143,"slug":144,"count":141},"Monitoring","monitoring",{"category":146,"slug":147,"count":141},"Structured Data","structured-data",{"category":10,"slug":61,"count":149},4,{"category":151,"slug":152,"count":149},"Redirections","redirections",{"category":154,"slug":155,"count":149},"Outils","outils",{"category":157,"slug":158,"count":149},"E-commerce","e-commerce",{"category":160,"slug":161,"count":149},"Avancé","avance",{"category":163,"slug":164,"count":165},"IA & SEO","ia-seo",3,{"category":167,"slug":168,"count":165},"Contenu","contenu"]