[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fYa4pEDL7f2nSZBox1_QK0oPVZVjlloTUIhnxkeJMTB8":3,"$fOY4K07D9Kg6xq2EhZybIICPumFK1OvVsAQ-TJ_G24r4":25,"$fcTFjTaPPT1sVcnh1wWEGoTWMSaOD4bwmwntkMae6w5s":100},{"_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":23,"updatedAt":24},"6a21a15daa6b273b0cb36647","lazy-load-du-hero-h1-dans-une-section-v-if-invisible-au-fetch-http-brut",0,"Equipe Seogard","# Lazy-load du hero : quand le H1 n'existe pas pour Googlebot\n\nMercredi 14h20. Un développeur front pousse une refonte du composant hero sur un site e-commerce Nuxt 3 — 320 pages catégories, 40 000 visites organiques par semaine. Le hero embarque le H1, une image plein écran, et un CTA. Pour améliorer le Largest Contentful Paint, il enveloppe la section entière dans un `v-if=\"isMounted\"`. Dans le navigateur, l'animation d'entrée est fluide. Dans le HTML brut servi au fetch HTTP, le H1 n'existe plus.\n\n## Jeudi, T+18h — L'alerte qui ne vient pas\n\nPersonne ne remarque rien. Le déploiement est passé en CI sans erreur. Les tests Cypress valident que le hero s'affiche. Lighthouse donne un score Performance en hausse de 8 points grâce au lazy-load de l'image hero. L'équipe se félicite.\n\nLe lead SEO ouvre Search Console le lundi suivant pour un reporting hebdomadaire. Les impressions sont stables. Rien d'alarmant. Il ne regarde pas les données par page — le volume global masque la tendance.\n\nC'est au jour 12 que le premier signal apparaît. Un chef de produit signale que la page catégorie \"Mobilier de bureau\" a perdu sa position 3 sur une requête à 2 400 recherches mensuelles. Le lead SEO vérifie. Position passée de 3 à 14. Il regarde les autres catégories. Neuf pages dans le top 10 ont glissé entre 5 et 15 positions.\n\nPremier réflexe : vérifier les backlinks. Rien n'a bougé. Deuxième réflexe : regarder les mises à jour de l'algorithme. Aucune annoncée. Troisième réflexe : inspecter l'URL dans Search Console avec l'outil \"Inspection d'URL\". Le HTML rendu affiché par Google montre le hero. Mais le lead SEO sait que cet outil utilise un rendu JavaScript complet — pas le fetch HTTP initial.\n\nIl ouvre un terminal.\n\n```bash\ncurl -s -A \"Googlebot\" https://www.example.com/c/mobilier-de-bureau | grep -i \"\u003Ch1\"\n```\n\nAucun résultat. Le H1 n'est pas dans la réponse HTTP.\n\nIl relance avec un user-agent Chrome classique. Même résultat. Le H1 n'est tout simplement pas dans le HTML initial. Il est injecté côté client, après hydration.\n\nL'équipe réalise à ce moment que 320 pages catégories sont servies sans H1 depuis 12 jours. Le trafic organique sur ces pages a déjà baissé de 18 %. Les requêtes brandées tiennent, mais les requêtes informationnelles et transactionnelles longue traîne décrochent.\n\nLe CTO est prévenu. Un channel Slack incident est créé. Priorité : comprendre pourquoi le SSR ne rend pas le hero.\n\n## Le bug : v-if client-only tue le H1 côté serveur\n\nLe composant hero, avant la refonte, ressemblait à ceci dans le template Nuxt 3 :\n\n```vue\n\u003Ctemplate>\n  \u003Csection class=\"hero\">\n    \u003Ch1>{{ category.title }}\u003C/h1>\n    \u003CNuxtImg :src=\"category.heroImage\" alt=\"\" />\n    \u003CNuxtLink :to=\"category.ctaLink\" class=\"cta\">{{ category.ctaText }}\u003C/NuxtLink>\n  \u003C/section>\n\u003C/template>\n```\n\nLe H1 était rendu côté serveur sans condition. Google recevait un document HTML complet avec le titre principal en place.\n\nLe développeur, soucieux de performance, a modifié le composant pour lazy-loader l'image et animer l'entrée du hero. La nouvelle version :\n\n```vue\n\u003Ctemplate>\n  \u003Csection v-if=\"isMounted\" class=\"hero\">\n    \u003Ch1>{{ category.title }}\u003C/h1>\n    \u003CNuxtImg\n      :src=\"category.heroImage\"\n      loading=\"lazy\"\n      alt=\"\"\n    />\n    \u003CNuxtLink :to=\"category.ctaLink\" class=\"cta\">{{ category.ctaText }}\u003C/NuxtLink>\n  \u003C/section>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nimport { ref, onMounted } from 'vue'\n\nconst isMounted = ref(false)\n\nonMounted(() => {\n  isMounted.value = true\n})\n\u003C/script>\n```\n\nLe piège est limpide une fois identifié. `onMounted` ne s'exécute jamais côté serveur. Dans le cycle de vie Vue 3, `onMounted` est un hook client-only. Côté SSR, `isMounted` reste à `false`. La directive `v-if=\"isMounted\"` évalue à `false`. La section entière — y compris le H1 — est exclue du HTML généré par le serveur.\n\nLe HTML servi par Nuxt en SSR pour cette page :\n\n```html\n\u003C!DOCTYPE html>\n\u003Chtml lang=\"fr\">\n\u003Chead>\n  \u003Ctitle>Mobilier de bureau — Example Store\u003C/title>\n  \u003Cmeta name=\"description\" content=\"Découvrez notre sélection...\" />\n\u003C/head>\n\u003Cbody>\n  \u003Cdiv id=\"__nuxt\">\n    \u003Cheader>\u003C!-- nav -->\u003C/header>\n    \u003Cmain>\n      \u003C!-- section hero : absente -->\n      \u003Cdiv class=\"product-grid\">\n        \u003C!-- produits -->\n      \u003C/div>\n    \u003C/main>\n    \u003Cfooter>\u003C!-- footer -->\u003C/footer>\n  \u003C/div>\n\u003C/body>\n\u003C/html>\n```\n\nPas de `\u003Ch1>`. Pas de section hero. Juste un commentaire vide dans le DOM virtuel sérialisé.\n\n### Ce que voit le développeur vs ce que voit Googlebot\n\nDans Chrome, le développeur ouvre la page. Le JavaScript s'exécute. `onMounted` tire. `isMounted` passe à `true`. Le hero apparaît en 200ms. Le H1 est visible dans l'inspecteur. Tout semble normal.\n\nGooglebot, dans sa première passe de crawl, récupère le HTML brut. Pas de H1. Google peut exécuter le JavaScript dans une seconde passe (le Web Rendering Service), mais cette seconde passe intervient [avec un délai variable](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics) — parfois quelques secondes, parfois plusieurs jours. Et même quand le WRS exécute le JS, le budget de rendering est limité. Les pages à faible PageRank passent en dernier.\n\nLe résultat concret : sur 320 pages catégories, Google a indexé pendant 26 jours des pages sans H1. Pour un moteur qui utilise le heading principal comme signal fort de pertinence thématique, c'est une amputation directe du ranking.\n\n### Pourquoi les tests n'ont rien détecté\n\nL'équipe utilisait Cypress pour les tests e2e. Cypress exécute le JavaScript. Le hero apparaissait. Le test passait.\n\nLe pipeline CI incluait un Lighthouse audit. Lighthouse rend la page avec Chromium. Le hero était rendu. Score OK.\n\nPersonne n'avait de test qui vérifiait le HTML SSR brut. Pas de `curl` dans la CI. Pas de check HTML statique. Pas de diff entre le rendu serveur et le rendu client.\n\nL'outil \"Inspection d'URL\" de Search Console, souvent utilisé comme filet de sécurité, rend lui aussi le JavaScript complet. Il montrait le H1. C'est un faux ami dans ce scénario exact.\n\nL'équipe n'avait pas non plus de crawl automatisé en mode fetch-only. Un passage avec Screaming Frog en mode \"HTML brut\" (désactiver le rendering JavaScript dans Configuration > Spider > Rendering) aurait immédiatement révélé le H1 manquant sur toutes les pages catégories.\n\nCe type de divergence entre le HTML SSR et le DOM hydraté est un classique des régressions silencieuses en Vue et Nuxt. Un scénario proche avait frappé la même stack lors d'une [migration Vue 2 vers Vue 3](/blog/migration-vue-2-vers-vue-3-47-pages-produit-en-panne-seo-en-silence), mais cette fois le composant `useHead` n'était pas en cause. C'est plus insidieux : le contenu sémantique structurant vit dans un template conditionné par un état client-only.\n\n### L'illusion du v-show\n\nUn développeur de l'équipe a suggéré de remplacer `v-if` par `v-show`. Sur le papier, `v-show` rend l'élément dans le DOM et applique un `display: none` en CSS. Le H1 serait donc dans le HTML SSR.\n\nMais un H1 en `display: none` pose un autre problème. Google a explicitement documenté que le [contenu masqué en CSS peut être dépriorisé](https://developers.google.com/search/docs/fundamentals/seo-starter-guide). Un H1 rendu mais caché en CSS n'est pas une solution propre. C'est exactement le scénario documenté dans le cas du [H1 en display none sur desktop](/blog/design-mobile-first-h1-en-display-none-sur-desktop-invisible-pour-l-index-mobile-first).\n\n## Le fix : séparer le H1 du lazy-load\n\nLa solution correcte est de ne jamais conditionner le rendu du H1 à un état client-only. Le contenu sémantique critique — H1, méta-données, contenu textuel principal — doit être dans le HTML SSR initial, inconditionnellement.\n\nLe fix déployé par l'équipe sépare le H1 de la section animée :\n\n```vue\n\u003Ctemplate>\n  \u003Csection class=\"hero\">\n    \u003Ch1>{{ category.title }}\u003C/h1>\n    \u003Cdiv v-if=\"isMounted\" class=\"hero__media\">\n      \u003CNuxtImg\n        :src=\"category.heroImage\"\n        loading=\"lazy\"\n        alt=\"\"\n      />\n      \u003CNuxtLink :to=\"category.ctaLink\" class=\"cta\">\n        {{ category.ctaText }}\n      \u003C/NuxtLink>\n    \u003C/div>\n    \u003Cdiv v-else class=\"hero__media hero__media--placeholder\" aria-hidden=\"true\">\n      \u003C!-- placeholder SSR : même dimensions, pas de contenu lourd -->\n    \u003C/div>\n  \u003C/section>\n\u003C/template>\n\n\u003Cscript setup lang=\"ts\">\nimport { ref, onMounted } from 'vue'\n\nconst isMounted = ref(false)\n\nonMounted(() => {\n  isMounted.value = true\n})\n\u003C/script>\n```\n\nLe H1 est rendu côté serveur, toujours. La partie media/CTA du hero reste lazy-loadée côté client pour la performance. Le placeholder SSR évite le layout shift.\n\nLe HTML SSR après le fix :\n\n```html\n\u003Csection class=\"hero\">\n  \u003Ch1>Mobilier de bureau\u003C/h1>\n  \u003Cdiv class=\"hero__media hero__media--placeholder\" aria-hidden=\"true\">\n    \u003C!-- placeholder -->\n  \u003C/div>\n\u003C/section>\n```\n\nLe H1 est là. Google le voit dès le premier fetch.\n\n### Vérification et redéploiement\n\nAprès le merge, l'équipe ajoute un test dans la CI pour prévenir toute régression future :\n\n```bash\n# test-ssr-h1.sh — exécuté dans le pipeline CI après build\nURL=\"http://localhost:3000/c/mobilier-de-bureau\"\nH1_COUNT=$(curl -s \"$URL\" | grep -c \"\u003Ch1\")\n\nif [ \"$H1_COUNT\" -lt 1 ]; then\n  echo \"ERREUR : aucun H1 trouvé dans le HTML SSR de $URL\"\n  exit 1\nfi\n\necho \"OK : $H1_COUNT H1 trouvé(s) dans le HTML SSR\"\n```\n\nSimple. Brutal. Efficace. Ce script tourne sur 5 URL représentatives (une catégorie, une page produit, la homepage, une page CMS, une page recherche). Temps d'exécution : 3 secondes.\n\nL'équipe lance également un crawl Screaming Frog complet en mode HTML brut (rendering JavaScript désactivé) sur les 320 pages catégories. Résultat : 320/320 ont maintenant un H1 dans le HTML SSR.\n\nLe déploiement est poussé un mardi à 10h. Les caches CDN (Cloudflare) sont purgés immédiatement via l'API :\n\n```bash\ncurl -X POST \"https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache\" \\\n  -H \"Authorization: Bearer CF_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  --data '{\"purge_everything\":true}'\n```\n\nLe lead SEO soumet les 30 URL les plus stratégiques via l'API Indexing de Search Console pour accélérer le re-crawl.\n\n### Récupération\n\nLes premiers signes de récupération apparaissent au jour 5 après le fix. Les pages à fort PageRank (celles qui reçoivent des backlinks directs) remontent en premier. Au jour 12, 80 % des positions perdues sont récupérées. Au jour 19, le trafic organique sur les pages catégories revient à son niveau pré-incident.\n\nBilan de l'incident : 26 jours d'exposition, 19 jours de récupération. Impact estimé : −12 000 visites organiques, soit environ −35 % de trafic sur les pages catégories pendant la période.\n\nLe pattern identifié ici — un composant de design system qui contrôle le rendu d'un heading sémantique — n'est pas isolé. Une variante a été documentée avec un [composant Heading qui rend un div selon une prop mal configurée](/blog/design-system-composant-heading-qui-rend-div-selon-la-prop-as-mal-configuree). Et le même type de divergence SSR/client a provoqué des incidents similaires lors de [migrations Nuxt 2 vers Nuxt 3](/blog/migration-nuxt-2-vers-nuxt-3-200-pages-en-fallback-layout-pendant-6-semaines), où des layouts entiers disparaissaient du rendu serveur.\n\nUn [header refondu qui remplace un H1 par un div](/blog/refonte-header-le-h1-remplace-par-un-div-title-par-le-design-system) est un cousin germain de ce bug. Le point commun : le développeur ne pense pas \"HTML sémantique pour le crawler\" quand il manipule des composants visuels.\n\n## Ce qu'on en retient\n\nLe H1 n'est pas un élément visuel. C'est un signal sémantique. Le conditionner à un état client-only, c'est le supprimer pour tout consommateur qui ne rend pas le JavaScript — et Googlebot en première passe en fait partie.\n\nTrois règles émergent de cet incident. Un : le H1, les headings structurants et le contenu textuel principal ne doivent jamais vivre dans un `v-if` lié à `onMounted`, `useRequestEvent`, ou tout état client-only. Deux : un test SSR en `curl` dans la CI coûte 3 secondes et attrape ce type de régression avant la production. Trois : l'outil \"Inspection d'URL\" de Search Console n'est pas un substitut à un crawl en HTML brut.\n\nUn monitoring continu qui compare le HTML SSR au DOM hydraté — comme ce que propose Seogard — détecte cette divergence en quelques minutes après le déploiement, pas 12 jours plus tard dans un reporting hebdomadaire.\n```","https://seogard.io/blog/lazy-load-du-hero-h1-dans-une-section-v-if-invisible-au-fetch-http-brut","Rendering","2026-06-04T16:01:33.508Z","2026-06-04","Un hero section en v-if masque le H1 au SSR. Récit d'une régression silencieuse sur 320 pages, diagnostic technique et fix Nuxt complet.","\u003Ch1>Lazy-load du hero : quand le H1 n'existe pas pour Googlebot\u003C/h1>\n\u003Cp>Mercredi 14h20. Un développeur front pousse une refonte du composant hero sur un site e-commerce Nuxt 3 — 320 pages catégories, 40 000 visites organiques par semaine. Le hero embarque le H1, une image plein écran, et un CTA. Pour améliorer le Largest Contentful Paint, il enveloppe la section entière dans un \u003Ccode>v-if=\"isMounted\"\u003C/code>. Dans le navigateur, l'animation d'entrée est fluide. Dans le HTML brut servi au fetch HTTP, le H1 n'existe plus.\u003C/p>\n\u003Ch2>Jeudi, T+18h — L'alerte qui ne vient pas\u003C/h2>\n\u003Cp>Personne ne remarque rien. Le déploiement est passé en CI sans erreur. Les tests Cypress valident que le hero s'affiche. Lighthouse donne un score Performance en hausse de 8 points grâce au lazy-load de l'image hero. L'équipe se félicite.\u003C/p>\n\u003Cp>Le lead SEO ouvre Search Console le lundi suivant pour un reporting hebdomadaire. Les impressions sont stables. Rien d'alarmant. Il ne regarde pas les données par page — le volume global masque la tendance.\u003C/p>\n\u003Cp>C'est au jour 12 que le premier signal apparaît. Un chef de produit signale que la page catégorie \"Mobilier de bureau\" a perdu sa position 3 sur une requête à 2 400 recherches mensuelles. Le lead SEO vérifie. Position passée de 3 à 14. Il regarde les autres catégories. Neuf pages dans le top 10 ont glissé entre 5 et 15 positions.\u003C/p>\n\u003Cp>Premier réflexe : vérifier les backlinks. Rien n'a bougé. Deuxième réflexe : regarder les mises à jour de l'algorithme. Aucune annoncée. Troisième réflexe : inspecter l'URL dans Search Console avec l'outil \"Inspection d'URL\". Le HTML rendu affiché par Google montre le hero. Mais le lead SEO sait que cet outil utilise un rendu JavaScript complet — pas le fetch HTTP initial.\u003C/p>\n\u003Cp>Il ouvre un terminal.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -s\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -A\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"Googlebot\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> https://www.example.com/c/mobilier-de-bureau\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -i\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"&#x3C;h1\"\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Aucun résultat. Le H1 n'est pas dans la réponse HTTP.\u003C/p>\n\u003Cp>Il relance avec un user-agent Chrome classique. Même résultat. Le H1 n'est tout simplement pas dans le HTML initial. Il est injecté côté client, après hydration.\u003C/p>\n\u003Cp>L'équipe réalise à ce moment que 320 pages catégories sont servies sans H1 depuis 12 jours. Le trafic organique sur ces pages a déjà baissé de 18 %. Les requêtes brandées tiennent, mais les requêtes informationnelles et transactionnelles longue traîne décrochent.\u003C/p>\n\u003Cp>Le CTO est prévenu. Un channel Slack incident est créé. Priorité : comprendre pourquoi le SSR ne rend pas le hero.\u003C/p>\n\u003Ch2>Le bug : v-if client-only tue le H1 côté serveur\u003C/h2>\n\u003Cp>Le composant hero, avant la refonte, ressemblait à ceci dans le template Nuxt 3 :\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\">template\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:#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:#E1E4E8\">>{{ category.title }}&#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\">NuxtImg\u003C/span>\u003Cspan style=\"color:#B392F0\"> :src\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"category.heroImage\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> alt\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">NuxtLink\u003C/span>\u003Cspan style=\"color:#B392F0\"> :to\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"category.ctaLink\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"cta\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>{{ category.ctaText }}&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">NuxtLink\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\">template\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le H1 était rendu côté serveur sans condition. Google recevait un document HTML complet avec le titre principal en place.\u003C/p>\n\u003Cp>Le développeur, soucieux de performance, a modifié le composant pour lazy-loader l'image et animer l'entrée du hero. La nouvelle version :\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\">template\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:#B392F0\"> v-if\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"isMounted\"\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:#E1E4E8\">>{{ category.title }}&#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\">NuxtImg\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">      :src\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"category.heroImage\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">      loading\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"lazy\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">      alt\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">NuxtLink\u003C/span>\u003Cspan style=\"color:#B392F0\"> :to\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"category.ctaLink\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"cta\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>{{ category.ctaText }}&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">NuxtLink\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\">template\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\">script\u003C/span>\u003Cspan style=\"color:#B392F0\"> setup\u003C/span>\u003Cspan style=\"color:#B392F0\"> lang\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"ts\"\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\"> { ref, onMounted } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'vue'\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\"> isMounted\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> ref\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#79B8FF\">false\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">onMounted\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\">  isMounted.value \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> true\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">})\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">script\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le piège est limpide une fois identifié. \u003Ccode>onMounted\u003C/code> ne s'exécute jamais côté serveur. Dans le cycle de vie Vue 3, \u003Ccode>onMounted\u003C/code> est un hook client-only. Côté SSR, \u003Ccode>isMounted\u003C/code> reste à \u003Ccode>false\u003C/code>. La directive \u003Ccode>v-if=\"isMounted\"\u003C/code> évalue à \u003Ccode>false\u003C/code>. La section entière — y compris le H1 — est exclue du HTML généré par le serveur.\u003C/p>\n\u003Cp>Le HTML servi par Nuxt en SSR pour cette page :\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\">DOCTYPE\u003C/span>\u003Cspan style=\"color:#B392F0\"> html\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\">html\u003C/span>\u003Cspan style=\"color:#B392F0\"> lang\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"fr\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">head\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">title\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Mobilier de bureau — Example Store&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">title\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">meta\u003C/span>\u003Cspan style=\"color:#B392F0\"> name\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"description\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> content\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"Découvrez notre sélection...\"\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\">head\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">body\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\"> id\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"__nuxt\"\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\">header\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003Cspan style=\"color:#6A737D\">&#x3C;!-- nav -->\u003C/span>\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">header\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:#6A737D\">      &#x3C;!-- section hero : absente -->\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\">\"product-grid\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">        &#x3C;!-- produits -->\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\">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\">footer\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003Cspan style=\"color:#6A737D\">&#x3C;!-- footer -->\u003C/span>\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">footer\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\">body\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\">html\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Pas de \u003Ccode>&#x3C;h1>\u003C/code>. Pas de section hero. Juste un commentaire vide dans le DOM virtuel sérialisé.\u003C/p>\n\u003Ch3>Ce que voit le développeur vs ce que voit Googlebot\u003C/h3>\n\u003Cp>Dans Chrome, le développeur ouvre la page. Le JavaScript s'exécute. \u003Ccode>onMounted\u003C/code> tire. \u003Ccode>isMounted\u003C/code> passe à \u003Ccode>true\u003C/code>. Le hero apparaît en 200ms. Le H1 est visible dans l'inspecteur. Tout semble normal.\u003C/p>\n\u003Cp>Googlebot, dans sa première passe de crawl, récupère le HTML brut. Pas de H1. Google peut exécuter le JavaScript dans une seconde passe (le Web Rendering Service), mais cette seconde passe intervient \u003Ca href=\"https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics\">avec un délai variable\u003C/a> — parfois quelques secondes, parfois plusieurs jours. Et même quand le WRS exécute le JS, le budget de rendering est limité. Les pages à faible PageRank passent en dernier.\u003C/p>\n\u003Cp>Le résultat concret : sur 320 pages catégories, Google a indexé pendant 26 jours des pages sans H1. Pour un moteur qui utilise le heading principal comme signal fort de pertinence thématique, c'est une amputation directe du ranking.\u003C/p>\n\u003Ch3>Pourquoi les tests n'ont rien détecté\u003C/h3>\n\u003Cp>L'équipe utilisait Cypress pour les tests e2e. Cypress exécute le JavaScript. Le hero apparaissait. Le test passait.\u003C/p>\n\u003Cp>Le pipeline CI incluait un Lighthouse audit. Lighthouse rend la page avec Chromium. Le hero était rendu. Score OK.\u003C/p>\n\u003Cp>Personne n'avait de test qui vérifiait le HTML SSR brut. Pas de \u003Ccode>curl\u003C/code> dans la CI. Pas de check HTML statique. Pas de diff entre le rendu serveur et le rendu client.\u003C/p>\n\u003Cp>L'outil \"Inspection d'URL\" de Search Console, souvent utilisé comme filet de sécurité, rend lui aussi le JavaScript complet. Il montrait le H1. C'est un faux ami dans ce scénario exact.\u003C/p>\n\u003Cp>L'équipe n'avait pas non plus de crawl automatisé en mode fetch-only. Un passage avec Screaming Frog en mode \"HTML brut\" (désactiver le rendering JavaScript dans Configuration > Spider > Rendering) aurait immédiatement révélé le H1 manquant sur toutes les pages catégories.\u003C/p>\n\u003Cp>Ce type de divergence entre le HTML SSR et le DOM hydraté est un classique des régressions silencieuses en Vue et Nuxt. Un scénario proche avait frappé la même stack lors d'une \u003Ca href=\"/blog/migration-vue-2-vers-vue-3-47-pages-produit-en-panne-seo-en-silence\">migration Vue 2 vers Vue 3\u003C/a>, mais cette fois le composant \u003Ccode>useHead\u003C/code> n'était pas en cause. C'est plus insidieux : le contenu sémantique structurant vit dans un template conditionné par un état client-only.\u003C/p>\n\u003Ch3>L'illusion du v-show\u003C/h3>\n\u003Cp>Un développeur de l'équipe a suggéré de remplacer \u003Ccode>v-if\u003C/code> par \u003Ccode>v-show\u003C/code>. Sur le papier, \u003Ccode>v-show\u003C/code> rend l'élément dans le DOM et applique un \u003Ccode>display: none\u003C/code> en CSS. Le H1 serait donc dans le HTML SSR.\u003C/p>\n\u003Cp>Mais un H1 en \u003Ccode>display: none\u003C/code> pose un autre problème. Google a explicitement documenté que le \u003Ca href=\"https://developers.google.com/search/docs/fundamentals/seo-starter-guide\">contenu masqué en CSS peut être dépriorisé\u003C/a>. Un H1 rendu mais caché en CSS n'est pas une solution propre. C'est exactement le scénario documenté dans le cas du \u003Ca href=\"/blog/design-mobile-first-h1-en-display-none-sur-desktop-invisible-pour-l-index-mobile-first\">H1 en display none sur desktop\u003C/a>.\u003C/p>\n\u003Ch2>Le fix : séparer le H1 du lazy-load\u003C/h2>\n\u003Cp>La solution correcte est de ne jamais conditionner le rendu du H1 à un état client-only. Le contenu sémantique critique — H1, méta-données, contenu textuel principal — doit être dans le HTML SSR initial, inconditionnellement.\u003C/p>\n\u003Cp>Le fix déployé par l'équipe sépare le H1 de la section animée :\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\">template\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:#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:#E1E4E8\">>{{ category.title }}&#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\">div\u003C/span>\u003Cspan style=\"color:#B392F0\"> v-if\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"isMounted\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"hero__media\"\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\">NuxtImg\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">        :src\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"category.heroImage\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">        loading\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"lazy\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">        alt\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">NuxtLink\u003C/span>\u003Cspan style=\"color:#B392F0\"> :to\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"category.ctaLink\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"cta\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        {{ category.ctaText }}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">NuxtLink\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\">div\u003C/span>\u003Cspan style=\"color:#B392F0\"> v-else\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"hero__media hero__media--placeholder\"\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>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">      &#x3C;!-- placeholder SSR : même dimensions, pas de contenu lourd -->\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>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">template\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\">script\u003C/span>\u003Cspan style=\"color:#B392F0\"> setup\u003C/span>\u003Cspan style=\"color:#B392F0\"> lang\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"ts\"\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\"> { ref, onMounted } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'vue'\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\"> isMounted\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> ref\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#79B8FF\">false\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">onMounted\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\">  isMounted.value \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> true\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">})\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">script\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le H1 est rendu côté serveur, toujours. La partie media/CTA du hero reste lazy-loadée côté client pour la performance. Le placeholder SSR évite le layout shift.\u003C/p>\n\u003Cp>Le HTML SSR après le fix :\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\">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:#E1E4E8\">>Mobilier de bureau&#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\">div\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"hero__media hero__media--placeholder\"\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>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    &#x3C;!-- placeholder -->\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 H1 est là. Google le voit dès le premier fetch.\u003C/p>\n\u003Ch3>Vérification et redéploiement\u003C/h3>\n\u003Cp>Après le merge, l'équipe ajoute un test dans la CI pour prévenir toute régression future :\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-ssr-h1.sh — exécuté dans le pipeline CI après build\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">URL\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"http://localhost:3000/c/mobilier-de-bureau\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">H1_COUNT\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$(\u003C/span>\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\"> -c\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"&#x3C;h1\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [ \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$H1_COUNT\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#F97583\"> -lt\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 1\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ]; \u003C/span>\u003Cspan style=\"color:#F97583\">then\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  echo\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"ERREUR : aucun H1 trouvé dans le HTML SSR de \u003C/span>\u003Cspan style=\"color:#E1E4E8\">$URL\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  exit\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 1\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">fi\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">echo\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"OK : \u003C/span>\u003Cspan style=\"color:#E1E4E8\">$H1_COUNT\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> H1 trouvé(s) dans le HTML SSR\"\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Simple. Brutal. Efficace. Ce script tourne sur 5 URL représentatives (une catégorie, une page produit, la homepage, une page CMS, une page recherche). Temps d'exécution : 3 secondes.\u003C/p>\n\u003Cp>L'équipe lance également un crawl Screaming Frog complet en mode HTML brut (rendering JavaScript désactivé) sur les 320 pages catégories. Résultat : 320/320 ont maintenant un H1 dans le HTML SSR.\u003C/p>\n\u003Cp>Le déploiement est poussé un mardi à 10h. Les caches CDN (Cloudflare) sont purgés immédiatement via l'API :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -X\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> POST\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  -H\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"Authorization: Bearer CF_TOKEN\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  -H\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"Content-Type: application/json\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  --data\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '{\"purge_everything\":true}'\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le lead SEO soumet les 30 URL les plus stratégiques via l'API Indexing de Search Console pour accélérer le re-crawl.\u003C/p>\n\u003Ch3>Récupération\u003C/h3>\n\u003Cp>Les premiers signes de récupération apparaissent au jour 5 après le fix. Les pages à fort PageRank (celles qui reçoivent des backlinks directs) remontent en premier. Au jour 12, 80 % des positions perdues sont récupérées. Au jour 19, le trafic organique sur les pages catégories revient à son niveau pré-incident.\u003C/p>\n\u003Cp>Bilan de l'incident : 26 jours d'exposition, 19 jours de récupération. Impact estimé : −12 000 visites organiques, soit environ −35 % de trafic sur les pages catégories pendant la période.\u003C/p>\n\u003Cp>Le pattern identifié ici — un composant de design system qui contrôle le rendu d'un heading sémantique — n'est pas isolé. Une variante a été documentée avec un \u003Ca href=\"/blog/design-system-composant-heading-qui-rend-div-selon-la-prop-as-mal-configuree\">composant Heading qui rend un div selon une prop mal configurée\u003C/a>. Et le même type de divergence SSR/client a provoqué des incidents similaires lors de \u003Ca href=\"/blog/migration-nuxt-2-vers-nuxt-3-200-pages-en-fallback-layout-pendant-6-semaines\">migrations Nuxt 2 vers Nuxt 3\u003C/a>, où des layouts entiers disparaissaient du rendu serveur.\u003C/p>\n\u003Cp>Un \u003Ca href=\"/blog/refonte-header-le-h1-remplace-par-un-div-title-par-le-design-system\">header refondu qui remplace un H1 par un div\u003C/a> est un cousin germain de ce bug. Le point commun : le développeur ne pense pas \"HTML sémantique pour le crawler\" quand il manipule des composants visuels.\u003C/p>\n\u003Ch2>Ce qu'on en retient\u003C/h2>\n\u003Cp>Le H1 n'est pas un élément visuel. C'est un signal sémantique. Le conditionner à un état client-only, c'est le supprimer pour tout consommateur qui ne rend pas le JavaScript — et Googlebot en première passe en fait partie.\u003C/p>\n\u003Cp>Trois règles émergent de cet incident. Un : le H1, les headings structurants et le contenu textuel principal ne doivent jamais vivre dans un \u003Ccode>v-if\u003C/code> lié à \u003Ccode>onMounted\u003C/code>, \u003Ccode>useRequestEvent\u003C/code>, ou tout état client-only. Deux : un test SSR en \u003Ccode>curl\u003C/code> dans la CI coûte 3 secondes et attrape ce type de régression avant la production. Trois : l'outil \"Inspection d'URL\" de Search Console n'est pas un substitut à un crawl en HTML brut.\u003C/p>\n\u003Cp>Un monitoring continu qui compare le HTML SSR au DOM hydraté — comme ce que propose Seogard — détecte cette divergence en quelques minutes après le déploiement, pas 12 jours plus tard dans un reporting hebdomadaire.\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,11,[18,19,20,21,22],"lazy load","hero","ssr","h1","vue","Lazy-load du hero Vue : H1 invisible pour Google","Thu Jun 04 2026 16:01:33 GMT+0000 (Coordinated Universal Time)",[26,42,52,64,77,88],{"_id":27,"slug":28,"__v":6,"author":7,"canonical":29,"category":10,"createdAt":30,"date":31,"description":32,"image":15,"imageAlt":15,"readingTime":33,"tags":34,"title":40,"updatedAt":41},"69d1b816c84600c5cb7faaad","ssr-vs-csr-impact-reel-sur-le-seo","https://seogard.io/blog/ssr-vs-csr-impact-reel-sur-le-seo","2026-04-05T01:17:10.132Z","2026-04-05","Comparaison technique SSR et CSR avec exemples de crawl, code et scénarios concrets. Ce que Googlebot voit vraiment selon votre mode de rendering.",12,[20,35,36,37,38,39],"csr","rendering","seo","javascript","googlebot","SSR vs CSR : impact réel sur le SEO technique","Sun Apr 05 2026 01:19:09 GMT+0000 (Coordinated Universal Time)",{"_id":43,"slug":44,"__v":6,"author":7,"canonical":45,"category":10,"createdAt":46,"date":31,"description":47,"image":15,"imageAlt":15,"readingTime":33,"tags":48,"title":50,"updatedAt":51},"69d1b986c84600c5cb8052d0","pourquoi-google-voit-une-page-blanche-sur-votre-spa","https://seogard.io/blog/pourquoi-google-voit-une-page-blanche-sur-votre-spa","2026-04-05T01:23:18.128Z","Diagnostic technique complet des problèmes de rendering JavaScript sur les SPA. Solutions SSR, prerendering et monitoring pour Googlebot.",[49,38,36,39,20],"spa","Google voit une page blanche sur votre SPA : diagnostic et solutions","Sun Apr 05 2026 01:25:04 GMT+0000 (Coordinated Universal Time)",{"_id":53,"slug":54,"__v":6,"author":7,"canonical":55,"category":10,"createdAt":56,"date":31,"description":57,"image":15,"imageAlt":15,"readingTime":33,"tags":58,"title":62,"updatedAt":63},"69d1bae9c84600c5cb80f61b","hydration-mismatch-le-bug-invisible-qui-tue-votre-seo","https://seogard.io/blog/hydration-mismatch-le-bug-invisible-qui-tue-votre-seo","2026-04-05T01:29:13.018Z","Détectez et corrigez les erreurs d'hydratation SSR qui dégradent silencieusement votre indexation. Méthodes, outils et code pour debug avancé.",[59,20,60,61,36],"hydration","mismatch","debug","Hydration mismatch : le bug invisible qui tue votre SEO","Sun Apr 05 2026 01:31:19 GMT+0000 (Coordinated Universal Time)",{"_id":65,"slug":66,"__v":6,"author":7,"canonical":67,"category":10,"createdAt":68,"date":31,"description":69,"image":15,"imageAlt":15,"readingTime":33,"tags":70,"title":75,"updatedAt":76},"69d1bc48c84600c5cb819800","isr-ssr-ssg-quel-mode-de-rendering-pour-le-seo","https://seogard.io/blog/isr-ssr-ssg-quel-mode-de-rendering-pour-le-seo","2026-04-05T01:35:04.805Z","Guide technique pour choisir entre ISR, SSR et SSG selon votre type de site. Comparatif, code Next.js/Nuxt, et scénarios réels e-commerce, média, SaaS.",[71,20,72,36,73,74],"isr","ssg","nextjs","nuxt","ISR, SSR, SSG : quel rendering choisir pour le SEO","Sun Apr 05 2026 01:37:13 GMT+0000 (Coordinated Universal Time)",{"_id":78,"slug":79,"__v":6,"author":7,"canonical":80,"category":10,"createdAt":81,"date":31,"description":82,"image":15,"imageAlt":15,"readingTime":33,"tags":83,"title":86,"updatedAt":87},"69d1be04c84600c5cb8262f0","prerendering-quand-et-comment-l-utiliser-pour-le-seo","https://seogard.io/blog/prerendering-quand-et-comment-l-utiliser-pour-le-seo","2026-04-05T01:42:28.066Z","Guide technique du prerendering pour le SEO : cas d'usage concrets, implémentation avec Next.js, Nuxt, Astro, et pièges à éviter sur les SPA.",[84,37,49,85,36],"prerendering","crawl","Prerendering SEO : quand et comment l'implémenter","Sun Apr 05 2026 01:44:05 GMT+0000 (Coordinated Universal Time)",{"_id":89,"slug":90,"__v":6,"author":7,"canonical":91,"category":10,"createdAt":92,"date":31,"description":93,"image":15,"imageAlt":15,"readingTime":33,"tags":94,"title":98,"updatedAt":99},"69d1c2309a9aebd273623b8e","dynamic-rendering-solution-temporaire-ou-piege-seo","https://seogard.io/blog/dynamic-rendering-solution-temporaire-ou-piege-seo","2026-04-05T02:00:13.677Z","Avantages, limites et alternatives au dynamic rendering. Pourquoi cette solution recommandée par Google devient un risque technique à mesure que votre site scale.",[95,39,96,36,97],"dynamic-rendering","cloaking","SEO technique","Dynamic rendering : solution temporaire ou piège SEO","Sun Apr 05 2026 02:02:11 GMT+0000 (Coordinated Universal Time)",{"categories":101},[102,106,110,112,116,118,121,124,128,132,135,138,142,145,148,151,155,158],{"category":103,"slug":104,"count":105},"Actualités SEO","actualites-seo",160,{"category":107,"slug":108,"count":109},"Migration","migration",18,{"category":10,"slug":36,"count":111},8,{"category":113,"slug":114,"count":115},"SEO Technique","seo-technique",7,{"category":117,"slug":85,"count":115},"Crawl",{"category":119,"slug":120,"count":115},"Meta Tags","meta-tags",{"category":122,"slug":123,"count":115},"Performance","performance",{"category":125,"slug":126,"count":127},"Architecture","architecture",6,{"category":129,"slug":130,"count":131},"Structured Data","structured-data",5,{"category":133,"slug":134,"count":131},"Monitoring","monitoring",{"category":136,"slug":137,"count":131},"JavaScript SEO","javascript-seo",{"category":139,"slug":140,"count":141},"E-commerce","e-commerce",4,{"category":143,"slug":144,"count":141},"Avancé","avance",{"category":146,"slug":147,"count":141},"Redirections","redirections",{"category":149,"slug":150,"count":141},"Outils","outils",{"category":152,"slug":153,"count":154},"IA & SEO","ia-seo",3,{"category":156,"slug":157,"count":154},"Refonte","refonte",{"category":159,"slug":160,"count":154},"Contenu","contenu"]