[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fq71D0AK474v7DWHP38Ex8_thMZEd5EgerWC3WOE8_MM":3,"$fEnjGbw5eIe9L-xTb-8bAHzrHiNTJU8Mj-qVfFCwY3w4":25},{"_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},"6a129444aa6b273b0c453fac","migration-vue-2-vers-vue-3-47-pages-produit-en-panne-seo-en-silence",0,"Equipe Seogard","# Migration Vue 2 vers Vue 3 : 47 pages produit en panne SEO pendant 21 jours\n\nJeudi 14h. Une équipe de quatre développeurs pousse en production la dernière brique d'une migration Vue 2 vers Vue 3. Le composant `ProductPage.vue` a été réécrit en Composition API. Les tests Cypress passent. Le design est pixel-perfect. Le PO valide. Trois semaines plus tard, la Search Console révèle que 47 pages produit n'ont plus de meta title ni de meta description. Le trafic organique de la catégorie a fondu de 34 %. Personne n'a reçu d'alerte.\n\n## Lundi, T+18 jours — \"Pourquoi on a perdu des clics sur les fiches produit ?\"\n\nLe lundi matin, la responsable SEO ouvre son rapport hebdomadaire Search Console. Elle repère une chute progressive sur le segment \"fiches produit\" du site — une plateforme e-commerce française spécialisée dans le matériel de sport outdoor, environ 1 200 pages indexées.\n\nLes chiffres sont nets. Sur les 21 derniers jours, le segment produit affiche :\n\n- Impressions : −41 % (de 112K à 66K)\n- Clics : −34 % (de 8 200 à 5 400)\n- Position moyenne : passée de 14.2 à 22.7\n\nPremier réflexe : vérifier si Google a lancé une mise à jour. Le [core update de mai 2026](/blog/google-may-2026-core-update-rolling-out-now) est en cours de déploiement. L'hypothèse tient cinq minutes. Mais la chute ne touche que les fiches produit, pas les pages catégories, pas le blog. Un core update ne cible pas chirurgicalement 47 URLs d'un même template.\n\nDeuxième hypothèse : un problème d'indexation. La responsable SEO ouvre l'outil d'inspection d'URL sur une fiche produit en baisse. Le rendu HTML renvoyé par Google montre un `\u003Ctitle>` vide. Pas absent du DOM — vide. La balise est là, mais son contenu est une chaîne nulle.\n\nElle inspecte la page dans Chrome. Le title s'affiche correctement dans l'onglet du navigateur. Elle fait un `View Source` : le title est bien dans le HTML initial. Elle ouvre les DevTools, regarde le `\u003Chead>` dans le DOM live : le title est là, avec le bon texte.\n\nAlors elle fait ce que tout le monde oublie de faire : elle désactive JavaScript dans Chrome et recharge la page.\n\nLe `\u003Ctitle>` est vide.\n\nElle envoie un message Slack à 9h47 : \"Les meta titles des fiches produit sont vides côté SSR. Quelqu'un a touché au composant ProductPage récemment ?\"\n\nLe lead dev répond à 10h12 : \"On a migré ce composant en Composition API il y a trois semaines. Mais les tests passent.\"\n\nLa conversation bascule. Ce n'est pas un bug mineur. C'est une régression silencieuse qui dure depuis le déploiement du 27 avril.\n\n## Le bug : useHead en Options API ne survit pas à la migration Composition API\n\nL'équipe remonte au commit du 27 avril. Le diff est clair. Avant la migration, `ProductPage.vue` utilisait l'Options API avec le plugin `vue-meta` (version 2.x, compatible Vue 2). Après la migration, le composant a été réécrit avec `\u003Cscript setup>` et `@unhead/vue` — le successeur recommandé pour Vue 3.\n\nLe problème est dans la façon dont `useHead` a été porté.\n\n### L'ancien code (Vue 2 + vue-meta)\n\n```javascript\n// ProductPage.vue — Vue 2 / Options API\nexport default {\n  name: 'ProductPage',\n  metaInfo() {\n    return {\n      title: this.product.name + ' | OutdoorShop',\n      meta: [\n        {\n          vmid: 'description',\n          name: 'description',\n          content: this.product.shortDescription\n        },\n        {\n          property: 'og:title',\n          content: this.product.name\n        }\n      ]\n    }\n  },\n  data() {\n    return {\n      product: {}\n    }\n  },\n  async created() {\n    const res = await fetch(`/api/products/${this.$route.params.slug}`)\n    this.product = await res.json()\n  }\n}\n```\n\nAvec `vue-meta` côté SSR, `metaInfo()` était résolue **après** le hook `created`. Le cycle de vie SSR de Vue 2 garantissait que `this.product` était populé avant que `vue-meta` ne collecte les balises meta pour le rendu serveur. Le title contenait le nom du produit. Tout fonctionnait.\n\n### Le nouveau code (Vue 3 + @unhead/vue)\n\n```vue\n\u003C!-- ProductPage.vue — Vue 3 / Composition API -->\n\u003Cscript setup>\nimport { ref } from 'vue'\nimport { useHead } from '@unhead/vue'\nimport { useRoute } from 'vue-router'\n\nconst route = useRoute()\nconst product = ref({})\n\nuseHead({\n  title: product.value.name\n    ? product.value.name + ' | OutdoorShop'\n    : '',\n  meta: [\n    {\n      name: 'description',\n      content: product.value.shortDescription || ''\n    },\n    {\n      property: 'og:title',\n      content: product.value.name || ''\n    }\n  ]\n})\n\nconst fetchProduct = async () => {\n  const res = await fetch(`/api/products/${route.params.slug}`)\n  product.value = await res.json()\n}\n\nfetchProduct()\n\u003C/script>\n```\n\nLe bug saute aux yeux une fois qu'on connaît le cycle de vie.\n\n`useHead()` est appelé de manière **synchrone** dans `\u003Cscript setup>`. Au moment de l'exécution, `fetchProduct()` n'a pas encore résolu. `product.value` est un objet vide. `product.value.name` est `undefined`. L'expression ternaire retombe sur la chaîne vide `''`.\n\nLe title injecté côté SSR est donc : `\"\"`.\n\nLa meta description : `\"\"`.\n\nL'og:title : `\"\"`.\n\n### Ce que voit le développeur vs ce que voit Googlebot\n\nVoici le HTML renvoyé par le serveur — ce que Googlebot reçoit :\n\n```html\n\u003C!DOCTYPE html>\n\u003Chtml lang=\"fr\">\n\u003Chead>\n  \u003Cmeta charset=\"UTF-8\">\n  \u003Ctitle>\u003C/title>\n  \u003Cmeta name=\"description\" content=\"\">\n  \u003Cmeta property=\"og:title\" content=\"\">\n  \u003C!-- ... -->\n\u003C/head>\n\u003Cbody>\n  \u003Cdiv id=\"app\">\n    \u003C!-- contenu SSR avec product data vide ou placeholder -->\n  \u003C/div>\n  \u003Cscript type=\"module\" src=\"/assets/app.a3f8b2c1.js\">\u003C/script>\n\u003C/body>\n\u003C/html>\n```\n\nLe title est une balise vide. La description est vide. Googlebot indexe la page avec un title vide, ou — dans le meilleur des cas — fabrique un title à partir du contenu visible de la page, souvent mal choisi.\n\nCôté navigateur, le scénario est différent. Après hydratation, Vue 3 monte le composant côté client. `fetchProduct()` résout le fetch. `product.value` se remplit. Mais `useHead` avec des valeurs statiques ne se met **pas** à jour. Les meta restent vides dans le DOM.\n\nSauf que le développeur ne le voit pas. Le navigateur affiche le title de la page dans l'onglet via le `document.title` initial du SSR pendant le chargement, puis l'hydratation remplace le contenu visible du `\u003Cbody>`. L'onglet du navigateur peut afficher le title SSR initial (vide) ou le mettre à jour si un autre script le change. Mais dans le cas présent, personne ne modifie `document.title` après coup.\n\nLe développeur teste autrement : il navigue sur le site en SPA, via le router. En navigation client-side, le composant se monte, le fetch résout, et si un watcher était en place, le title se mettrait à jour. Mais ici, il n'y a pas de réactivité sur `useHead` — les valeurs sont passées en brut, pas en computed.\n\nRésultat : en navigation SPA (le mode de test naturel du dev), le title peut sembler correct dans certains cas. En accès direct ou en SSR (le mode de Googlebot), le title est vide.\n\n### Pourquoi les tests n'ont rien détecté\n\nL'équipe avait des tests Cypress end-to-end. Mais les tests Cypress s'exécutent **après hydratation**, dans un navigateur headless complet. Ils vérifient le DOM final, pas le HTML SSR. Le test `cy.title().should('contain', 'Chaussure Trail')` passait — parce qu'en environnement de test, le composant chargeait les données assez vite pour que le title soit mis à jour côté client avant que Cypress ne l'évalue.\n\nAucun test ne vérifiait le HTML brut retourné par le serveur avant exécution JavaScript.\n\nPas de crawl Screaming Frog post-déploiement avec le mode \"JavaScript rendering OFF\". Pas de `curl` automatisé dans la CI. Pas de diff sur les balises meta entre deux déploiements.\n\nLe bug est passé en production sans résistance.\n\n### Vérification manuelle du problème\n\nPour confirmer, l'équipe lance un `curl` depuis le terminal :\n\n```bash\ncurl -s https://www.outdoorshop.example/produit/chaussure-trail-x500 \\\n  | grep -oP '\u003Ctitle>\\K[^\u003C]*'\n```\n\nRésultat : une ligne vide. Aucun contenu dans la balise title.\n\nPuis la même commande sur une page catégorie (non migrée, encore en Options API) :\n\n```bash\ncurl -s https://www.outdoorshop.example/categorie/chaussures-trail \\\n  | grep -oP '\u003Ctitle>\\K[^\u003C]*'\n```\n\nRésultat : `Chaussures Trail Homme & Femme | OutdoorShop`. Le title est bien présent.\n\nLe diagnostic est confirmé. Le problème touche exclusivement les 47 fiches produit dont le composant a été migré vers la Composition API le 27 avril.\n\n## Le fix : useHead réactif et vérification SSR dans la CI\n\n### Patch correctif\n\nLa correction est simple une fois le problème compris. `useHead` de `@unhead/vue` accepte des valeurs réactives — `ref`, `computed`, ou une fonction retournant un objet. Le code original passait des valeurs statiques évaluées au moment de l'appel. Il faut passer des computed.\n\n```vue\n\u003C!-- ProductPage.vue — Vue 3 / Composition API — CORRIGÉ -->\n\u003Cscript setup>\nimport { ref, computed } from 'vue'\nimport { useHead } from '@unhead/vue'\nimport { useRoute } from 'vue-router'\n\nconst route = useRoute()\nconst product = ref({})\n\nuseHead({\n  title: computed(\n    () => product.value.name\n      ? `${product.value.name} | OutdoorShop`\n      : 'OutdoorShop'\n  ),\n  meta: [\n    {\n      name: 'description',\n      content: computed(\n        () => product.value.shortDescription || ''\n      )\n    },\n    {\n      property: 'og:title',\n      content: computed(() => product.value.name || '')\n    }\n  ]\n})\n\nconst fetchProduct = async () => {\n  const res = await fetch(`/api/products/${route.params.slug}`)\n  product.value = await res.json()\n}\n\n// Attente du fetch AVANT le rendu SSR\nawait fetchProduct()\n\u003C/script>\n```\n\nDeux corrections critiques dans ce patch :\n\n1. **`computed()` autour de chaque valeur dynamique.** `useHead` observe les refs et computed. Quand `product.value` change, les balises meta se mettent à jour — côté client ET dans la collecte SSR si le fetch est résolu avant le rendu.\n\n2. **`await` devant `fetchProduct()`.** En Vue 3 avec `\u003Cscript setup>`, un `await` au top-level transforme le composant en composant asynchrone. Le serveur SSR (Vite SSR, Nuxt, ou un setup custom) **attend la résolution** avant de rendre le HTML. Sans ce `await`, le fetch est fire-and-forget, et le rendu SSR se fait avec `product.value` encore vide.\n\nLa documentation officielle de `@unhead/vue` précise explicitement que les valeurs dynamiques doivent être passées via des [refs ou computed](https://unhead.unjs.io/docs/vue/guides/reactivity) pour bénéficier de la réactivité. L'équipe avait porté la syntaxe, pas la sémantique.\n\n### Ajout d'un test SSR dans la CI\n\nL'équipe ajoute un script dans le pipeline CI qui vérifie le HTML SSR brut après chaque build :\n\n```bash\n#!/bin/bash\n# ci/check-ssr-meta.sh\n# Vérifie que les meta titles ne sont pas vides sur les pages critiques\n\nURLS=(\n  \"/produit/chaussure-trail-x500\"\n  \"/produit/veste-gore-tex-pro\"\n  \"/produit/sac-a-dos-40l\"\n  \"/categorie/chaussures-trail\"\n)\n\nEXIT_CODE=0\n\nfor path in \"${URLS[@]}\"; do\n  TITLE=$(curl -s \"http://localhost:4173${path}\" | grep -oP '\u003Ctitle>\\K[^\u003C]*')\n  \n  if [ -z \"$TITLE\" ]; then\n    echo \"FAIL: ${path} — title vide\"\n    EXIT_CODE=1\n  else\n    echo \"OK: ${path} — title: ${TITLE}\"\n  fi\ndone\n\nexit $EXIT_CODE\n```\n\nCe script tourne contre le serveur SSR de preview (port 4173 par défaut avec `vite preview`). Pas besoin de headless browser. Un simple `curl` suffit pour attraper le problème.\n\n### Cache et re-crawl\n\nLe site utilise un CDN Cloudflare avec cache HTML. Après le déploiement du fix, l'équipe purge le cache de toutes les URLs `/produit/*` via l'API Cloudflare. Sans cette purge, le CDN continuerait à servir le HTML avec les titles vides — y compris à Googlebot.\n\nL'équipe soumet ensuite les 47 URLs via l'outil d'inspection d'URL de la Search Console, puis relance un crawl Screaming Frog complet en mode \"HTML statique\" (sans exécution JavaScript) pour confirmer que chaque fiche produit retourne un title et une description non vides.\n\n### Temps de récupération\n\nLes premiers signes de récupération apparaissent 5 jours après le fix. Les impressions remontent progressivement. Au bout de 14 jours, le segment produit retrouve 91 % de son niveau de trafic d'avant la régression. Les 9 % restants mettent encore une semaine à revenir — certaines pages avaient perdu leur position en featured snippet et la reconquête est plus lente.\n\nBilan total de l'incident : 21 jours de régression + 19 jours de récupération. Soit 40 jours d'impact. Environ 5 600 clics perdus sur la période, estimés à 8 400 € de chiffre d'affaires manqué d'après le taux de conversion moyen du segment.\n\n## Ce qu'on en retient\n\nLe portage syntaxique n'est pas un portage fonctionnel. Réécrire un composant de l'Options API vers `\u003Cscript setup>` sans comprendre les différences de cycle de vie SSR crée des régressions invisibles aux tests classiques.\n\nTrois règles auraient empêché cet incident :\n\n- **Tester le HTML SSR brut**, pas seulement le DOM post-hydratation. Un `curl` dans la CI coûte zéro euro et cinq minutes de setup.\n- **Toujours passer des computed à `useHead`** quand les données viennent d'un fetch asynchrone. Les valeurs statiques dans `useHead` sont un piège documenté.\n- **Comparer les meta avant/après chaque déploiement touchant des composants de page.** Un outil de monitoring continu type Seogard détecte ce type de divergence SSR/CSR en quelques minutes — pas trois semaines après, quand la Search Console daigne remonter l'alerte.\n\nLa migration technique la plus propre du monde ne vaut rien si personne ne vérifie ce que Googlebot reçoit vraiment. Et Googlebot ne lance pas Cypress.\n```","https://seogard.io/blog/migration-vue-2-vers-vue-3-47-pages-produit-en-panne-seo-en-silence","Migration","2026-05-24T06:01:40.987Z","2026-05-24","Récit d'une migration Vue 2 vers Vue 3 où useHead mal porté a supprimé les meta titles de 47 pages produit. Diagnostic, code du bug, et fix complet.","\u003Ch1>Migration Vue 2 vers Vue 3 : 47 pages produit en panne SEO pendant 21 jours\u003C/h1>\n\u003Cp>Jeudi 14h. Une équipe de quatre développeurs pousse en production la dernière brique d'une migration Vue 2 vers Vue 3. Le composant \u003Ccode>ProductPage.vue\u003C/code> a été réécrit en Composition API. Les tests Cypress passent. Le design est pixel-perfect. Le PO valide. Trois semaines plus tard, la Search Console révèle que 47 pages produit n'ont plus de meta title ni de meta description. Le trafic organique de la catégorie a fondu de 34 %. Personne n'a reçu d'alerte.\u003C/p>\n\u003Ch2>Lundi, T+18 jours — \"Pourquoi on a perdu des clics sur les fiches produit ?\"\u003C/h2>\n\u003Cp>Le lundi matin, la responsable SEO ouvre son rapport hebdomadaire Search Console. Elle repère une chute progressive sur le segment \"fiches produit\" du site — une plateforme e-commerce française spécialisée dans le matériel de sport outdoor, environ 1 200 pages indexées.\u003C/p>\n\u003Cp>Les chiffres sont nets. Sur les 21 derniers jours, le segment produit affiche :\u003C/p>\n\u003Cul>\n\u003Cli>Impressions : −41 % (de 112K à 66K)\u003C/li>\n\u003Cli>Clics : −34 % (de 8 200 à 5 400)\u003C/li>\n\u003Cli>Position moyenne : passée de 14.2 à 22.7\u003C/li>\n\u003C/ul>\n\u003Cp>Premier réflexe : vérifier si Google a lancé une mise à jour. Le \u003Ca href=\"/blog/google-may-2026-core-update-rolling-out-now\">core update de mai 2026\u003C/a> est en cours de déploiement. L'hypothèse tient cinq minutes. Mais la chute ne touche que les fiches produit, pas les pages catégories, pas le blog. Un core update ne cible pas chirurgicalement 47 URLs d'un même template.\u003C/p>\n\u003Cp>Deuxième hypothèse : un problème d'indexation. La responsable SEO ouvre l'outil d'inspection d'URL sur une fiche produit en baisse. Le rendu HTML renvoyé par Google montre un \u003Ccode>&#x3C;title>\u003C/code> vide. Pas absent du DOM — vide. La balise est là, mais son contenu est une chaîne nulle.\u003C/p>\n\u003Cp>Elle inspecte la page dans Chrome. Le title s'affiche correctement dans l'onglet du navigateur. Elle fait un \u003Ccode>View Source\u003C/code> : le title est bien dans le HTML initial. Elle ouvre les DevTools, regarde le \u003Ccode>&#x3C;head>\u003C/code> dans le DOM live : le title est là, avec le bon texte.\u003C/p>\n\u003Cp>Alors elle fait ce que tout le monde oublie de faire : elle désactive JavaScript dans Chrome et recharge la page.\u003C/p>\n\u003Cp>Le \u003Ccode>&#x3C;title>\u003C/code> est vide.\u003C/p>\n\u003Cp>Elle envoie un message Slack à 9h47 : \"Les meta titles des fiches produit sont vides côté SSR. Quelqu'un a touché au composant ProductPage récemment ?\"\u003C/p>\n\u003Cp>Le lead dev répond à 10h12 : \"On a migré ce composant en Composition API il y a trois semaines. Mais les tests passent.\"\u003C/p>\n\u003Cp>La conversation bascule. Ce n'est pas un bug mineur. C'est une régression silencieuse qui dure depuis le déploiement du 27 avril.\u003C/p>\n\u003Ch2>Le bug : useHead en Options API ne survit pas à la migration Composition API\u003C/h2>\n\u003Cp>L'équipe remonte au commit du 27 avril. Le diff est clair. Avant la migration, \u003Ccode>ProductPage.vue\u003C/code> utilisait l'Options API avec le plugin \u003Ccode>vue-meta\u003C/code> (version 2.x, compatible Vue 2). Après la migration, le composant a été réécrit avec \u003Ccode>&#x3C;script setup>\u003C/code> et \u003Ccode>@unhead/vue\u003C/code> — le successeur recommandé pour Vue 3.\u003C/p>\n\u003Cp>Le problème est dans la façon dont \u003Ccode>useHead\u003C/code> a été porté.\u003C/p>\n\u003Ch3>L'ancien code (Vue 2 + vue-meta)\u003C/h3>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// ProductPage.vue — Vue 2 / Options API\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> default\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  name: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'ProductPage'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  metaInfo\u003C/span>\u003Cspan style=\"color:#E1E4E8\">() {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      title: \u003C/span>\u003Cspan style=\"color:#79B8FF\">this\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.product.name \u003C/span>\u003Cspan style=\"color:#F97583\">+\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> ' | OutdoorShop'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      meta: [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          vmid: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'description'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          name: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'description'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          content: \u003C/span>\u003Cspan style=\"color:#79B8FF\">this\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.product.shortDescription\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          property: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'og:title'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          content: \u003C/span>\u003Cspan style=\"color:#79B8FF\">this\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.product.name\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      ]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  data\u003C/span>\u003Cspan style=\"color:#E1E4E8\">() {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      product: {}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  async\u003C/span>\u003Cspan style=\"color:#B392F0\"> created\u003C/span>\u003Cspan style=\"color:#E1E4E8\">() {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> res\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`/api/products/${\u003C/span>\u003Cspan style=\"color:#79B8FF\">this\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$route\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">params\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    this\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.product \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> res.\u003C/span>\u003Cspan style=\"color:#B392F0\">json\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>Avec \u003Ccode>vue-meta\u003C/code> côté SSR, \u003Ccode>metaInfo()\u003C/code> était résolue \u003Cstrong>après\u003C/strong> le hook \u003Ccode>created\u003C/code>. Le cycle de vie SSR de Vue 2 garantissait que \u003Ccode>this.product\u003C/code> était populé avant que \u003Ccode>vue-meta\u003C/code> ne collecte les balises meta pour le rendu serveur. Le title contenait le nom du produit. Tout fonctionnait.\u003C/p>\n\u003Ch3>Le nouveau code (Vue 3 + @unhead/vue)\u003C/h3>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">&#x3C;!-- ProductPage.vue — Vue 3 / Composition API -->\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:#B392F0\"> setup\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 } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'vue'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { useHead } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '@unhead/vue'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { useRoute } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'vue-router'\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\"> route\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> useRoute\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\"> product\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> ref\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({})\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">useHead\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  title: product.value.name\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    ?\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> product.value.name \u003C/span>\u003Cspan style=\"color:#F97583\">+\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> ' | OutdoorShop'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    :\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> ''\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  meta: [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      name: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'description'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      content: product.value.shortDescription \u003C/span>\u003Cspan style=\"color:#F97583\">||\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\">    {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      property: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'og:title'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      content: product.value.name \u003C/span>\u003Cspan style=\"color:#F97583\">||\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\">  ]\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:#B392F0\"> fetchProduct\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> async\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> () \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> res\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`/api/products/${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">route\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">params\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  product.value \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> res.\u003C/span>\u003Cspan style=\"color:#B392F0\">json\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\">fetchProduct\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\">script\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le bug saute aux yeux une fois qu'on connaît le cycle de vie.\u003C/p>\n\u003Cp>\u003Ccode>useHead()\u003C/code> est appelé de manière \u003Cstrong>synchrone\u003C/strong> dans \u003Ccode>&#x3C;script setup>\u003C/code>. Au moment de l'exécution, \u003Ccode>fetchProduct()\u003C/code> n'a pas encore résolu. \u003Ccode>product.value\u003C/code> est un objet vide. \u003Ccode>product.value.name\u003C/code> est \u003Ccode>undefined\u003C/code>. L'expression ternaire retombe sur la chaîne vide \u003Ccode>''\u003C/code>.\u003C/p>\n\u003Cp>Le title injecté côté SSR est donc : \u003Ccode>\"\"\u003C/code>.\u003C/p>\n\u003Cp>La meta description : \u003Ccode>\"\"\u003C/code>.\u003C/p>\n\u003Cp>L'og:title : \u003Ccode>\"\"\u003C/code>.\u003C/p>\n\u003Ch3>Ce que voit le développeur vs ce que voit Googlebot\u003C/h3>\n\u003Cp>Voici le HTML renvoyé par le serveur — ce que Googlebot reçoit :\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\">meta\u003C/span>\u003Cspan style=\"color:#B392F0\"> charset\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"UTF-8\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">title\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">title\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">meta\u003C/span>\u003Cspan style=\"color:#B392F0\"> name\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"description\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> content\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">meta\u003C/span>\u003Cspan style=\"color:#B392F0\"> property\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"og:title\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> content\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  &#x3C;!-- ... -->\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\">\"app\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    &#x3C;!-- contenu SSR avec product data vide ou 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\">script\u003C/span>\u003Cspan style=\"color:#B392F0\"> type\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"module\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> src\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"/assets/app.a3f8b2c1.js\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">script\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>Le title est une balise vide. La description est vide. Googlebot indexe la page avec un title vide, ou — dans le meilleur des cas — fabrique un title à partir du contenu visible de la page, souvent mal choisi.\u003C/p>\n\u003Cp>Côté navigateur, le scénario est différent. Après hydratation, Vue 3 monte le composant côté client. \u003Ccode>fetchProduct()\u003C/code> résout le fetch. \u003Ccode>product.value\u003C/code> se remplit. Mais \u003Ccode>useHead\u003C/code> avec des valeurs statiques ne se met \u003Cstrong>pas\u003C/strong> à jour. Les meta restent vides dans le DOM.\u003C/p>\n\u003Cp>Sauf que le développeur ne le voit pas. Le navigateur affiche le title de la page dans l'onglet via le \u003Ccode>document.title\u003C/code> initial du SSR pendant le chargement, puis l'hydratation remplace le contenu visible du \u003Ccode>&#x3C;body>\u003C/code>. L'onglet du navigateur peut afficher le title SSR initial (vide) ou le mettre à jour si un autre script le change. Mais dans le cas présent, personne ne modifie \u003Ccode>document.title\u003C/code> après coup.\u003C/p>\n\u003Cp>Le développeur teste autrement : il navigue sur le site en SPA, via le router. En navigation client-side, le composant se monte, le fetch résout, et si un watcher était en place, le title se mettrait à jour. Mais ici, il n'y a pas de réactivité sur \u003Ccode>useHead\u003C/code> — les valeurs sont passées en brut, pas en computed.\u003C/p>\n\u003Cp>Résultat : en navigation SPA (le mode de test naturel du dev), le title peut sembler correct dans certains cas. En accès direct ou en SSR (le mode de Googlebot), le title est vide.\u003C/p>\n\u003Ch3>Pourquoi les tests n'ont rien détecté\u003C/h3>\n\u003Cp>L'équipe avait des tests Cypress end-to-end. Mais les tests Cypress s'exécutent \u003Cstrong>après hydratation\u003C/strong>, dans un navigateur headless complet. Ils vérifient le DOM final, pas le HTML SSR. Le test \u003Ccode>cy.title().should('contain', 'Chaussure Trail')\u003C/code> passait — parce qu'en environnement de test, le composant chargeait les données assez vite pour que le title soit mis à jour côté client avant que Cypress ne l'évalue.\u003C/p>\n\u003Cp>Aucun test ne vérifiait le HTML brut retourné par le serveur avant exécution JavaScript.\u003C/p>\n\u003Cp>Pas de crawl Screaming Frog post-déploiement avec le mode \"JavaScript rendering OFF\". Pas de \u003Ccode>curl\u003C/code> automatisé dans la CI. Pas de diff sur les balises meta entre deux déploiements.\u003C/p>\n\u003Cp>Le bug est passé en production sans résistance.\u003C/p>\n\u003Ch3>Vérification manuelle du problème\u003C/h3>\n\u003Cp>Pour confirmer, l'équipe lance un \u003Ccode>curl\u003C/code> depuis le 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:#9ECBFF\"> https://www.outdoorshop.example/produit/chaussure-trail-x500\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  |\u003C/span>\u003Cspan style=\"color:#B392F0\"> grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -oP\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '&#x3C;title>\\K[^&#x3C;]*'\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Résultat : une ligne vide. Aucun contenu dans la balise title.\u003C/p>\n\u003Cp>Puis la même commande sur une page catégorie (non migrée, encore en Options 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\"> -s\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> https://www.outdoorshop.example/categorie/chaussures-trail\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  |\u003C/span>\u003Cspan style=\"color:#B392F0\"> grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -oP\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '&#x3C;title>\\K[^&#x3C;]*'\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Résultat : \u003Ccode>Chaussures Trail Homme &#x26; Femme | OutdoorShop\u003C/code>. Le title est bien présent.\u003C/p>\n\u003Cp>Le diagnostic est confirmé. Le problème touche exclusivement les 47 fiches produit dont le composant a été migré vers la Composition API le 27 avril.\u003C/p>\n\u003Ch2>Le fix : useHead réactif et vérification SSR dans la CI\u003C/h2>\n\u003Ch3>Patch correctif\u003C/h3>\n\u003Cp>La correction est simple une fois le problème compris. \u003Ccode>useHead\u003C/code> de \u003Ccode>@unhead/vue\u003C/code> accepte des valeurs réactives — \u003Ccode>ref\u003C/code>, \u003Ccode>computed\u003C/code>, ou une fonction retournant un objet. Le code original passait des valeurs statiques évaluées au moment de l'appel. Il faut passer des computed.\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;!-- ProductPage.vue — Vue 3 / Composition API — CORRIGÉ -->\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:#B392F0\"> setup\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, computed } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'vue'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { useHead } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '@unhead/vue'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { useRoute } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'vue-router'\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\"> route\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> useRoute\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\"> product\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> ref\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({})\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">useHead\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  title: \u003C/span>\u003Cspan style=\"color:#B392F0\">computed\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    () \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> product.value.name\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      ?\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> `${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">product\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">value\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">name\u003C/span>\u003Cspan style=\"color:#9ECBFF\">} | OutdoorShop`\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      :\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'OutdoorShop'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  ),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  meta: [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      name: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'description'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      content: \u003C/span>\u003Cspan style=\"color:#B392F0\">computed\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        () \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> product.value.shortDescription \u003C/span>\u003Cspan style=\"color:#F97583\">||\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\">    },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      property: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'og:title'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      content: \u003C/span>\u003Cspan style=\"color:#B392F0\">computed\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(() \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> product.value.name \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> ''\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    }\u003C/span>\u003C/span>\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:#F97583\">const\u003C/span>\u003Cspan style=\"color:#B392F0\"> fetchProduct\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> async\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> () \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> res\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`/api/products/${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">route\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">params\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  product.value \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> res.\u003C/span>\u003Cspan style=\"color:#B392F0\">json\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:#6A737D\">// Attente du fetch AVANT le rendu SSR\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">await\u003C/span>\u003Cspan style=\"color:#B392F0\"> fetchProduct\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\">script\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Deux corrections critiques dans ce patch :\u003C/p>\n\u003Col>\n\u003Cli>\n\u003Cp>\u003Cstrong>\u003Ccode>computed()\u003C/code> autour de chaque valeur dynamique.\u003C/strong> \u003Ccode>useHead\u003C/code> observe les refs et computed. Quand \u003Ccode>product.value\u003C/code> change, les balises meta se mettent à jour — côté client ET dans la collecte SSR si le fetch est résolu avant le rendu.\u003C/p>\n\u003C/li>\n\u003Cli>\n\u003Cp>\u003Cstrong>\u003Ccode>await\u003C/code> devant \u003Ccode>fetchProduct()\u003C/code>.\u003C/strong> En Vue 3 avec \u003Ccode>&#x3C;script setup>\u003C/code>, un \u003Ccode>await\u003C/code> au top-level transforme le composant en composant asynchrone. Le serveur SSR (Vite SSR, Nuxt, ou un setup custom) \u003Cstrong>attend la résolution\u003C/strong> avant de rendre le HTML. Sans ce \u003Ccode>await\u003C/code>, le fetch est fire-and-forget, et le rendu SSR se fait avec \u003Ccode>product.value\u003C/code> encore vide.\u003C/p>\n\u003C/li>\n\u003C/ol>\n\u003Cp>La documentation officielle de \u003Ccode>@unhead/vue\u003C/code> précise explicitement que les valeurs dynamiques doivent être passées via des \u003Ca href=\"https://unhead.unjs.io/docs/vue/guides/reactivity\">refs ou computed\u003C/a> pour bénéficier de la réactivité. L'équipe avait porté la syntaxe, pas la sémantique.\u003C/p>\n\u003Ch3>Ajout d'un test SSR dans la CI\u003C/h3>\n\u003Cp>L'équipe ajoute un script dans le pipeline CI qui vérifie le HTML SSR brut après chaque build :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">#!/bin/bash\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># ci/check-ssr-meta.sh\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Vérifie que les meta titles ne sont pas vides sur les pages critiques\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">URLS\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  \"/produit/chaussure-trail-x500\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  \"/produit/veste-gore-tex-pro\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  \"/produit/sac-a-dos-40l\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  \"/categorie/chaussures-trail\"\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\">EXIT_CODE\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">0\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> path \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">URLS\u003C/span>\u003Cspan style=\"color:#9ECBFF\">[\u003C/span>\u003Cspan style=\"color:#F97583\">@\u003C/span>\u003Cspan style=\"color:#9ECBFF\">]}\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; \u003C/span>\u003Cspan style=\"color:#F97583\">do\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  TITLE\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\"> \"http://localhost:4173${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">path\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}\"\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -oP\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '&#x3C;title>\\K[^&#x3C;]*'\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:#F97583\">  if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [ \u003C/span>\u003Cspan style=\"color:#F97583\">-z\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$TITLE\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\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\"> \"FAIL: ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">path\u003C/span>\u003Cspan style=\"color:#9ECBFF\">} — title vide\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    EXIT_CODE\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">1\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  else\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    echo\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"OK: ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">path\u003C/span>\u003Cspan style=\"color:#9ECBFF\">} — title: ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">TITLE\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  fi\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">done\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">exit\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> $EXIT_CODE\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Ce script tourne contre le serveur SSR de preview (port 4173 par défaut avec \u003Ccode>vite preview\u003C/code>). Pas besoin de headless browser. Un simple \u003Ccode>curl\u003C/code> suffit pour attraper le problème.\u003C/p>\n\u003Ch3>Cache et re-crawl\u003C/h3>\n\u003Cp>Le site utilise un CDN Cloudflare avec cache HTML. Après le déploiement du fix, l'équipe purge le cache de toutes les URLs \u003Ccode>/produit/*\u003C/code> via l'API Cloudflare. Sans cette purge, le CDN continuerait à servir le HTML avec les titles vides — y compris à Googlebot.\u003C/p>\n\u003Cp>L'équipe soumet ensuite les 47 URLs via l'outil d'inspection d'URL de la Search Console, puis relance un crawl Screaming Frog complet en mode \"HTML statique\" (sans exécution JavaScript) pour confirmer que chaque fiche produit retourne un title et une description non vides.\u003C/p>\n\u003Ch3>Temps de récupération\u003C/h3>\n\u003Cp>Les premiers signes de récupération apparaissent 5 jours après le fix. Les impressions remontent progressivement. Au bout de 14 jours, le segment produit retrouve 91 % de son niveau de trafic d'avant la régression. Les 9 % restants mettent encore une semaine à revenir — certaines pages avaient perdu leur position en featured snippet et la reconquête est plus lente.\u003C/p>\n\u003Cp>Bilan total de l'incident : 21 jours de régression + 19 jours de récupération. Soit 40 jours d'impact. Environ 5 600 clics perdus sur la période, estimés à 8 400 € de chiffre d'affaires manqué d'après le taux de conversion moyen du segment.\u003C/p>\n\u003Ch2>Ce qu'on en retient\u003C/h2>\n\u003Cp>Le portage syntaxique n'est pas un portage fonctionnel. Réécrire un composant de l'Options API vers \u003Ccode>&#x3C;script setup>\u003C/code> sans comprendre les différences de cycle de vie SSR crée des régressions invisibles aux tests classiques.\u003C/p>\n\u003Cp>Trois règles auraient empêché cet incident :\u003C/p>\n\u003Cul>\n\u003Cli>\u003Cstrong>Tester le HTML SSR brut\u003C/strong>, pas seulement le DOM post-hydratation. Un \u003Ccode>curl\u003C/code> dans la CI coûte zéro euro et cinq minutes de setup.\u003C/li>\n\u003Cli>\u003Cstrong>Toujours passer des computed à \u003Ccode>useHead\u003C/code>\u003C/strong> quand les données viennent d'un fetch asynchrone. Les valeurs statiques dans \u003Ccode>useHead\u003C/code> sont un piège documenté.\u003C/li>\n\u003Cli>\u003Cstrong>Comparer les meta avant/après chaque déploiement touchant des composants de page.\u003C/strong> Un outil de monitoring continu type Seogard détecte ce type de divergence SSR/CSR en quelques minutes — pas trois semaines après, quand la Search Console daigne remonter l'alerte.\u003C/li>\n\u003C/ul>\n\u003Cp>La migration technique la plus propre du monde ne vaut rien si personne ne vérifie ce que Googlebot reçoit vraiment. Et Googlebot ne lance pas Cypress.\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,11,[18,19,20,21,22],"vue 3","migration","usehead","composition api","seo","Migration Vue 3 : 47 pages produit sans meta titles pendant 21 jours","Sun May 24 2026 06:01:40 GMT+0000 (Coordinated Universal Time)",[26,39,50],{"_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":37,"updatedAt":38},"69d7e9c3aa6b273b0c95cc57","migration-http-vers-https-checklist-seo-complete","https://seogard.io/blog/migration-http-vers-https-checklist-seo-complete","2026-04-09T18:02:43.120Z","2026-04-09","Checklist technique pour migrer de HTTP à HTTPS sans perdre de trafic organique. Redirections, HSTS, Search Console, mixed content.",14,[35,19,36,22],"https","redirections","Migration HTTP vers HTTPS : checklist SEO complète","Thu Apr 09 2026 18:02:43 GMT+0000 (Coordinated Universal Time)",{"_id":40,"slug":41,"__v":6,"author":7,"canonical":42,"category":10,"createdAt":43,"date":31,"description":44,"image":15,"imageAlt":15,"readingTime":33,"tags":45,"title":48,"updatedAt":49},"69d8372aaa6b273b0cd3ab6d","refonte-de-site-les-20-verifications-seo-indispensables","https://seogard.io/blog/refonte-de-site-les-20-verifications-seo-indispensables","2026-04-09T23:32:58.408Z","Checklist technique complète pour réussir une refonte sans perdre de trafic organique. 20 points de contrôle concrets avec code et config.",[46,19,47,22],"refonte","checklist","Refonte de site : 20 vérifications SEO indispensables","Thu Apr 09 2026 23:32:58 GMT+0000 (Coordinated Universal Time)",{"_id":51,"slug":52,"__v":6,"author":7,"canonical":53,"category":10,"createdAt":54,"date":31,"description":55,"image":15,"imageAlt":15,"readingTime":33,"tags":56,"title":60,"updatedAt":61},"69d83cc6aa6b273b0cd8286f","changer-de-framework-next-js-vers-nuxt-ou-l-inverse-sans-perte-seo","https://seogard.io/blog/changer-de-framework-next-js-vers-nuxt-ou-l-inverse-sans-perte-seo","2026-04-09T23:56:54.564Z","Guide technique complet pour migrer entre Next.js et Nuxt sans perdre de trafic organique : redirections, SSR, sitemap, monitoring et cas concret.",[19,57,58,59,22],"nextjs","nuxt","framework","Migration Next.js vers Nuxt (ou l'inverse) sans perte SEO","Thu Apr 09 2026 23:56:54 GMT+0000 (Coordinated Universal Time)"]