[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$f1QerOjVFyWn0uJWqgrQGEQRvimcdCQ7MDmYNcul-9uk":3,"$f-aPHVV5AQ6hNB_9Dx2yEvaCCSUtWKz-1ip8G8XThINQ":24,"$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":22,"updatedAt":23},"6a2114ceaa6b273b0c3f5edb","design-system-composant-heading-qui-rend-div-selon-la-prop-as-mal-configuree",0,"Equipe Seogard","# Design system React : quand un composant Heading rend des div et détruit la sémantique de 1 200 pages\n\nMardi 14h20. Le design system interne, fraîchement déployé sur l'ensemble du catalogue produit, passe en production. Dans Storybook, chaque composant est impeccable. Dans le navigateur, les titres s'affichent avec les bonnes tailles, les bonnes couleurs, les bons espacements. Personne ne regarde le HTML rendu. Pendant 19 jours, Googlebot crawle 1 247 pages produit qui n'ont plus un seul heading sémantique. Que des `\u003Cdiv>`.\n\n## Mercredi, J+8 — \"Pourquoi les impressions baissent ?\"\n\nLe premier signal arrive par Slack. L'acquisition manager partage une capture d'écran de Google Search Console. Le rapport Performance montre un décrochage net sur les requêtes de type `[catégorie] + [marque]`. Les impressions ont chuté de 31 % sur les sept derniers jours comparés à la période précédente. Les clics suivent : −2 400 clics hebdomadaires.\n\nL'équipe SEO ouvre Screaming Frog. Un crawl du répertoire `/produits/` remonte 1 247 URLs. Le rapport \"H1\" affiche un résultat brutal : **0 pages avec un H1 détecté**. Zéro.\n\nPremier réflexe : vérifier si Screaming Frog est bien configuré pour le rendu JavaScript. L'option \"JavaScript Rendering\" est active, le moteur Chromium est à jour. Le crawl est relancé en mode \"View Rendered Page\" sur cinq URLs au hasard. Même résultat.\n\nL'hypothèse initiale de l'équipe : un problème de rendu côté serveur. Le site tourne sur Next.js 14 (App Router). Peut-être que le SSR ne rend pas les composants correctement. Un dev ouvre une URL dans Chrome, fait `View Source`. Le HTML brut arrive du serveur. Les balises sont là. Mais ce ne sont pas des `\u003Ch1>`, ni des `\u003Ch2>`. Ce sont des `\u003Cdiv>`.\n\nL'équipe vérifie cinq, dix, vingt pages. Toujours pareil. Chaque titre de produit, chaque sous-titre de section — tout est rendu en `\u003Cdiv>` avec des classes CSS. Visuellement identique. Sémantiquement vide.\n\nLe lead dev ouvre le composant `Heading` dans le repo du design system. Et il comprend.\n\n## Le bug : un composant polymorphe sans overrides\n\nLe design system de cette marketplace française (15 000 SKU, 1 247 pages produit indexées, 85K visites organiques mensuelles) a été reconstruit six semaines plus tôt. L'équipe design a migré d'un système ad hoc vers une bibliothèque de composants React centralisée, versionée, publiée sur un registre npm interne.\n\nLe composant `Heading` a été conçu comme un composant polymorphe. L'idée : un seul composant pour tous les niveaux de titre, avec une prop `as` qui contrôle la balise HTML rendue.\n\nVoici le composant tel qu'il existait dans le design system (`v2.4.0`) :\n\n```tsx\n// packages/ui/src/components/Heading/Heading.tsx\nimport React from 'react';\nimport { cn } from '../../utils/cn';\n\ntype HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'div' | 'span';\n\ninterface HeadingProps {\n  as?: HeadingLevel;\n  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';\n  children: React.ReactNode;\n  className?: string;\n}\n\nexport const Heading: React.FC\u003CHeadingProps> = ({\n  as = 'div',\n  size = 'md',\n  children,\n  className,\n}) => {\n  const Component = as;\n  return (\n    \u003CComponent\n      className={cn(\n        'font-heading font-bold tracking-tight',\n        {\n          'text-xs': size === 'xs',\n          'text-sm': size === 'sm',\n          'text-base': size === 'md',\n          'text-xl': size === 'lg',\n          'text-2xl': size === 'xl',\n          'text-3xl': size === '2xl',\n        },\n        className\n      )}\n    >\n      {children}\n    \u003C/Component>\n  );\n};\n```\n\nLe problème est sur la ligne 14 : `as = 'div'`.\n\nLa valeur par défaut de la prop `as` est `div`. Pas `h2`. Pas `h3`. Un `div`.\n\nLe designer qui a créé le composant a raisonné en termes visuels. Dans Figma, le composant \"Heading\" a des variantes par taille : `2xl` pour les titres principaux, `lg` pour les sous-titres, `sm` pour les titres de cards. La sémantique HTML — le choix entre `h1`, `h2`, `h3` — n'apparaît nulle part dans les specs Figma. Elle était censée être gérée \"côté dev, au moment de l'intégration\".\n\nSauf que les développeurs, en consommant le composant, ont fait confiance à l'API. Voici comment le composant était utilisé dans le template produit :\n\n```tsx\n// app/produits/[slug]/page.tsx\nimport { Heading } from '@acme/ui';\n\nexport default function ProductPage({ product }) {\n  return (\n    \u003Cmain>\n      \u003CHeading size=\"2xl\">{product.name}\u003C/Heading>\n\n      \u003Csection>\n        \u003CHeading size=\"lg\">Caractéristiques\u003C/Heading>\n        {/* ... */}\n      \u003C/section>\n\n      \u003Csection>\n        \u003CHeading size=\"lg\">Avis clients\u003C/Heading>\n        {/* ... */}\n      \u003C/section>\n\n      \u003Csection>\n        \u003CHeading size=\"md\">Produits similaires\u003C/Heading>\n        {/* ... */}\n      \u003C/section>\n    \u003C/main>\n  );\n}\n```\n\nAucun `as` n'est passé. Le développeur a utilisé `size` pour contrôler l'apparence. La prop `as` prend sa valeur par défaut : `div`. Partout.\n\nLe HTML rendu côté serveur pour chaque page produit ressemblait à ceci :\n\n```html\n\u003Cmain>\n  \u003Cdiv class=\"font-heading font-bold tracking-tight text-3xl\">\n    Chaussure de trail UltraGrip 3000\n  \u003C/div>\n\n  \u003Csection>\n    \u003Cdiv class=\"font-heading font-bold tracking-tight text-xl\">\n      Caractéristiques\n    \u003C/div>\n    \u003C!-- ... -->\n  \u003C/section>\n\n  \u003Csection>\n    \u003Cdiv class=\"font-heading font-bold tracking-tight text-xl\">\n      Avis clients\n    \u003C/div>\n    \u003C!-- ... -->\n  \u003C/section>\n\n  \u003Csection>\n    \u003Cdiv class=\"font-heading font-bold tracking-tight text-base\">\n      Produits similaires\n    \u003C/div>\n    \u003C!-- ... -->\n  \u003C/section>\n\u003C/main>\n```\n\nZéro heading sémantique. Googlebot voit une page sans aucune structure de titres. Le nom du produit, qui était autrefois un `\u003Ch1>`, est devenu un `\u003Cdiv>` stylisé en gros.\n\n### Pourquoi personne n'a rien vu\n\nTrois raisons.\n\n**1. Le rendu visuel était parfait.** Le composant `Heading` appliquait les bonnes classes Tailwind. Dans le navigateur, les titres avaient la bonne taille, la bonne graisse. L'œil humain ne distingue pas un `\u003Cdiv class=\"text-3xl font-bold\">` d'un `\u003Ch1 class=\"text-3xl font-bold\">`.\n\n**2. Les tests ne couvraient pas la sémantique.** Le design system avait des tests unitaires — pour les variants CSS, pour les tailles, pour le rendu conditionnel. Aucun test ne vérifiait la balise HTML rendue. Le test existant ressemblait à ceci :\n\n```tsx\n// Heading.test.tsx — tests existants (insuffisants)\nit('renders with correct size class', () => {\n  const { container } = render(\u003CHeading size=\"xl\">Test\u003C/Heading>);\n  expect(container.firstChild).toHaveClass('text-2xl');\n});\n\nit('renders children', () => {\n  const { getByText } = render(\u003CHeading>Mon titre\u003C/Heading>);\n  expect(getByText('Mon titre')).toBeInTheDocument();\n});\n```\n\nLe test passe. Le composant rend bien du texte avec la bonne classe. Personne ne vérifie que `container.firstChild?.tagName` vaut `'H2'` ou `'H1'`.\n\n**3. L'inspection Chrome DevTools masque le problème.** Quand un développeur inspecte un élément dans le panneau Elements, la balise `\u003Cdiv>` est visible — mais noyée dans l'arbre DOM. Sans aller vérifier explicitement l'onglet \"Accessibility\" ou lancer un audit Lighthouse sur la structure des headings, le bug est invisible.\n\n### Le diff qui a tout cassé\n\nEn remontant l'historique Git du design system, l'équipe retrouve le commit responsable. Lors de la v2.0.0 (refonte complète), le composant `Heading` de l'ancienne version avait `as = 'h2'` comme valeur par défaut. Le designer principal a changé cette valeur en `div` dans un commit intitulé \"fix: Heading should not assume semantic level\".\n\nLe message de commit explique le raisonnement : \"Le composant ne devrait pas imposer un niveau sémantique. C'est au consommateur de le définir.\" Un raisonnement défendable en théorie. Catastrophique en pratique, parce que personne n'a mis à jour les 43 fichiers qui utilisaient le composant sans passer la prop `as`.\n\nLe `git diff` entre la v1.x et la v2.0.0 pour ce fichier :\n\n```diff\n- as = 'h2',\n+ as = 'div',\n```\n\nUne ligne. 1 247 pages impactées.\n\n### L'impact mesuré\n\nL'équipe reconstitue la timeline via Search Console (données disponibles avec 48h de décalage) et Google Analytics 4 :\n\n- **J+0 à J+3** : Googlebot recrawle 340 pages produit. Les impressions commencent à baisser sur les requêtes longue traîne.\n- **J+4 à J+8** : 890 pages recrawlées. Perte de 31 % des impressions sur le segment produit. Les positions moyennes passent de 8.2 à 14.7.\n- **J+8 à J+19** : L'intégralité des 1 247 pages est recrawlée. Perte cumulée estimée : −4 100 clics organiques. Les featured snippets sur 12 requêtes \"caractéristiques [produit]\" disparaissent.\n\nL'outil de test des résultats enrichis de Google confirme : sans heading sémantique, les sections \"Caractéristiques\" et \"Avis\" ne sont plus identifiées comme des passages structurés.\n\n## Le fix : imposer la sémantique, tester la sémantique\n\nLe correctif se déploie en deux temps.\n\n### 1. Patch du composant (v2.4.1)\n\nL'équipe change la valeur par défaut de `as` et ajoute un warning en développement quand la prop n'est pas explicitement passée :\n\n```tsx\n// packages/ui/src/components/Heading/Heading.tsx — v2.4.1\nimport React from 'react';\nimport { cn } from '../../utils/cn';\n\ntype HeadingElement = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';\n\ninterface HeadingProps {\n  as: HeadingElement; // requis — plus de valeur par défaut\n  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';\n  children: React.ReactNode;\n  className?: string;\n}\n\nexport const Heading: React.FC\u003CHeadingProps> = ({\n  as,\n  size = 'md',\n  children,\n  className,\n}) => {\n  const Component = as;\n  return (\n    \u003CComponent\n      className={cn(\n        'font-heading font-bold tracking-tight',\n        {\n          'text-xs': size === 'xs',\n          'text-sm': size === 'sm',\n          'text-base': size === 'md',\n          'text-xl': size === 'lg',\n          'text-2xl': size === 'xl',\n          'text-3xl': size === '2xl',\n        },\n        className\n      )}\n    >\n      {children}\n    \u003C/Component>\n  );\n};\n```\n\nLe changement clé : `as` est maintenant **requis** (`as: HeadingElement` sans `?`). Le type `HeadingElement` n'accepte plus `div` ni `span`. TypeScript refuse la compilation si un consommateur oublie la prop. Les 43 fichiers qui utilisaient `\u003CHeading>` sans `as` cassent immédiatement au build — ce qui force le passage en revue de chaque usage.\n\nLe template produit corrigé :\n\n```tsx\n// app/produits/[slug]/page.tsx — corrigé\nimport { Heading } from '@acme/ui';\n\nexport default function ProductPage({ product }) {\n  return (\n    \u003Cmain>\n      \u003CHeading as=\"h1\" size=\"2xl\">{product.name}\u003C/Heading>\n\n      \u003Csection>\n        \u003CHeading as=\"h2\" size=\"lg\">Caractéristiques\u003C/Heading>\n        {/* ... */}\n      \u003C/section>\n\n      \u003Csection>\n        \u003CHeading as=\"h2\" size=\"lg\">Avis clients\u003C/Heading>\n        {/* ... */}\n      \u003C/section>\n\n      \u003Csection>\n        \u003CHeading as=\"h3\" size=\"md\">Produits similaires\u003C/Heading>\n        {/* ... */}\n      \u003C/section>\n    \u003C/main>\n  );\n}\n```\n\n### 2. Tests sémantiques ajoutés à la CI\n\nL'équipe ajoute un test qui vérifie la balise rendue, pas seulement le style :\n\n```tsx\n// Heading.test.tsx — tests ajoutés\nit('renders the correct HTML element based on \"as\" prop', () => {\n  const { container } = render(\u003CHeading as=\"h1\" size=\"xl\">Titre\u003C/Heading>);\n  expect(container.querySelector('h1')).toBeInTheDocument();\n  expect(container.querySelector('div')).not.toBeInTheDocument();\n});\n\nit('does not render a div or span', () => {\n  const levels: Array\u003C'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'> = [\n    'h1', 'h2', 'h3', 'h4', 'h5', 'h6',\n  ];\n  levels.forEach((level) => {\n    const { container } = render(\u003CHeading as={level}>Test\u003C/Heading>);\n    const el = container.firstChild as HTMLElement;\n    expect(el.tagName.toLowerCase()).toBe(level);\n    cleanup();\n  });\n});\n```\n\n### 3. Lint rule custom pour les templates\n\nUn plugin ESLint custom avertit quand `\u003CHeading>` est utilisé sans `as` explicite, en complément de la contrainte TypeScript (pour les fichiers `.jsx` non typés) :\n\n```bash\nnpx eslint --rule '@acme/require-heading-as: error' app/\n```\n\n### 4. Validation post-déploiement\n\nAprès le déploiement du fix, l'équipe lance un crawl Screaming Frog ciblé :\n\n```bash\n# Vérification rapide via curl + grep sur un échantillon\nfor url in $(cat urls-produits-sample.txt); do\n  echo \"--- $url ---\"\n  curl -s \"$url\" | grep -oP '\u003Ch[1-6][^>]*>.*?\u003C/h[1-6]>' | head -5\ndone\n```\n\nLes 1 247 pages rendent désormais un `\u003Ch1>` pour le nom produit, des `\u003Ch2>` pour les sections principales.\n\n### Récupération\n\nLe fix est déployé un mercredi soir. La récupération est progressive :\n\n- **J+3 post-fix** : 410 pages recrawlées par Googlebot (vérifié via les logs serveur, user-agent `Googlebot`).\n- **J+7** : Les impressions cessent de baisser. Stabilisation.\n- **J+14** : 80 % du trafic organique produit est récupéré. Les positions moyennes reviennent à 9.1 (vs 8.2 avant l'incident).\n- **J+21** : Retour quasi complet. Les featured snippets \"caractéristiques\" reviennent sur 9 des 12 requêtes perdues.\n\nTotal de l'impact : 19 jours d'exposition, environ 4 100 clics perdus, 3 semaines de récupération. Le coût estimé, ramené au CPC moyen des requêtes concernées (0.85 €), dépasse 3 400 €.\n\nL'incident a aussi révélé un problème similaire sur le composant `Text`, qui pouvait rendre des `\u003Cp>` ou des `\u003Cdiv>` selon un mécanisme identique. Ce composant n'avait pas d'impact SEO direct mais a été corrigé dans la foulée, par cohérence — un cas typique de [régression liée au design system](/blog/refonte-header-le-h1-remplace-par-un-div-title-par-le-design-system) qu'il faut traiter à la racine.\n\n## Ce qu'on en retient\n\nUn composant polymorphe sans valeur par défaut sémantique est une bombe à retardement. Le problème n'est pas le pattern `as` — c'est le `div` comme fallback silencieux.\n\nTrois règles à graver dans la documentation du design system :\n\n1. **La prop `as` d'un composant Heading ne doit jamais accepter `div` ou `span`.** Le type TypeScript est le premier rempart.\n2. **Chaque composant sémantique doit avoir un test qui vérifie la balise HTML rendue**, pas seulement le contenu ou le style.\n3. **Un crawl sémantique post-déploiement doit vérifier la présence de `h1`-`h6`** sur un échantillon de pages critiques.\n\nCe type de régression — invisible à l'œil, invisible dans les tests classiques, invisible dans les métriques pendant une semaine — est exactement ce qu'un [monitoring continu de la structure HTML](/blog/design-mobile-first-h1-en-display-none-sur-desktop-invisible-pour-l-index-mobile-first) permet de détecter avant que Search Console ne tire l'alarme. Un outil comme Seogard compare le HTML rendu côté serveur à chaque déploiement et alerte sur la disparition d'éléments sémantiques critiques — en minutes, pas en semaines.\n\nLe design system est censé garantir la cohérence. Quand il la garantit uniquement sur le plan visuel et pas sur le plan sémantique, il devient le vecteur d'une régression silencieuse à l'échelle de tout le site. Le diff faisait une ligne. L'impact a duré trois semaines.\n```","https://seogard.io/blog/design-system-composant-heading-qui-rend-div-selon-la-prop-as-mal-configuree","Refonte","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.","\u003Ch1>Design system React : quand un composant Heading rend des div et détruit la sémantique de 1 200 pages\u003C/h1>\n\u003Cp>Mardi 14h20. Le design system interne, fraîchement déployé sur l'ensemble du catalogue produit, passe en production. Dans Storybook, chaque composant est impeccable. Dans le navigateur, les titres s'affichent avec les bonnes tailles, les bonnes couleurs, les bons espacements. Personne ne regarde le HTML rendu. Pendant 19 jours, Googlebot crawle 1 247 pages produit qui n'ont plus un seul heading sémantique. Que des \u003Ccode>&#x3C;div>\u003C/code>.\u003C/p>\n\u003Ch2>Mercredi, J+8 — \"Pourquoi les impressions baissent ?\"\u003C/h2>\n\u003Cp>Le premier signal arrive par Slack. L'acquisition manager partage une capture d'écran de Google Search Console. Le rapport Performance montre un décrochage net sur les requêtes de type \u003Ccode>[catégorie] + [marque]\u003C/code>. Les impressions ont chuté de 31 % sur les sept derniers jours comparés à la période précédente. Les clics suivent : −2 400 clics hebdomadaires.\u003C/p>\n\u003Cp>L'équipe SEO ouvre Screaming Frog. Un crawl du répertoire \u003Ccode>/produits/\u003C/code> remonte 1 247 URLs. Le rapport \"H1\" affiche un résultat brutal : \u003Cstrong>0 pages avec un H1 détecté\u003C/strong>. Zéro.\u003C/p>\n\u003Cp>Premier réflexe : vérifier si Screaming Frog est bien configuré pour le rendu JavaScript. L'option \"JavaScript Rendering\" est active, le moteur Chromium est à jour. Le crawl est relancé en mode \"View Rendered Page\" sur cinq URLs au hasard. Même résultat.\u003C/p>\n\u003Cp>L'hypothèse initiale de l'équipe : un problème de rendu côté serveur. Le site tourne sur Next.js 14 (App Router). Peut-être que le SSR ne rend pas les composants correctement. Un dev ouvre une URL dans Chrome, fait \u003Ccode>View Source\u003C/code>. Le HTML brut arrive du serveur. Les balises sont là. Mais ce ne sont pas des \u003Ccode>&#x3C;h1>\u003C/code>, ni des \u003Ccode>&#x3C;h2>\u003C/code>. Ce sont des \u003Ccode>&#x3C;div>\u003C/code>.\u003C/p>\n\u003Cp>L'équipe vérifie cinq, dix, vingt pages. Toujours pareil. Chaque titre de produit, chaque sous-titre de section — tout est rendu en \u003Ccode>&#x3C;div>\u003C/code> avec des classes CSS. Visuellement identique. Sémantiquement vide.\u003C/p>\n\u003Cp>Le lead dev ouvre le composant \u003Ccode>Heading\u003C/code> dans le repo du design system. Et il comprend.\u003C/p>\n\u003Ch2>Le bug : un composant polymorphe sans overrides\u003C/h2>\n\u003Cp>Le design system de cette marketplace française (15 000 SKU, 1 247 pages produit indexées, 85K visites organiques mensuelles) a été reconstruit six semaines plus tôt. L'équipe design a migré d'un système ad hoc vers une bibliothèque de composants React centralisée, versionée, publiée sur un registre npm interne.\u003C/p>\n\u003Cp>Le composant \u003Ccode>Heading\u003C/code> a été conçu comme un composant polymorphe. L'idée : un seul composant pour tous les niveaux de titre, avec une prop \u003Ccode>as\u003C/code> qui contrôle la balise HTML rendue.\u003C/p>\n\u003Cp>Voici le composant tel qu'il existait dans le design system (\u003Ccode>v2.4.0\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\">// packages/ui/src/components/Heading/Heading.tsx\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> React \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'react'\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\"> { cn } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '../../utils/cn'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">type\u003C/span>\u003Cspan style=\"color:#B392F0\"> HeadingLevel\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'h1'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'h2'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'h3'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'h4'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'h5'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'h6'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'div'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'span'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">interface\u003C/span>\u003Cspan style=\"color:#B392F0\"> HeadingProps\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  as\u003C/span>\u003Cspan style=\"color:#F97583\">?:\u003C/span>\u003Cspan style=\"color:#B392F0\"> HeadingLevel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  size\u003C/span>\u003Cspan style=\"color:#F97583\">?:\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'xs'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'sm'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'md'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'lg'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'xl'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '2xl'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  children\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> React\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#B392F0\">ReactNode\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  className\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:#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:#B392F0\"> Heading\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> React\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#B392F0\">FC\u003C/span>\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#B392F0\">HeadingProps\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\">  as\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> = \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'div'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  size \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'md'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  children,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  className,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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\"> Component\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> as;\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:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">Component\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">      className\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{\u003C/span>\u003Cspan style=\"color:#B392F0\">cn\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        'font-heading font-bold tracking-tight'\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:#9ECBFF\">          'text-xs'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: size \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'xs'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">          'text-sm'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: size \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'sm'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">          'text-base'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: size \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'md'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">          'text-xl'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: size \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'lg'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">          'text-2xl'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: size \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'xl'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">          'text-3xl'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: size \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '2xl'\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\">        className\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\">      {children}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;/\u003C/span>\u003Cspan style=\"color:#79B8FF\">Component\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>Le problème est sur la ligne 14 : \u003Ccode>as = 'div'\u003C/code>.\u003C/p>\n\u003Cp>La valeur par défaut de la prop \u003Ccode>as\u003C/code> est \u003Ccode>div\u003C/code>. Pas \u003Ccode>h2\u003C/code>. Pas \u003Ccode>h3\u003C/code>. Un \u003Ccode>div\u003C/code>.\u003C/p>\n\u003Cp>Le designer qui a créé le composant a raisonné en termes visuels. Dans Figma, le composant \"Heading\" a des variantes par taille : \u003Ccode>2xl\u003C/code> pour les titres principaux, \u003Ccode>lg\u003C/code> pour les sous-titres, \u003Ccode>sm\u003C/code> pour les titres de cards. La sémantique HTML — le choix entre \u003Ccode>h1\u003C/code>, \u003Ccode>h2\u003C/code>, \u003Ccode>h3\u003C/code> — n'apparaît nulle part dans les specs Figma. Elle était censée être gérée \"côté dev, au moment de l'intégration\".\u003C/p>\n\u003Cp>Sauf que les développeurs, en consommant le composant, ont fait confiance à l'API. Voici comment le composant était utilisé dans le template produit :\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/produits/[slug]/page.tsx\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { Heading } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '@acme/ui'\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\"> default\u003C/span>\u003Cspan style=\"color:#F97583\"> function\u003C/span>\u003Cspan style=\"color:#B392F0\"> ProductPage\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({ \u003C/span>\u003Cspan style=\"color:#FFAB70\">product\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:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">main\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\u003C/span>\u003Cspan style=\"color:#B392F0\"> size\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"2xl\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>{product.name}&#x3C;/\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">section\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        &#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\u003C/span>\u003Cspan style=\"color:#B392F0\"> size\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"lg\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Caractéristiques&#x3C;/\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        {\u003C/span>\u003Cspan style=\"color:#6A737D\">/* ... */\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\">section\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">section\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        &#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\u003C/span>\u003Cspan style=\"color:#B392F0\"> size\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"lg\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Avis clients&#x3C;/\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        {\u003C/span>\u003Cspan style=\"color:#6A737D\">/* ... */\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\">section\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">section\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        &#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\u003C/span>\u003Cspan style=\"color:#B392F0\"> size\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"md\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Produits similaires&#x3C;/\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        {\u003C/span>\u003Cspan style=\"color:#6A737D\">/* ... */\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\">section\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\">main\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>Aucun \u003Ccode>as\u003C/code> n'est passé. Le développeur a utilisé \u003Ccode>size\u003C/code> pour contrôler l'apparence. La prop \u003Ccode>as\u003C/code> prend sa valeur par défaut : \u003Ccode>div\u003C/code>. Partout.\u003C/p>\n\u003Cp>Le HTML rendu côté serveur pour chaque page produit ressemblait à 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\">main\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\">div\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"font-heading font-bold tracking-tight text-3xl\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    Chaussure de trail UltraGrip 3000\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">div\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">section\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\">div\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"font-heading font-bold tracking-tight text-xl\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      Caractéristiques\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">div\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    &#x3C;!-- ... -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">section\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">section\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\">div\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"font-heading font-bold tracking-tight text-xl\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      Avis clients\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">div\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    &#x3C;!-- ... -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">section\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">section\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\">div\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"font-heading font-bold tracking-tight text-base\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      Produits similaires\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">div\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    &#x3C;!-- ... -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">section\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\">main\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Zéro heading sémantique. Googlebot voit une page sans aucune structure de titres. Le nom du produit, qui était autrefois un \u003Ccode>&#x3C;h1>\u003C/code>, est devenu un \u003Ccode>&#x3C;div>\u003C/code> stylisé en gros.\u003C/p>\n\u003Ch3>Pourquoi personne n'a rien vu\u003C/h3>\n\u003Cp>Trois raisons.\u003C/p>\n\u003Cp>\u003Cstrong>1. Le rendu visuel était parfait.\u003C/strong> Le composant \u003Ccode>Heading\u003C/code> appliquait les bonnes classes Tailwind. Dans le navigateur, les titres avaient la bonne taille, la bonne graisse. L'œil humain ne distingue pas un \u003Ccode>&#x3C;div class=\"text-3xl font-bold\">\u003C/code> d'un \u003Ccode>&#x3C;h1 class=\"text-3xl font-bold\">\u003C/code>.\u003C/p>\n\u003Cp>\u003Cstrong>2. Les tests ne couvraient pas la sémantique.\u003C/strong> Le design system avait des tests unitaires — pour les variants CSS, pour les tailles, pour le rendu conditionnel. Aucun test ne vérifiait la balise HTML rendue. Le test existant ressemblait à ceci :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Heading.test.tsx — tests existants (insuffisants)\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\">'renders with correct size class'\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:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#79B8FF\">container\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#B392F0\"> render\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(&#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\u003C/span>\u003Cspan style=\"color:#B392F0\"> size\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"xl\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Test&#x3C;/\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\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\">(container.firstChild).\u003C/span>\u003Cspan style=\"color:#B392F0\">toHaveClass\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'text-2xl'\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\">'renders children'\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:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#79B8FF\">getByText\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#B392F0\"> render\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(&#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Mon titre&#x3C;/\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\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\">(\u003C/span>\u003Cspan style=\"color:#B392F0\">getByText\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Mon titre'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)).\u003C/span>\u003Cspan style=\"color:#B392F0\">toBeInTheDocument\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>Le test passe. Le composant rend bien du texte avec la bonne classe. Personne ne vérifie que \u003Ccode>container.firstChild?.tagName\u003C/code> vaut \u003Ccode>'H2'\u003C/code> ou \u003Ccode>'H1'\u003C/code>.\u003C/p>\n\u003Cp>\u003Cstrong>3. L'inspection Chrome DevTools masque le problème.\u003C/strong> Quand un développeur inspecte un élément dans le panneau Elements, la balise \u003Ccode>&#x3C;div>\u003C/code> est visible — mais noyée dans l'arbre DOM. Sans aller vérifier explicitement l'onglet \"Accessibility\" ou lancer un audit Lighthouse sur la structure des headings, le bug est invisible.\u003C/p>\n\u003Ch3>Le diff qui a tout cassé\u003C/h3>\n\u003Cp>En remontant l'historique Git du design system, l'équipe retrouve le commit responsable. Lors de la v2.0.0 (refonte complète), le composant \u003Ccode>Heading\u003C/code> de l'ancienne version avait \u003Ccode>as = 'h2'\u003C/code> comme valeur par défaut. Le designer principal a changé cette valeur en \u003Ccode>div\u003C/code> dans un commit intitulé \"fix: Heading should not assume semantic level\".\u003C/p>\n\u003Cp>Le message de commit explique le raisonnement : \"Le composant ne devrait pas imposer un niveau sémantique. C'est au consommateur de le définir.\" Un raisonnement défendable en théorie. Catastrophique en pratique, parce que personne n'a mis à jour les 43 fichiers qui utilisaient le composant sans passer la prop \u003Ccode>as\u003C/code>.\u003C/p>\n\u003Cp>Le \u003Ccode>git diff\u003C/code> entre la v1.x et la v2.0.0 pour ce fichier :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#FDAEB7\">- as = 'h2',\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">+ as = 'div',\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Une ligne. 1 247 pages impactées.\u003C/p>\n\u003Ch3>L'impact mesuré\u003C/h3>\n\u003Cp>L'équipe reconstitue la timeline via Search Console (données disponibles avec 48h de décalage) et Google Analytics 4 :\u003C/p>\n\u003Cul>\n\u003Cli>\u003Cstrong>J+0 à J+3\u003C/strong> : Googlebot recrawle 340 pages produit. Les impressions commencent à baisser sur les requêtes longue traîne.\u003C/li>\n\u003Cli>\u003Cstrong>J+4 à J+8\u003C/strong> : 890 pages recrawlées. Perte de 31 % des impressions sur le segment produit. Les positions moyennes passent de 8.2 à 14.7.\u003C/li>\n\u003Cli>\u003Cstrong>J+8 à J+19\u003C/strong> : L'intégralité des 1 247 pages est recrawlée. Perte cumulée estimée : −4 100 clics organiques. Les featured snippets sur 12 requêtes \"caractéristiques [produit]\" disparaissent.\u003C/li>\n\u003C/ul>\n\u003Cp>L'outil de test des résultats enrichis de Google confirme : sans heading sémantique, les sections \"Caractéristiques\" et \"Avis\" ne sont plus identifiées comme des passages structurés.\u003C/p>\n\u003Ch2>Le fix : imposer la sémantique, tester la sémantique\u003C/h2>\n\u003Cp>Le correctif se déploie en deux temps.\u003C/p>\n\u003Ch3>1. Patch du composant (v2.4.1)\u003C/h3>\n\u003Cp>L'équipe change la valeur par défaut de \u003Ccode>as\u003C/code> et ajoute un warning en développement quand la prop n'est pas explicitement passée :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// packages/ui/src/components/Heading/Heading.tsx — v2.4.1\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> React \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'react'\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\"> { cn } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '../../utils/cn'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">type\u003C/span>\u003Cspan style=\"color:#B392F0\"> HeadingElement\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'h1'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'h2'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'h3'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'h4'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'h5'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'h6'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">interface\u003C/span>\u003Cspan style=\"color:#B392F0\"> HeadingProps\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  as\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> HeadingElement\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; \u003C/span>\u003Cspan style=\"color:#6A737D\">// requis — plus de valeur par défaut\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  size\u003C/span>\u003Cspan style=\"color:#F97583\">?:\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'xs'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'sm'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'md'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'lg'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'xl'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '2xl'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  children\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> React\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#B392F0\">ReactNode\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  className\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:#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:#B392F0\"> Heading\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> React\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#B392F0\">FC\u003C/span>\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#B392F0\">HeadingProps\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\">  as,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  size \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'md'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  children,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  className,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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\"> Component\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> as;\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:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">Component\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">      className\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{\u003C/span>\u003Cspan style=\"color:#B392F0\">cn\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        'font-heading font-bold tracking-tight'\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:#9ECBFF\">          'text-xs'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: size \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'xs'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">          'text-sm'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: size \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'sm'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">          'text-base'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: size \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'md'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">          'text-xl'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: size \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'lg'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">          'text-2xl'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: size \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'xl'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">          'text-3xl'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: size \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '2xl'\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\">        className\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\">      {children}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;/\u003C/span>\u003Cspan style=\"color:#79B8FF\">Component\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>Le changement clé : \u003Ccode>as\u003C/code> est maintenant \u003Cstrong>requis\u003C/strong> (\u003Ccode>as: HeadingElement\u003C/code> sans \u003Ccode>?\u003C/code>). Le type \u003Ccode>HeadingElement\u003C/code> n'accepte plus \u003Ccode>div\u003C/code> ni \u003Ccode>span\u003C/code>. TypeScript refuse la compilation si un consommateur oublie la prop. Les 43 fichiers qui utilisaient \u003Ccode>&#x3C;Heading>\u003C/code> sans \u003Ccode>as\u003C/code> cassent immédiatement au build — ce qui force le passage en revue de chaque usage.\u003C/p>\n\u003Cp>Le template produit corrigé :\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/produits/[slug]/page.tsx — corrigé\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { Heading } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '@acme/ui'\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\"> default\u003C/span>\u003Cspan style=\"color:#F97583\"> function\u003C/span>\u003Cspan style=\"color:#B392F0\"> ProductPage\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({ \u003C/span>\u003Cspan style=\"color:#FFAB70\">product\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:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">main\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\u003C/span>\u003Cspan style=\"color:#B392F0\"> as\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"h1\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> size\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"2xl\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>{product.name}&#x3C;/\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">section\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        &#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\u003C/span>\u003Cspan style=\"color:#B392F0\"> as\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"h2\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> size\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"lg\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Caractéristiques&#x3C;/\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        {\u003C/span>\u003Cspan style=\"color:#6A737D\">/* ... */\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\">section\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">section\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        &#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\u003C/span>\u003Cspan style=\"color:#B392F0\"> as\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"h2\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> size\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"lg\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Avis clients&#x3C;/\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        {\u003C/span>\u003Cspan style=\"color:#6A737D\">/* ... */\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\">section\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">section\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        &#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\u003C/span>\u003Cspan style=\"color:#B392F0\"> as\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"h3\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> size\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"md\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Produits similaires&#x3C;/\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        {\u003C/span>\u003Cspan style=\"color:#6A737D\">/* ... */\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\">section\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\">main\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\u003Ch3>2. Tests sémantiques ajoutés à la CI\u003C/h3>\n\u003Cp>L'équipe ajoute un test qui vérifie la balise rendue, pas seulement le style :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Heading.test.tsx — tests ajoutés\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\">'renders the correct HTML element based on \"as\" prop'\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:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#79B8FF\">container\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#B392F0\"> render\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(&#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\u003C/span>\u003Cspan style=\"color:#B392F0\"> as\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"h1\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> size\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"xl\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Titre&#x3C;/\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\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\">(container.\u003C/span>\u003Cspan style=\"color:#B392F0\">querySelector\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'h1'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)).\u003C/span>\u003Cspan style=\"color:#B392F0\">toBeInTheDocument\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\">(container.\u003C/span>\u003Cspan style=\"color:#B392F0\">querySelector\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'div'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)).not.\u003C/span>\u003Cspan style=\"color:#B392F0\">toBeInTheDocument\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\">'does not render a div or span'\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\"> levels\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> Array\u003C/span>\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'h1'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'h2'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'h3'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'h4'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'h5'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'h6'\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:#9ECBFF\">    'h1'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'h2'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'h3'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'h4'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'h5'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'h6'\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\">  levels.\u003C/span>\u003Cspan style=\"color:#B392F0\">forEach\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">level\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:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#79B8FF\">container\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#B392F0\"> render\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(&#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\u003C/span>\u003Cspan style=\"color:#B392F0\"> as\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{level}>Test&#x3C;/\u003C/span>\u003Cspan style=\"color:#79B8FF\">Heading\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\"> el\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> container.firstChild \u003C/span>\u003Cspan style=\"color:#F97583\">as\u003C/span>\u003Cspan style=\"color:#B392F0\"> HTMLElement\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\">(el.tagName.\u003C/span>\u003Cspan style=\"color:#B392F0\">toLowerCase\u003C/span>\u003Cspan style=\"color:#E1E4E8\">()).\u003C/span>\u003Cspan style=\"color:#B392F0\">toBe\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(level);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    cleanup\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\u003Ch3>3. Lint rule custom pour les templates\u003C/h3>\n\u003Cp>Un plugin ESLint custom avertit quand \u003Ccode>&#x3C;Heading>\u003C/code> est utilisé sans \u003Ccode>as\u003C/code> explicite, en complément de la contrainte TypeScript (pour les fichiers \u003Ccode>.jsx\u003C/code> non typés) :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">npx\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> eslint\u003C/span>\u003Cspan style=\"color:#79B8FF\"> --rule\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '@acme/require-heading-as: error'\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> app/\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Ch3>4. Validation post-déploiement\u003C/h3>\n\u003Cp>Après le déploiement du fix, l'équipe lance un crawl Screaming Frog ciblé :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Vérification rapide via curl + grep sur un échantillon\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> url \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> $(\u003C/span>\u003Cspan style=\"color:#B392F0\">cat\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> urls-produits-sample.txt\u003C/span>\u003Cspan style=\"color:#E1E4E8\">); \u003C/span>\u003Cspan style=\"color:#F97583\">do\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  echo\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"--- \u003C/span>\u003Cspan style=\"color:#E1E4E8\">$url\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> ---\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -s\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$url\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -oP\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '&#x3C;h[1-6][^>]*>.*?&#x3C;/h[1-6]>'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> head\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -5\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">done\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Les 1 247 pages rendent désormais un \u003Ccode>&#x3C;h1>\u003C/code> pour le nom produit, des \u003Ccode>&#x3C;h2>\u003C/code> pour les sections principales.\u003C/p>\n\u003Ch3>Récupération\u003C/h3>\n\u003Cp>Le fix est déployé un mercredi soir. La récupération est progressive :\u003C/p>\n\u003Cul>\n\u003Cli>\u003Cstrong>J+3 post-fix\u003C/strong> : 410 pages recrawlées par Googlebot (vérifié via les logs serveur, user-agent \u003Ccode>Googlebot\u003C/code>).\u003C/li>\n\u003Cli>\u003Cstrong>J+7\u003C/strong> : Les impressions cessent de baisser. Stabilisation.\u003C/li>\n\u003Cli>\u003Cstrong>J+14\u003C/strong> : 80 % du trafic organique produit est récupéré. Les positions moyennes reviennent à 9.1 (vs 8.2 avant l'incident).\u003C/li>\n\u003Cli>\u003Cstrong>J+21\u003C/strong> : Retour quasi complet. Les featured snippets \"caractéristiques\" reviennent sur 9 des 12 requêtes perdues.\u003C/li>\n\u003C/ul>\n\u003Cp>Total de l'impact : 19 jours d'exposition, environ 4 100 clics perdus, 3 semaines de récupération. Le coût estimé, ramené au CPC moyen des requêtes concernées (0.85 €), dépasse 3 400 €.\u003C/p>\n\u003Cp>L'incident a aussi révélé un problème similaire sur le composant \u003Ccode>Text\u003C/code>, qui pouvait rendre des \u003Ccode>&#x3C;p>\u003C/code> ou des \u003Ccode>&#x3C;div>\u003C/code> selon un mécanisme identique. Ce composant n'avait pas d'impact SEO direct mais a été corrigé dans la foulée, par cohérence — un cas typique de \u003Ca href=\"/blog/refonte-header-le-h1-remplace-par-un-div-title-par-le-design-system\">régression liée au design system\u003C/a> qu'il faut traiter à la racine.\u003C/p>\n\u003Ch2>Ce qu'on en retient\u003C/h2>\n\u003Cp>Un composant polymorphe sans valeur par défaut sémantique est une bombe à retardement. Le problème n'est pas le pattern \u003Ccode>as\u003C/code> — c'est le \u003Ccode>div\u003C/code> comme fallback silencieux.\u003C/p>\n\u003Cp>Trois règles à graver dans la documentation du design system :\u003C/p>\n\u003Col>\n\u003Cli>\u003Cstrong>La prop \u003Ccode>as\u003C/code> d'un composant Heading ne doit jamais accepter \u003Ccode>div\u003C/code> ou \u003Ccode>span\u003C/code>.\u003C/strong> Le type TypeScript est le premier rempart.\u003C/li>\n\u003Cli>\u003Cstrong>Chaque composant sémantique doit avoir un test qui vérifie la balise HTML rendue\u003C/strong>, pas seulement le contenu ou le style.\u003C/li>\n\u003Cli>\u003Cstrong>Un crawl sémantique post-déploiement doit vérifier la présence de \u003Ccode>h1\u003C/code>-\u003Ccode>h6\u003C/code>\u003C/strong> sur un échantillon de pages critiques.\u003C/li>\n\u003C/ol>\n\u003Cp>Ce type de régression — invisible à l'œil, invisible dans les tests classiques, invisible dans les métriques pendant une semaine — est exactement ce qu'un \u003Ca href=\"/blog/design-mobile-first-h1-en-display-none-sur-desktop-invisible-pour-l-index-mobile-first\">monitoring continu de la structure HTML\u003C/a> permet de détecter avant que Search Console ne tire l'alarme. Un outil comme Seogard compare le HTML rendu côté serveur à chaque déploiement et alerte sur la disparition d'éléments sémantiques critiques — en minutes, pas en semaines.\u003C/p>\n\u003Cp>Le design system est censé garantir la cohérence. Quand il la garantit uniquement sur le plan visuel et pas sur le plan sémantique, il devient le vecteur d'une régression silencieuse à l'échelle de tout le site. Le diff faisait une ligne. L'impact a duré trois semaines.\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,11,[18,19,20,21],"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)",[25,40,53,68,82,96],{"_id":26,"slug":27,"__v":6,"author":7,"canonical":28,"category":10,"createdAt":29,"date":30,"description":31,"image":15,"imageAlt":15,"readingTime":32,"tags":33,"title":38,"updatedAt":39},"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.",12,[34,35,36,37],"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":41,"slug":42,"__v":6,"author":7,"canonical":43,"category":10,"createdAt":44,"date":45,"description":46,"image":15,"imageAlt":15,"readingTime":32,"tags":47,"title":51,"updatedAt":52},"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.",[18,35,48,49,50],"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":54,"slug":55,"__v":6,"author":7,"canonical":56,"category":57,"createdAt":58,"date":30,"description":59,"image":15,"imageAlt":15,"readingTime":32,"tags":60,"title":66,"updatedAt":67},"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","Actualités SEO","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.",[61,62,63,64,65],"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)",{"_id":69,"slug":70,"__v":6,"author":7,"canonical":71,"category":57,"createdAt":72,"date":73,"description":74,"image":15,"imageAlt":15,"readingTime":32,"tags":75,"title":80,"updatedAt":81},"6a1e720eaa6b273b0c11b618","entitymap-the-open-standard-that-gives-ai-systems-a-structured-view-of-your-business-via-sejournal-dixon-jones","https://seogard.io/blog/entitymap-the-open-standard-that-gives-ai-systems-a-structured-view-of-your-business-via-sejournal-dixon-jones","2026-06-02T06:02:54.612Z","2026-06-02","Analyse technique d'EntityMap, le fichier JSON-LD qui expose vos entités aux LLM. Implémentation, déploiement, limites et monitoring.",[76,77,63,78,79],"entitymap","structured-data","knowledge-graph","JSON-LD","EntityMap : le standard ouvert qui structure votre marque pour l'IA","Tue Jun 02 2026 06:02:54 GMT+0000 (Coordinated Universal Time)",{"_id":83,"slug":84,"__v":6,"author":7,"canonical":85,"category":86,"createdAt":87,"date":73,"description":88,"image":15,"imageAlt":15,"readingTime":32,"tags":89,"title":94,"updatedAt":95},"6a1efe60aa6b273b0c85c586","a-b-test-header-la-variante-b-sert-un-noindex-a-50-du-trafic-pendant-9-jours","https://seogard.io/blog/a-b-test-header-la-variante-b-sert-un-noindex-a-50-du-trafic-pendant-9-jours","A/B test","2026-06-02T16:01:36.997Z","Un snippet d'expérimentation injecte un meta noindex sur la variante B. 50% du crawl touché pendant 9 jours. Récit, diagnostic logs, fix.",[90,91,92,93],"a/b test","noindex","meta robots","experimentation","A/B test header : noindex servi à 50% du trafic pendant 9 jours","Tue Jun 02 2026 16:01:36 GMT+0000 (Coordinated Universal Time)",{"_id":97,"slug":98,"__v":6,"author":7,"canonical":99,"category":100,"createdAt":101,"date":45,"description":102,"image":15,"imageAlt":15,"readingTime":32,"tags":103,"title":109,"updatedAt":110},"6a1d2048aa6b273b0cfa9382","migration-vercel-vers-railway-perte-du-edge-isr-ttfb-multiplie-par-4","https://seogard.io/blog/migration-vercel-vers-railway-perte-du-edge-isr-ttfb-multiplie-par-4","Migration","2026-06-01T06:01:44.888Z","Récit d'une migration Next.js de Vercel vers Railway. Perte de l'Edge ISR, TTFB multiplié par 4, Core Web Vitals en chute. Diagnostic et fix complet.",[104,105,106,107,108],"vercel","railway","edge","isr","ttfb","Migration Vercel → Railway : TTFB ×4, 2000 pages sans Edge ISR","Mon Jun 01 2026 06:01:44 GMT+0000 (Coordinated Universal Time)",{"categories":112},[113,116,119,123,126,129,132,135,139,143,146,148,152,155,158,161,163,166],{"category":57,"slug":114,"count":115},"actualites-seo",160,{"category":100,"slug":117,"count":118},"migration",18,{"category":120,"slug":121,"count":122},"Rendering","rendering",7,{"category":124,"slug":125,"count":122},"Performance","performance",{"category":127,"slug":128,"count":122},"Crawl","crawl",{"category":130,"slug":131,"count":122},"SEO Technique","seo-technique",{"category":133,"slug":134,"count":122},"Meta Tags","meta-tags",{"category":136,"slug":137,"count":138},"Architecture","architecture",6,{"category":140,"slug":141,"count":142},"JavaScript SEO","javascript-seo",5,{"category":144,"slug":145,"count":142},"Monitoring","monitoring",{"category":147,"slug":77,"count":142},"Structured Data",{"category":149,"slug":150,"count":151},"Redirections","redirections",4,{"category":153,"slug":154,"count":151},"Outils","outils",{"category":156,"slug":157,"count":151},"E-commerce","e-commerce",{"category":159,"slug":160,"count":151},"Avancé","avance",{"category":10,"slug":48,"count":162},3,{"category":164,"slug":165,"count":162},"Contenu","contenu",{"category":167,"slug":168,"count":162},"IA & SEO","ia-seo"]