[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fZNbow2W0zQj_OJzwwBMNgUjXurYB2RpWezovhbIE3jQ":3,"$fVRAOZ7eE635IKWqpohCYKiloFG52FoEgRvrOxg3A210":24,"$fcTFjTaPPT1sVcnh1wWEGoTWMSaOD4bwmwntkMae6w5s":113},{"_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},"6a1fc353aa6b273b0c28a952","design-mobile-first-h1-en-display-none-sur-desktop-invisible-pour-l-index-mobile-first",0,"Equipe Seogard","# H1 en display:none sur mobile : comment un breakpoint CSS a effacé 1 200 headings de l'index Google\n\nMercredi 14h. L'équipe design d'un site éditorial français — 8 000 pages, 1,4 million de visites organiques par mois — déploie une refonte du hero banner. Le nouveau composant affiche un titre visuel stylisé sur desktop et un titre condensé sur mobile. Le code passe la QA. Le staging est validé. Lighthouse donne 94 en performance. Le vendredi soir, tout est en production. Le lundi suivant, personne ne remarque rien. Trois semaines plus tard, le trafic organique sur les pages catégories a fondu de 34 %. Le H1 n'existe plus pour Google.\n\n## Lundi, J+17 — Le dashboard vire au rouge\n\nLe responsable acquisition ouvre Looker Studio le lundi matin. Le rapport hebdomadaire montre un décrochage progressif sur les pages catégories. Pas un crash brutal — une érosion. −8 % la première semaine. −14 % la deuxième. La troisième semaine affiche −34 % cumulé sur 1 200 pages de catégories et sous-catégories.\n\nPremier réflexe : vérifier Search Console. L'onglet Performances filtre sur les pages concernées. Les impressions chutent. Les positions moyennes glissent de 6,2 à 14,7 sur les requêtes cibles. Pas de message d'erreur dans la couverture. Pas de pénalité manuelle. Pas de core update récent — le dernier [a été déployé bien avant](/blog/google-launches-core-update-amid-i-o-ai-search-overhaul-seo-pulse-via-sejournal-mattgsouthern).\n\nL'équipe SEO lance un crawl Screaming Frog sur le périmètre touché. 1 247 pages catégories. Le rapport \"H1\" remonte une anomalie : **1 203 pages ont un H1 présent dans le DOM mais marqué comme `display:none` dans le CSS calculé**. Screaming Frog le signale en warning, pas en erreur. L'outil détecte le H1 dans le HTML brut. Mais il flag le `display:none` comme un risque.\n\nLe lead SEO ouvre une page catégorie dans Chrome, redimensionne la fenêtre en desktop : le titre s'affiche. Il passe en mode responsive 375px : le titre disparaît. Un autre élément visuel le remplace — un `\u003Cspan>` stylisé en gros, sans balise heading.\n\nL'hypothèse initiale est rapide : \"C'est cosmétique, le H1 est quand même dans le DOM, Google le voit.\" L'équipe croit que le contenu masqué par CSS est simplement dépriorisé, pas ignoré.\n\nC'est faux. Et c'est exactement là que le piège se referme.\n\nGoogle utilise l'[indexation mobile-first](https://developers.google.com/search/docs/crawling-indexing/mobile/mobile-sites-mobile-first-indexing) depuis 2021. Le crawler rend la page avec un viewport mobile. Le CSS est interprété. `display:none` sur un breakpoint mobile signifie que le contenu **n'existe pas** dans le rendu crawlé. Ce n'est pas un contenu dépriorisé. C'est un contenu absent.\n\nLe moment de bascule arrive quand un dev senior utilise l'outil d'inspection d'URL de Search Console et clique sur \"Afficher la page explorée\". Le screenshot montre le rendu mobile. Pas de H1. Juste le `\u003Cspan>` décoratif. Google ne voit littéralement pas le titre principal de 1 200 pages.\n\n## Le bug : un breakpoint qui efface le heading sémantique\n\nLe composant hero a été restructuré pendant la refonte. L'ancienne version était simple :\n\n```html\n\u003C!-- Ancien composant hero — avant refonte -->\n\u003Csection class=\"hero\">\n  \u003Ch1 class=\"hero__title\">Chaussures de running homme\u003C/h1>\n  \u003Cp class=\"hero__subtitle\">Découvrez notre sélection\u003C/p>\n\u003C/section>\n```\n\nLe nouveau design introduit deux éléments distincts : un titre \"riche\" pour desktop avec un SVG décoratif, et un titre \"compact\" pour mobile. Le problème : le titre desktop porte le `\u003Ch1>`, et le titre mobile est un `\u003Cspan>`.\n\n```html\n\u003C!-- Nouveau composant hero — après refonte -->\n\u003Csection class=\"hero\">\n  \u003C!-- Titre desktop : contient le H1 -->\n  \u003Cdiv class=\"hero__title-desktop\">\n    \u003Ch1 class=\"hero__heading\">Chaussures de running homme\u003C/h1>\n    \u003Csvg class=\"hero__decoration\" aria-hidden=\"true\">\u003C!-- flourish -->\u003C/svg>\n  \u003C/div>\n\n  \u003C!-- Titre mobile : pas de heading sémantique -->\n  \u003Cdiv class=\"hero__title-mobile\">\n    \u003Cspan class=\"hero__heading-visual\">Running homme\u003C/span>\n  \u003C/div>\n\u003C/section>\n```\n\nLe CSS associé utilise une approche mobile-first classique. Les breakpoints sont écrits avec `min-width`, ce qui signifie que les styles par défaut s'appliquent au mobile :\n\n```css\n/* Base = mobile */\n.hero__title-desktop {\n  display: none; /* masqué par défaut (mobile) */\n}\n\n.hero__title-mobile {\n  display: block; /* visible par défaut (mobile) */\n}\n\n/* Desktop : à partir de 1024px */\n@media (min-width: 1024px) {\n  .hero__title-desktop {\n    display: flex;\n    align-items: center;\n    gap: 1rem;\n  }\n\n  .hero__title-mobile {\n    display: none;\n  }\n}\n```\n\nLa logique CSS est techniquement correcte pour le design. Sur un écran mobile, `hero__title-desktop` est masqué, `hero__title-mobile` est visible. Sur desktop, c'est l'inverse. Le problème : **le H1 sémantique vit uniquement dans le bloc desktop**.\n\nGooglebot rend avec un viewport de 412×823 pixels (Nexus 5X). À cette largeur, `min-width: 1024px` ne s'applique pas. Le CSS par défaut s'applique. `hero__title-desktop` est en `display:none`. Le `\u003Ch1>` disparaît du rendu.\n\nCe qui reste visible pour Google : un `\u003Cspan>` sans valeur sémantique. Pas de `\u003Ch1>`. Pas de `\u003Ch2>` de remplacement. La page devient **headingless** dans l'index.\n\n### Pourquoi les tests n'ont rien détecté\n\nTrois raisons convergentes :\n\n**1. La QA teste en desktop.** L'équipe QA vérifie le rendu sur Chrome 1440px. Le H1 est visible. Le test passe. Personne n'inspecte le DOM rendu en viewport mobile pour vérifier la présence sémantique d'un heading.\n\n**2. Screaming Frog crawle le HTML brut par défaut.** En mode statique (sans rendu JavaScript), Screaming Frog voit le H1 dans le source HTML. Il est là, dans le DOM. Le warning `display:none` existe, mais il est souvent ignoré parmi des dizaines d'autres flags. L'équipe n'avait pas configuré de rendu JavaScript dans Screaming Frog, et même avec le rendu activé, le viewport par défaut de Screaming Frog est desktop.\n\n**3. Lighthouse ne vérifie pas la présence de H1.** Lighthouse vérifie l'accessibilité des headings (ordre logique, hiérarchie), mais ne lève pas d'erreur si un H1 est en `display:none`. L'audit \"Heading elements are not in a sequentially-descending order\" reste vert tant que les headings visibles sont ordonnés — et ici, il n'y en a simplement aucun.\n\nPour reproduire exactement ce que Google voit, il faut utiliser l'outil d'inspection d'URL de Search Console, ou exécuter un fetch avec un user-agent mobile et un viewport contraint :\n\n```bash\n# Simuler le crawl mobile de Googlebot avec curl + Puppeteer headless\nnpx puppeteer-cli screenshot \\\n  --url \"https://example.com/chaussures-running-homme\" \\\n  --viewport \"412x823\" \\\n  --user-agent \"Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.126 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\" \\\n  --output rendered-mobile.png\n```\n\nLe screenshot obtenu confirme : pas de H1 visible. Seul le `\u003Cspan>` \"Running homme\" apparaît.\n\nOn peut aussi inspecter le DOM rendu programmatiquement pour détecter les headings masqués :\n\n```javascript\n// Script Puppeteer pour auditer les H1 masqués en viewport mobile\nconst puppeteer = require('puppeteer');\n\n(async () => {\n  const browser = await puppeteer.launch();\n  const page = await browser.newPage();\n  await page.setViewport({ width: 412, height: 823 });\n  await page.setUserAgent(\n    'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) ' +\n    'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.126 ' +\n    'Mobile Safari/537.36 (compatible; Googlebot/2.1)'\n  );\n\n  await page.goto('https://example.com/chaussures-running-homme', {\n    waitUntil: 'networkidle0',\n  });\n\n  const h1Status = await page.evaluate(() => {\n    const h1 = document.querySelector('h1');\n    if (!h1) return { exists: false };\n    const style = window.getComputedStyle(h1);\n    return {\n      exists: true,\n      text: h1.textContent.trim(),\n      display: style.display,\n      visibility: style.visibility,\n      opacity: style.opacity,\n      hidden: style.display === 'none' || style.visibility === 'hidden',\n    };\n  });\n\n  console.log('H1 audit result:', h1Status);\n  // Résultat attendu : { exists: true, text: \"Chaussures de running homme\",\n  //                       display: \"none\", hidden: true }\n\n  await browser.close();\n})();\n```\n\nCe script retourne `hidden: true`. Le H1 existe dans le DOM, mais son `display` calculé est `none` en viewport mobile. Google le traite comme inexistant.\n\nCe type de divergence entre ce que voit un développeur sur son écran 27 pouces et ce que voit le crawler mobile est un classique des régressions silencieuses. L'incident rappelle directement les problèmes rencontrés lors de [refontes de header où le H1 est remplacé par un div](/blog/refonte-header-le-h1-remplace-par-un-div-title-par-le-design-system) — la cause diffère, mais le résultat pour l'index est identique.\n\n### L'impact documenté par Google\n\nLa documentation officielle de Google est explicite. Le contenu en `display:none` est considéré comme [du contenu caché](https://developers.google.com/search/docs/essentials/spam-policies#hidden-text-and-links). Google ne l'ignore pas toujours — le contexte compte. Mais pour un élément structurant comme le H1, l'absence de visibilité dans le rendu mobile signifie qu'il ne contribue plus au signal de pertinence de la page.\n\nPlus problématique : sur certaines pages, Google a commencé à réécrire le title snippet dans les SERP en utilisant le `\u003Cspan>` visible (\"Running homme\") au lieu du H1 complet (\"Chaussures de running homme\"). Les CTR ont chuté parce que le snippet raccourci est moins informatif.\n\n## Le fix : un seul H1, visible partout, stylisé par viewport\n\nLe correctif repose sur un principe simple : **un seul `\u003Ch1>` dans le DOM, toujours visible, avec un style adaptatif par media query**. Pas de duplication. Pas de masquage conditionnel de heading.\n\n```html\n\u003C!-- Composant hero corrigé -->\n\u003Csection class=\"hero\">\n  \u003Cdiv class=\"hero__title-wrapper\">\n    \u003Ch1 class=\"hero__heading\">Chaussures de running homme\u003C/h1>\n    \u003Csvg class=\"hero__decoration\" aria-hidden=\"true\">\u003C!-- flourish, desktop only -->\u003C/svg>\n  \u003C/div>\n\u003C/section>\n```\n\n```css\n/* Base = mobile : titre compact */\n.hero__heading {\n  font-size: 1.5rem;\n  line-height: 1.2;\n  /* Le texte complet est toujours visible, seul le style change */\n}\n\n.hero__decoration {\n  display: none; /* SVG décoratif masqué en mobile */\n}\n\n/* Desktop */\n@media (min-width: 1024px) {\n  .hero__heading {\n    font-size: 2.75rem;\n    line-height: 1.1;\n  }\n\n  .hero__title-wrapper {\n    display: flex;\n    align-items: center;\n    gap: 1rem;\n  }\n\n  .hero__decoration {\n    display: block;\n  }\n}\n```\n\nLe H1 reste dans le flux quel que soit le viewport. Seule la typographie change. Le SVG décoratif est masqué sur mobile — c'est un élément non sémantique marqué `aria-hidden=\"true\"`, sa disparition n'a aucun impact SEO.\n\nSi le design mobile exige vraiment un texte plus court que le desktop, la technique recommandée est d'utiliser le CSS pour tronquer visuellement tout en conservant le texte complet dans le DOM accessible :\n\n```css\n/* Alternative : texte complet dans le DOM, tronqué visuellement en mobile */\n.hero__heading {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  max-width: 100%;\n}\n\n@media (min-width: 1024px) {\n  .hero__heading {\n    white-space: normal;\n    overflow: visible;\n    text-overflow: unset;\n  }\n}\n```\n\nDans ce cas, Googlebot voit le texte complet dans le DOM rendu — `overflow:hidden` ne supprime pas le contenu, il masque seulement le dépassement visuel. La différence avec `display:none` est fondamentale : le contenu existe dans le layout, il participe au rendu.\n\n### Le déploiement et la récupération\n\nLe patch a été poussé le mercredi J+19. L'équipe a pris deux précautions supplémentaires :\n\n1. **Invalidation du cache CDN** sur toutes les pages catégories via un purge ciblé Cloudflare (`cf-cache-purge` par prefix `/categorie/`). Le HTML en cache avec l'ancien composant devait être évacué immédiatement.\n\n2. **Demande d'indexation manuelle** via Search Console sur les 15 pages catégories les plus stratégiques. Pour les 1 200 autres, l'équipe a soumis le sitemap mis à jour avec des `\u003Clastmod>` rafraîchis pour signaler le changement.\n\nLe suivi quotidien dans Search Console a montré la chronologie de récupération :\n\n- **J+19 à J+22** : Google re-crawle les pages prioritaires. L'inspection d'URL confirme que le H1 est visible dans le rendu mobile.\n- **J+25** : Les positions commencent à remonter sur les 200 premières pages re-crawlées.\n- **J+33** : 80 % du trafic perdu est récupéré.\n- **J+45** : Retour au niveau pré-incident. Les positions moyennes reviennent à 6,8 (contre 6,2 avant — un léger delta résiduel qui s'est résorbé sur les deux semaines suivantes).\n\nAu total, l'incident a coûté 26 jours de visibilité dégradée et environ 210 000 clics organiques perdus sur le périmètre impacté.\n\n### Les gardes-fous ajoutés\n\nL'équipe a mis en place trois contrôles post-incident :\n\n**Un test automatisé dans la CI/CD** qui exécute Puppeteer en viewport 412×823 sur un échantillon de pages et vérifie qu'un `\u003Ch1>` visible existe :\n\n```javascript\n// test/seo/h1-mobile-visible.test.js — exécuté en CI\ndescribe('H1 visibility on mobile viewport', () => {\n  it('should have a visible H1 on category pages', async () => {\n    await page.setViewport({ width: 412, height: 823 });\n    await page.goto(TEST_URL, { waitUntil: 'networkidle0' });\n\n    const h1 = await page.evaluate(() => {\n      const el = document.querySelector('h1');\n      if (!el) return null;\n      const s = window.getComputedStyle(el);\n      return {\n        text: el.textContent.trim(),\n        visible: s.display !== 'none' && s.visibility !== 'hidden'\n                 && parseFloat(s.opacity) > 0,\n      };\n    });\n\n    expect(h1).not.toBeNull();\n    expect(h1.visible).toBe(true);\n    expect(h1.text.length).toBeGreaterThan(5);\n  });\n});\n```\n\n**Une règle Screaming Frog custom** configurée pour crawler en user-agent mobile avec un viewport de 412px, et alerter sur tout H1 en `display:none`.\n\n**Un monitoring Search Console** avec une alerte sur la position moyenne des pages catégories — seuil à +3 positions de dégradation sur 7 jours glissants.\n\nCe type de contrôle rejoint les bonnes pratiques décrites dans d'autres incidents de refonte, comme celui où un [A/B test servait un noindex à 50 % du trafic](/blog/a-b-test-header-la-variante-b-sert-un-noindex-a-50-du-trafic-pendant-9-jours) — dans les deux cas, le problème est invisible dans un navigateur classique et ne se manifeste que via le comportement réel du crawler.\n\n## Ce qu'on en retient\n\nL'indexation mobile-first n'est pas une option de configuration. C'est le mode par défaut de Google depuis des années. Pourtant, la majorité des équipes continuent à valider le SEO technique sur un écran desktop.\n\nUn H1 en `display:none` sur un breakpoint mobile n'est pas \"dépriorisé\" — il est absent. La page perd son heading principal dans l'index. Les positions glissent. Les snippets se dégradent. Et personne ne reçoit d'alerte.\n\nLe fix est simple : un seul H1, toujours dans le flux, stylisé par CSS. Jamais masqué. Si le design impose deux variantes visuelles, le texte sémantique reste unique et visible — seul l'habillage change.\n\nUn monitoring continu comme Seogard détecte cette divergence entre le DOM desktop et le rendu mobile crawlé en quelques minutes, avant que 26 jours de trafic ne disparaissent dans un breakpoint CSS.\n```","https://seogard.io/blog/design-mobile-first-h1-en-display-none-sur-desktop-invisible-pour-l-index-mobile-first","Refonte","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.","\u003Ch1>H1 en display:none sur mobile : comment un breakpoint CSS a effacé 1 200 headings de l'index Google\u003C/h1>\n\u003Cp>Mercredi 14h. L'équipe design d'un site éditorial français — 8 000 pages, 1,4 million de visites organiques par mois — déploie une refonte du hero banner. Le nouveau composant affiche un titre visuel stylisé sur desktop et un titre condensé sur mobile. Le code passe la QA. Le staging est validé. Lighthouse donne 94 en performance. Le vendredi soir, tout est en production. Le lundi suivant, personne ne remarque rien. Trois semaines plus tard, le trafic organique sur les pages catégories a fondu de 34 %. Le H1 n'existe plus pour Google.\u003C/p>\n\u003Ch2>Lundi, J+17 — Le dashboard vire au rouge\u003C/h2>\n\u003Cp>Le responsable acquisition ouvre Looker Studio le lundi matin. Le rapport hebdomadaire montre un décrochage progressif sur les pages catégories. Pas un crash brutal — une érosion. −8 % la première semaine. −14 % la deuxième. La troisième semaine affiche −34 % cumulé sur 1 200 pages de catégories et sous-catégories.\u003C/p>\n\u003Cp>Premier réflexe : vérifier Search Console. L'onglet Performances filtre sur les pages concernées. Les impressions chutent. Les positions moyennes glissent de 6,2 à 14,7 sur les requêtes cibles. Pas de message d'erreur dans la couverture. Pas de pénalité manuelle. Pas de core update récent — le dernier \u003Ca href=\"/blog/google-launches-core-update-amid-i-o-ai-search-overhaul-seo-pulse-via-sejournal-mattgsouthern\">a été déployé bien avant\u003C/a>.\u003C/p>\n\u003Cp>L'équipe SEO lance un crawl Screaming Frog sur le périmètre touché. 1 247 pages catégories. Le rapport \"H1\" remonte une anomalie : \u003Cstrong>1 203 pages ont un H1 présent dans le DOM mais marqué comme \u003Ccode>display:none\u003C/code> dans le CSS calculé\u003C/strong>. Screaming Frog le signale en warning, pas en erreur. L'outil détecte le H1 dans le HTML brut. Mais il flag le \u003Ccode>display:none\u003C/code> comme un risque.\u003C/p>\n\u003Cp>Le lead SEO ouvre une page catégorie dans Chrome, redimensionne la fenêtre en desktop : le titre s'affiche. Il passe en mode responsive 375px : le titre disparaît. Un autre élément visuel le remplace — un \u003Ccode>&#x3C;span>\u003C/code> stylisé en gros, sans balise heading.\u003C/p>\n\u003Cp>L'hypothèse initiale est rapide : \"C'est cosmétique, le H1 est quand même dans le DOM, Google le voit.\" L'équipe croit que le contenu masqué par CSS est simplement dépriorisé, pas ignoré.\u003C/p>\n\u003Cp>C'est faux. Et c'est exactement là que le piège se referme.\u003C/p>\n\u003Cp>Google utilise l'\u003Ca href=\"https://developers.google.com/search/docs/crawling-indexing/mobile/mobile-sites-mobile-first-indexing\">indexation mobile-first\u003C/a> depuis 2021. Le crawler rend la page avec un viewport mobile. Le CSS est interprété. \u003Ccode>display:none\u003C/code> sur un breakpoint mobile signifie que le contenu \u003Cstrong>n'existe pas\u003C/strong> dans le rendu crawlé. Ce n'est pas un contenu dépriorisé. C'est un contenu absent.\u003C/p>\n\u003Cp>Le moment de bascule arrive quand un dev senior utilise l'outil d'inspection d'URL de Search Console et clique sur \"Afficher la page explorée\". Le screenshot montre le rendu mobile. Pas de H1. Juste le \u003Ccode>&#x3C;span>\u003C/code> décoratif. Google ne voit littéralement pas le titre principal de 1 200 pages.\u003C/p>\n\u003Ch2>Le bug : un breakpoint qui efface le heading sémantique\u003C/h2>\n\u003Cp>Le composant hero a été restructuré pendant la refonte. L'ancienne version était simple :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">&#x3C;!-- Ancien composant hero — avant refonte -->\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:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"hero\"\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\">h1\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"hero__title\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Chaussures de running homme&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">h1\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\">p\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"hero__subtitle\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Découvrez notre sélection&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">p\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>\u003C/code>\u003C/pre>\n\u003Cp>Le nouveau design introduit deux éléments distincts : un titre \"riche\" pour desktop avec un SVG décoratif, et un titre \"compact\" pour mobile. Le problème : le titre desktop porte le \u003Ccode>&#x3C;h1>\u003C/code>, et le titre mobile est un \u003Ccode>&#x3C;span>\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\">&#x3C;!-- Nouveau composant hero — après refonte -->\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:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"hero\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  &#x3C;!-- Titre desktop : contient le H1 -->\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\">\"hero__title-desktop\"\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\">h1\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"hero__heading\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Chaussures de running homme&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">h1\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\">svg\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"hero__decoration\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> aria-hidden\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"true\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003Cspan style=\"color:#6A737D\">&#x3C;!-- flourish -->\u003C/span>\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">svg\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:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  &#x3C;!-- Titre mobile : pas de heading sémantique -->\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\">\"hero__title-mobile\"\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\">span\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"hero__heading-visual\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Running homme&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">span\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:#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>\u003C/code>\u003C/pre>\n\u003Cp>Le CSS associé utilise une approche mobile-first classique. Les breakpoints sont écrits avec \u003Ccode>min-width\u003C/code>, ce qui signifie que les styles par défaut s'appliquent au mobile :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">/* Base = mobile */\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">.hero__title-desktop\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  display\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">none\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; \u003C/span>\u003Cspan style=\"color:#6A737D\">/* masqué par défaut (mobile) */\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\">.hero__title-mobile\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  display\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">block\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; \u003C/span>\u003Cspan style=\"color:#6A737D\">/* visible par défaut (mobile) */\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:#6A737D\">/* Desktop : à partir de 1024px */\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">@media\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#79B8FF\">min-width\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">1024\u003C/span>\u003Cspan style=\"color:#F97583\">px\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  .hero__title-desktop\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    display\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">flex\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    align-items\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">center\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    gap\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#F97583\">rem\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\">  .hero__title-mobile\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    display\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">none\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>La logique CSS est techniquement correcte pour le design. Sur un écran mobile, \u003Ccode>hero__title-desktop\u003C/code> est masqué, \u003Ccode>hero__title-mobile\u003C/code> est visible. Sur desktop, c'est l'inverse. Le problème : \u003Cstrong>le H1 sémantique vit uniquement dans le bloc desktop\u003C/strong>.\u003C/p>\n\u003Cp>Googlebot rend avec un viewport de 412×823 pixels (Nexus 5X). À cette largeur, \u003Ccode>min-width: 1024px\u003C/code> ne s'applique pas. Le CSS par défaut s'applique. \u003Ccode>hero__title-desktop\u003C/code> est en \u003Ccode>display:none\u003C/code>. Le \u003Ccode>&#x3C;h1>\u003C/code> disparaît du rendu.\u003C/p>\n\u003Cp>Ce qui reste visible pour Google : un \u003Ccode>&#x3C;span>\u003C/code> sans valeur sémantique. Pas de \u003Ccode>&#x3C;h1>\u003C/code>. Pas de \u003Ccode>&#x3C;h2>\u003C/code> de remplacement. La page devient \u003Cstrong>headingless\u003C/strong> dans l'index.\u003C/p>\n\u003Ch3>Pourquoi les tests n'ont rien détecté\u003C/h3>\n\u003Cp>Trois raisons convergentes :\u003C/p>\n\u003Cp>\u003Cstrong>1. La QA teste en desktop.\u003C/strong> L'équipe QA vérifie le rendu sur Chrome 1440px. Le H1 est visible. Le test passe. Personne n'inspecte le DOM rendu en viewport mobile pour vérifier la présence sémantique d'un heading.\u003C/p>\n\u003Cp>\u003Cstrong>2. Screaming Frog crawle le HTML brut par défaut.\u003C/strong> En mode statique (sans rendu JavaScript), Screaming Frog voit le H1 dans le source HTML. Il est là, dans le DOM. Le warning \u003Ccode>display:none\u003C/code> existe, mais il est souvent ignoré parmi des dizaines d'autres flags. L'équipe n'avait pas configuré de rendu JavaScript dans Screaming Frog, et même avec le rendu activé, le viewport par défaut de Screaming Frog est desktop.\u003C/p>\n\u003Cp>\u003Cstrong>3. Lighthouse ne vérifie pas la présence de H1.\u003C/strong> Lighthouse vérifie l'accessibilité des headings (ordre logique, hiérarchie), mais ne lève pas d'erreur si un H1 est en \u003Ccode>display:none\u003C/code>. L'audit \"Heading elements are not in a sequentially-descending order\" reste vert tant que les headings visibles sont ordonnés — et ici, il n'y en a simplement aucun.\u003C/p>\n\u003Cp>Pour reproduire exactement ce que Google voit, il faut utiliser l'outil d'inspection d'URL de Search Console, ou exécuter un fetch avec un user-agent mobile et un viewport contraint :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Simuler le crawl mobile de Googlebot avec curl + Puppeteer headless\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">npx\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> puppeteer-cli\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> screenshot\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  --url\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"https://example.com/chaussures-running-homme\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  --viewport\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"412x823\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  --user-agent\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.126 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  --output\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> rendered-mobile.png\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le screenshot obtenu confirme : pas de H1 visible. Seul le \u003Ccode>&#x3C;span>\u003C/code> \"Running homme\" apparaît.\u003C/p>\n\u003Cp>On peut aussi inspecter le DOM rendu programmatiquement pour détecter les headings masqués :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Script Puppeteer pour auditer les H1 masqués en viewport mobile\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> puppeteer\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> require\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'puppeteer'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\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\"> browser\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> puppeteer.\u003C/span>\u003Cspan style=\"color:#B392F0\">launch\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\"> page\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> browser.\u003C/span>\u003Cspan style=\"color:#B392F0\">newPage\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> page.\u003C/span>\u003Cspan style=\"color:#B392F0\">setViewport\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({ width: \u003C/span>\u003Cspan style=\"color:#79B8FF\">412\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, height: \u003C/span>\u003Cspan style=\"color:#79B8FF\">823\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> page.\u003C/span>\u003Cspan style=\"color:#B392F0\">setUserAgent\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) '\u003C/span>\u003Cspan style=\"color:#F97583\"> +\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.126 '\u003C/span>\u003Cspan style=\"color:#F97583\"> +\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'Mobile Safari/537.36 (compatible; Googlebot/2.1)'\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\">  await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> page.\u003C/span>\u003Cspan style=\"color:#B392F0\">goto\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'https://example.com/chaussures-running-homme'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    waitUntil: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'networkidle0'\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\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> h1Status\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> page.\u003C/span>\u003Cspan style=\"color:#B392F0\">evaluate\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\"> h1\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> document.\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>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">h1) \u003C/span>\u003Cspan style=\"color:#F97583\">return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { exists: \u003C/span>\u003Cspan style=\"color:#79B8FF\">false\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\"> style\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> window.\u003C/span>\u003Cspan style=\"color:#B392F0\">getComputedStyle\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(h1);\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\">      exists: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      text: h1.textContent.\u003C/span>\u003Cspan style=\"color:#B392F0\">trim\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      display: style.display,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      visibility: style.visibility,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      opacity: style.opacity,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      hidden: style.display \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'none'\u003C/span>\u003Cspan style=\"color:#F97583\"> ||\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> style.visibility \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'hidden'\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:#E1E4E8\">  console.\u003C/span>\u003Cspan style=\"color:#B392F0\">log\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'H1 audit result:'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, h1Status);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  // Résultat attendu : { exists: true, text: \"Chaussures de running homme\",\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  //                       display: \"none\", hidden: true }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> browser.\u003C/span>\u003Cspan style=\"color:#B392F0\">close\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>Ce script retourne \u003Ccode>hidden: true\u003C/code>. Le H1 existe dans le DOM, mais son \u003Ccode>display\u003C/code> calculé est \u003Ccode>none\u003C/code> en viewport mobile. Google le traite comme inexistant.\u003C/p>\n\u003Cp>Ce type de divergence entre ce que voit un développeur sur son écran 27 pouces et ce que voit le crawler mobile est un classique des régressions silencieuses. L'incident rappelle directement les problèmes rencontrés lors de \u003Ca href=\"/blog/refonte-header-le-h1-remplace-par-un-div-title-par-le-design-system\">refontes de header où le H1 est remplacé par un div\u003C/a> — la cause diffère, mais le résultat pour l'index est identique.\u003C/p>\n\u003Ch3>L'impact documenté par Google\u003C/h3>\n\u003Cp>La documentation officielle de Google est explicite. Le contenu en \u003Ccode>display:none\u003C/code> est considéré comme \u003Ca href=\"https://developers.google.com/search/docs/essentials/spam-policies#hidden-text-and-links\">du contenu caché\u003C/a>. Google ne l'ignore pas toujours — le contexte compte. Mais pour un élément structurant comme le H1, l'absence de visibilité dans le rendu mobile signifie qu'il ne contribue plus au signal de pertinence de la page.\u003C/p>\n\u003Cp>Plus problématique : sur certaines pages, Google a commencé à réécrire le title snippet dans les SERP en utilisant le \u003Ccode>&#x3C;span>\u003C/code> visible (\"Running homme\") au lieu du H1 complet (\"Chaussures de running homme\"). Les CTR ont chuté parce que le snippet raccourci est moins informatif.\u003C/p>\n\u003Ch2>Le fix : un seul H1, visible partout, stylisé par viewport\u003C/h2>\n\u003Cp>Le correctif repose sur un principe simple : \u003Cstrong>un seul \u003Ccode>&#x3C;h1>\u003C/code> dans le DOM, toujours visible, avec un style adaptatif par media query\u003C/strong>. Pas de duplication. Pas de masquage conditionnel de heading.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">&#x3C;!-- Composant hero corrigé -->\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:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"hero\"\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\">\"hero__title-wrapper\"\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\">h1\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"hero__heading\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Chaussures de running homme&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">h1\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\">svg\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"hero__decoration\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> aria-hidden\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"true\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003Cspan style=\"color:#6A737D\">&#x3C;!-- flourish, desktop only -->\u003C/span>\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">svg\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:#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>\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\">/* Base = mobile : titre compact */\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">.hero__heading\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  font-size\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">1.5\u003C/span>\u003Cspan style=\"color:#F97583\">rem\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  line-height\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">1.2\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  /* Le texte complet est toujours visible, seul le style change */\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\">.hero__decoration\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  display\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">none\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; \u003C/span>\u003Cspan style=\"color:#6A737D\">/* SVG décoratif masqué en mobile */\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:#6A737D\">/* Desktop */\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">@media\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#79B8FF\">min-width\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">1024\u003C/span>\u003Cspan style=\"color:#F97583\">px\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  .hero__heading\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    font-size\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">2.75\u003C/span>\u003Cspan style=\"color:#F97583\">rem\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    line-height\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">1.1\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\">  .hero__title-wrapper\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    display\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">flex\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    align-items\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">center\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    gap\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#F97583\">rem\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\">  .hero__decoration\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    display\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">block\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 H1 reste dans le flux quel que soit le viewport. Seule la typographie change. Le SVG décoratif est masqué sur mobile — c'est un élément non sémantique marqué \u003Ccode>aria-hidden=\"true\"\u003C/code>, sa disparition n'a aucun impact SEO.\u003C/p>\n\u003Cp>Si le design mobile exige vraiment un texte plus court que le desktop, la technique recommandée est d'utiliser le CSS pour tronquer visuellement tout en conservant le texte complet dans le DOM accessible :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">/* Alternative : texte complet dans le DOM, tronqué visuellement en mobile */\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">.hero__heading\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  overflow\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">hidden\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  text-overflow\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">ellipsis\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  white-space\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">nowrap\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  max-width\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">100\u003C/span>\u003Cspan style=\"color:#F97583\">%\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">@media\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#79B8FF\">min-width\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">1024\u003C/span>\u003Cspan style=\"color:#F97583\">px\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  .hero__heading\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    white-space\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">normal\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    overflow\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">visible\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    text-overflow\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">unset\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>Dans ce cas, Googlebot voit le texte complet dans le DOM rendu — \u003Ccode>overflow:hidden\u003C/code> ne supprime pas le contenu, il masque seulement le dépassement visuel. La différence avec \u003Ccode>display:none\u003C/code> est fondamentale : le contenu existe dans le layout, il participe au rendu.\u003C/p>\n\u003Ch3>Le déploiement et la récupération\u003C/h3>\n\u003Cp>Le patch a été poussé le mercredi J+19. L'équipe a pris deux précautions supplémentaires :\u003C/p>\n\u003Col>\n\u003Cli>\n\u003Cp>\u003Cstrong>Invalidation du cache CDN\u003C/strong> sur toutes les pages catégories via un purge ciblé Cloudflare (\u003Ccode>cf-cache-purge\u003C/code> par prefix \u003Ccode>/categorie/\u003C/code>). Le HTML en cache avec l'ancien composant devait être évacué immédiatement.\u003C/p>\n\u003C/li>\n\u003Cli>\n\u003Cp>\u003Cstrong>Demande d'indexation manuelle\u003C/strong> via Search Console sur les 15 pages catégories les plus stratégiques. Pour les 1 200 autres, l'équipe a soumis le sitemap mis à jour avec des \u003Ccode>&#x3C;lastmod>\u003C/code> rafraîchis pour signaler le changement.\u003C/p>\n\u003C/li>\n\u003C/ol>\n\u003Cp>Le suivi quotidien dans Search Console a montré la chronologie de récupération :\u003C/p>\n\u003Cul>\n\u003Cli>\u003Cstrong>J+19 à J+22\u003C/strong> : Google re-crawle les pages prioritaires. L'inspection d'URL confirme que le H1 est visible dans le rendu mobile.\u003C/li>\n\u003Cli>\u003Cstrong>J+25\u003C/strong> : Les positions commencent à remonter sur les 200 premières pages re-crawlées.\u003C/li>\n\u003Cli>\u003Cstrong>J+33\u003C/strong> : 80 % du trafic perdu est récupéré.\u003C/li>\n\u003Cli>\u003Cstrong>J+45\u003C/strong> : Retour au niveau pré-incident. Les positions moyennes reviennent à 6,8 (contre 6,2 avant — un léger delta résiduel qui s'est résorbé sur les deux semaines suivantes).\u003C/li>\n\u003C/ul>\n\u003Cp>Au total, l'incident a coûté 26 jours de visibilité dégradée et environ 210 000 clics organiques perdus sur le périmètre impacté.\u003C/p>\n\u003Ch3>Les gardes-fous ajoutés\u003C/h3>\n\u003Cp>L'équipe a mis en place trois contrôles post-incident :\u003C/p>\n\u003Cp>\u003Cstrong>Un test automatisé dans la CI/CD\u003C/strong> qui exécute Puppeteer en viewport 412×823 sur un échantillon de pages et vérifie qu'un \u003Ccode>&#x3C;h1>\u003C/code> visible existe :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// test/seo/h1-mobile-visible.test.js — exécuté en CI\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">describe\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'H1 visibility on mobile viewport'\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 have a visible H1 on category pages'\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\">    await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> page.\u003C/span>\u003Cspan style=\"color:#B392F0\">setViewport\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({ width: \u003C/span>\u003Cspan style=\"color:#79B8FF\">412\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, height: \u003C/span>\u003Cspan style=\"color:#79B8FF\">823\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> page.\u003C/span>\u003Cspan style=\"color:#B392F0\">goto\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#79B8FF\">TEST_URL\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, { waitUntil: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'networkidle0'\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\"> h1\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> page.\u003C/span>\u003Cspan style=\"color:#B392F0\">evaluate\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\"> el\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> document.\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>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">el) \u003C/span>\u003Cspan style=\"color:#F97583\">return\u003C/span>\u003Cspan style=\"color:#79B8FF\"> null\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\"> s\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> window.\u003C/span>\u003Cspan style=\"color:#B392F0\">getComputedStyle\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(el);\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\">        text: el.textContent.\u003C/span>\u003Cspan style=\"color:#B392F0\">trim\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        visible: s.display \u003C/span>\u003Cspan style=\"color:#F97583\">!==\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'none'\u003C/span>\u003Cspan style=\"color:#F97583\"> &#x26;&#x26;\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> s.visibility \u003C/span>\u003Cspan style=\"color:#F97583\">!==\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'hidden'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">                 &#x26;&#x26;\u003C/span>\u003Cspan style=\"color:#B392F0\"> parseFloat\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(s.opacity) \u003C/span>\u003Cspan style=\"color:#F97583\">>\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>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(h1).not.\u003C/span>\u003Cspan style=\"color:#B392F0\">toBeNull\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(h1.visible).\u003C/span>\u003Cspan style=\"color:#B392F0\">toBe\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#79B8FF\">true\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\">(h1.text.\u003C/span>\u003Cspan style=\"color:#79B8FF\">length\u003C/span>\u003Cspan style=\"color:#E1E4E8\">).\u003C/span>\u003Cspan style=\"color:#B392F0\">toBeGreaterThan\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#79B8FF\">5\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>\u003Cstrong>Une règle Screaming Frog custom\u003C/strong> configurée pour crawler en user-agent mobile avec un viewport de 412px, et alerter sur tout H1 en \u003Ccode>display:none\u003C/code>.\u003C/p>\n\u003Cp>\u003Cstrong>Un monitoring Search Console\u003C/strong> avec une alerte sur la position moyenne des pages catégories — seuil à +3 positions de dégradation sur 7 jours glissants.\u003C/p>\n\u003Cp>Ce type de contrôle rejoint les bonnes pratiques décrites dans d'autres incidents de refonte, comme celui où un \u003Ca href=\"/blog/a-b-test-header-la-variante-b-sert-un-noindex-a-50-du-trafic-pendant-9-jours\">A/B test servait un noindex à 50 % du trafic\u003C/a> — dans les deux cas, le problème est invisible dans un navigateur classique et ne se manifeste que via le comportement réel du crawler.\u003C/p>\n\u003Ch2>Ce qu'on en retient\u003C/h2>\n\u003Cp>L'indexation mobile-first n'est pas une option de configuration. C'est le mode par défaut de Google depuis des années. Pourtant, la majorité des équipes continuent à valider le SEO technique sur un écran desktop.\u003C/p>\n\u003Cp>Un H1 en \u003Ccode>display:none\u003C/code> sur un breakpoint mobile n'est pas \"dépriorisé\" — il est absent. La page perd son heading principal dans l'index. Les positions glissent. Les snippets se dégradent. Et personne ne reçoit d'alerte.\u003C/p>\n\u003Cp>Le fix est simple : un seul H1, toujours dans le flux, stylisé par CSS. Jamais masqué. Si le design impose deux variantes visuelles, le texte sémantique reste unique et visible — seul l'habillage change.\u003C/p>\n\u003Cp>Un monitoring continu comme Seogard détecte cette divergence entre le DOM desktop et le rendu mobile crawlé en quelques minutes, avant que 26 jours de trafic ne disparaissent dans un breakpoint CSS.\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,12,[18,19,20,21],"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)",[25,39,55,69,84,99],{"_id":26,"slug":27,"__v":6,"author":7,"canonical":28,"category":10,"createdAt":29,"date":30,"description":31,"image":15,"imageAlt":15,"readingTime":16,"tags":32,"title":37,"updatedAt":38},"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,19,34,35,36],"design system","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":40,"slug":41,"__v":6,"author":7,"canonical":42,"category":43,"createdAt":44,"date":45,"description":46,"image":15,"imageAlt":15,"readingTime":16,"tags":47,"title":53,"updatedAt":54},"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","Actualités SEO","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.",[48,49,50,51,52],"entitymap","structured-data","AI search","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":56,"slug":57,"__v":6,"author":7,"canonical":58,"category":59,"createdAt":60,"date":45,"description":61,"image":15,"imageAlt":15,"readingTime":16,"tags":62,"title":67,"updatedAt":68},"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.",[63,64,65,66],"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":70,"slug":71,"__v":6,"author":7,"canonical":72,"category":73,"createdAt":74,"date":30,"description":75,"image":15,"imageAlt":15,"readingTime":16,"tags":76,"title":82,"updatedAt":83},"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.",[77,78,79,80,81],"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)",{"_id":85,"slug":86,"__v":6,"author":7,"canonical":87,"category":73,"createdAt":88,"date":89,"description":90,"image":15,"imageAlt":15,"readingTime":16,"tags":91,"title":97,"updatedAt":98},"6a1bceceaa6b273b0ce39021","migration-cloudflare-vers-bunny-cdn-regles-redirect-https-oubliees","https://seogard.io/blog/migration-cloudflare-vers-bunny-cdn-regles-redirect-https-oubliees","2026-05-31T06:01:50.903Z","2026-05-31","Récit d'une migration CDN où les Page Rules Cloudflare n'ont pas été portées vers Bunny. 302 silencieux, jus SEO perdu, et fix complet.",[92,93,94,95,96],"cloudflare","bunny cdn","redirect 301","migration cdn","seo technique","Migration Cloudflare → Bunny CDN : 302 au lieu de 301 pendant 2 mois","Sun May 31 2026 06:01:50 GMT+0000 (Coordinated Universal Time)",{"_id":100,"slug":101,"__v":6,"author":7,"canonical":102,"category":43,"createdAt":103,"date":89,"description":104,"image":15,"imageAlt":15,"readingTime":16,"tags":105,"title":111,"updatedAt":112},"6a1c5bbfaa6b273b0c57e754","google-s-i-o-demos-reveal-the-new-business-visibility-problem-via-sejournal-mattgsouthern","https://seogard.io/blog/google-s-i-o-demos-reveal-the-new-business-visibility-problem-via-sejournal-mattgsouthern","2026-05-31T16:03:11.265Z","Les démos Google I/O finalisent des transactions sans jamais montrer de site. Analyse technique du nouveau problème de visibilité business et comment s'y préparer.",[106,107,108,109,110],"google i/o","ai search","business visibility","structured data","answer engine optimization","Google I/O 2026 : le problème de visibilité business que les démos révèlent","Sun May 31 2026 16:03:11 GMT+0000 (Coordinated Universal Time)",{"categories":114},[115,118,121,125,128,131,134,137,141,145,148,150,154,157,160,163,167],{"category":43,"slug":116,"count":117},"actualites-seo",159,{"category":73,"slug":119,"count":120},"migration",18,{"category":122,"slug":123,"count":124},"Crawl","crawl",7,{"category":126,"slug":127,"count":124},"Rendering","rendering",{"category":129,"slug":130,"count":124},"Performance","performance",{"category":132,"slug":133,"count":124},"Meta Tags","meta-tags",{"category":135,"slug":136,"count":124},"SEO Technique","seo-technique",{"category":138,"slug":139,"count":140},"Architecture","architecture",6,{"category":142,"slug":143,"count":144},"Monitoring","monitoring",5,{"category":146,"slug":147,"count":144},"JavaScript SEO","javascript-seo",{"category":149,"slug":49,"count":144},"Structured Data",{"category":151,"slug":152,"count":153},"Outils","outils",4,{"category":155,"slug":156,"count":153},"Avancé","avance",{"category":158,"slug":159,"count":153},"Redirections","redirections",{"category":161,"slug":162,"count":153},"E-commerce","e-commerce",{"category":164,"slug":165,"count":166},"Contenu","contenu",3,{"category":168,"slug":169,"count":166},"IA & SEO","ia-seo"]