[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$f9HJYv0Ag7reDEceqpNeFzzyg0JUOVpThTJXdCinsEKU":3,"$fK2F8s0FxpLT8P3PcYgdZnzDyEKuAtu74IIgmmIQ0uWI":25,"$fcTFjTaPPT1sVcnh1wWEGoTWMSaOD4bwmwntkMae6w5s":106},{"_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},"6a28fdbcaa6b273b0cc7b544","nuxt-useseometa-le-child-override-silencieusement-les-meta-du-layout-parent",0,"Equipe Seogard","# Nuxt useSeoMeta : quand le composant enfant efface silencieusement les meta du layout parent\n\nJeudi 16h20. L'équipe SEO d'une marketplace B2B française (12 000 références, 2 400 pages catégories) pousse en production un nouveau layout Nuxt 3 avec des meta par défaut centralisés via `useSeoMeta`. Le navigateur affiche les bonnes balises. Lighthouse score 100 en SEO. Le merge request est approuvé. Personne ne regarde le rendu SSR brut des pages enfants. Dix-neuf jours plus tard, Search Console signale un effondrement des impressions sur 340 URLs catégories. Les `og:description`, `og:image` et `description` ont disparu — remplacés par rien.\n\n## Lundi, T+19 jours — \"C'est quoi ce trou dans les impressions ?\"\n\nLe lead SEO ouvre Search Console le lundi à 9h15. Le rapport Performances affiche une chute de 34 % des impressions sur les pages catégories. Pas les fiches produit. Pas le blog. Uniquement les catégories.\n\nPremier réflexe : vérifier si Google a signalé un problème d'indexation. Onglet \"Pages\" → aucune alerte. Tout est marqué \"Indexée\". L'équipe suppose d'abord un changement algorithmique. Le dev frontend vérifie la branche `main` — aucun déploiement depuis quatre jours. Le CTO pense à un problème CDN.\n\nÀ 10h30, le lead SEO lance un crawl Screaming Frog sur les 2 400 URLs catégories. Résultat : 340 pages retournent une balise `\u003Cmeta name=\"description\">` vide. Pas absente — vide. `content=\"\"`. Idem pour `og:description` et `og:image`.\n\nLe dev frontend ouvre une de ces pages dans Chrome, inspecte le DOM. La meta description est bien là, remplie. Il rafraîchit. Toujours là. \"Chez moi ça marche.\"\n\nÀ 11h15, quelqu'un a le réflexe de faire un `curl` brut :\n\n```bash\ncurl -s https://marketplace.example.com/categorie/outillage-electrique | grep -i 'og:description'\n```\n\nRésultat :\n\n```html\n\u003Cmeta property=\"og:description\" content=\"\">\n```\n\nLe contenu est vide dans le HTML SSR. Côté navigateur, l'hydratation côté client corrige la meta après le mount du composant. Googlebot, qui utilise le HTML initial pour extraire les meta, ne voit rien.\n\nÀ 11h45, l'équipe ouvre le diff Git du déploiement du 19 jours plus tôt. Le layout `default.vue` a été refactoré. Et un composant enfant — `CategoryHead.vue` — a été ajouté dans certaines pages catégories. Le lead SEO commence à comprendre. Le problème n'est pas côté serveur, ni côté CDN. C'est un problème de composition de `useSeoMeta` entre layout parent et page enfant.\n\nL'impact mesuré a posteriori : −18 000 clics sur 19 jours, 340 pages touchées, temps moyen de récupération estimé à 10-14 jours après fix.\n\n## Le bug : useSeoMeta dans le child écrase le layout, même avec des valeurs vides\n\nPour comprendre ce qui s'est passé, il faut d'abord voir comment Nuxt 3 gère la composition des meta.\n\n### Le mécanisme attendu\n\n`useSeoMeta` est un composable fourni par [unhead](https://unhead.unjs.io/), la librairie de gestion du `\u003Chead>` utilisée par Nuxt 3. Quand plusieurs composants appellent `useSeoMeta`, les entrées sont fusionnées selon un principe de **dernier appelé gagne** (*last-in wins*) pour chaque clé.\n\nLe layout parent a été configuré ainsi :\n\n```vue\n\u003C!-- layouts/default.vue -->\n\u003Cscript setup lang=\"ts\">\nuseSeoMeta({\n  title: 'Marketplace B2B — Outillage Pro',\n  description: 'La marketplace de référence pour les pros du bâtiment. 12 000 références.',\n  ogTitle: 'Marketplace B2B — Outillage Pro',\n  ogDescription: 'La marketplace de référence pour les pros du bâtiment. 12 000 références.',\n  ogImage: 'https://marketplace.example.com/og-default.jpg',\n  robots: 'index, follow',\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003CAppHeader />\n    \u003Cslot />\n    \u003CAppFooter />\n  \u003C/div>\n\u003C/template>\n```\n\nL'idée est saine : le layout pose des valeurs par défaut. Chaque page enfant peut les surcharger si besoin. Et c'est exactement ce que fait la page catégorie :\n\n```vue\n\u003C!-- pages/categorie/[slug].vue -->\n\u003Cscript setup lang=\"ts\">\nconst { data: category } = await useFetch(`/api/categories/${route.params.slug}`)\n\nuseSeoMeta({\n  title: category.value?.seoTitle,\n  description: category.value?.seoDescription,\n  ogTitle: category.value?.seoTitle,\n  ogDescription: category.value?.seoDescription,\n  ogImage: category.value?.ogImage,\n})\n\u003C/script>\n```\n\n### Le piège\n\nLe problème est là, en clair : `category.value?.seoDescription` retourne `undefined` pour 340 catégories dont le champ `seoDescription` n'a jamais été renseigné dans le CMS (Strapi).\n\nOr, dans `useSeoMeta`, passer `undefined` pour une clé ne signifie pas \"ne touche pas à cette clé, laisse le parent la gérer\". Cela signifie : **\"définis cette clé à vide\"**. Le composable enfant écrase l'entrée du parent — même si la valeur est `undefined`.\n\nLe rendu SSR final pour ces 340 pages ressemble à :\n\n```html\n\u003Chead>\n  \u003Ctitle>\u003C/title>\n  \u003Cmeta name=\"description\" content=\"\">\n  \u003Cmeta property=\"og:title\" content=\"\">\n  \u003Cmeta property=\"og:description\" content=\"\">\n  \u003Cmeta property=\"og:image\" content=\"\">\n  \u003Cmeta name=\"robots\" content=\"index, follow\">\n\u003C/head>\n```\n\nLa clé `robots` est préservée parce que la page enfant ne la redéfinit pas. Toutes les autres sont écrasées par des chaînes vides.\n\n### Pourquoi personne n'a rien vu\n\nTrois raisons convergentes.\n\n**1. L'hydratation côté client masque le bug.** Après le rendu SSR, le composant page se monte côté client. À ce moment, `useFetch` résout potentiellement avec un cache Nuxt payload, et si la catégorie existe, `category.value` est peuplé. Le composable `useSeoMeta` est réévalué côté client avec les bonnes données (quand elles existent). Le DOM est patché. L'inspecteur Chrome montre les meta correctes. Mais Googlebot utilise le HTML initial dans la majorité des cas pour extraire les meta, pas le DOM post-hydratation.\n\n**2. Les tests E2E vérifient le DOM, pas le HTML SSR.** L'équipe utilise Playwright. Les assertions `page.locator('meta[name=\"description\"]').getAttribute('content')` passent parce que Playwright exécute le JavaScript. Le HTML brut n'est jamais vérifié.\n\n**3. Le QA humain se fait sur des catégories remplies.** Les 5 catégories de test dans l'environnement de staging ont toutes leur champ `seoDescription` rempli. Les 340 catégories \"vides\" sont des sous-catégories secondaires, rarement visitées par l'équipe.\n\n### Reproduction pas à pas\n\nPour reproduire le comportement, voici un setup minimal :\n\n```typescript\n// composables/test-override.ts\nimport { useSeoMeta } from '#imports'\n\n// Simule le layout\nuseSeoMeta({\n  description: 'Valeur par défaut du layout',\n  ogDescription: 'Valeur par défaut du layout',\n})\n\n// Simule la page enfant avec une valeur undefined\nconst apiValue: string | undefined = undefined\n\nuseSeoMeta({\n  description: apiValue,\n  ogDescription: apiValue,\n})\n```\n\nLe résultat dans le HTML SSR : `\u003Cmeta name=\"description\" content=\"\">`. Le fallback du layout est perdu.\n\nPour vérifier le comportement côté SSR sans navigateur, la commande `curl` reste le test le plus fiable :\n\n```bash\n# Vérifier le HTML SSR brut d'une page\ncurl -s -A \"Googlebot\" https://marketplace.example.com/categorie/vis-inox \\\n  | npx htmlq 'meta[name=\"description\"]' --attribute content\n```\n\nRésultat attendu si le bug est présent : une chaîne vide. On peut aussi utiliser l'outil d'inspection d'URL de Search Console, qui affiche le HTML tel que Googlebot le perçoit. Pour ces 340 pages, la description affichée dans l'outil était bien vide.\n\nCe type de divergence entre rendu SSR et rendu client est un classique des frameworks hydratés. Un incident similaire a touché des sites Next.js où [les metadata async qui throw servaient un fallback vide](/blog/next-js-metadata-async-qui-throw-la-page-sert-le-fallback-default-next-js). L'angle diffère, mais le mécanisme est le même : ce que le développeur voit dans le navigateur n'est pas ce que le bot indexe.\n\n## Le fix : filtrer les undefined avant d'appeler useSeoMeta\n\n### Le patch immédiat\n\nLa correction tient en quelques lignes. L'objectif : ne jamais passer de clé `undefined` à `useSeoMeta` dans les pages enfants. Si une valeur n'existe pas, la clé ne doit pas être incluse dans l'appel — pour que le layout parent conserve son default.\n\n```vue\n\u003C!-- pages/categorie/[slug].vue — APRÈS CORRECTION -->\n\u003Cscript setup lang=\"ts\">\nconst { data: category } = await useFetch(`/api/categories/${route.params.slug}`)\n\n// Utilitaire : supprime les clés undefined/null/vides\nfunction definedOnly\u003CT extends Record\u003Cstring, unknown>>(obj: T): Partial\u003CT> {\n  return Object.fromEntries(\n    Object.entries(obj).filter(([, v]) => v != null && v !== '')\n  ) as Partial\u003CT>\n}\n\nuseSeoMeta(definedOnly({\n  title: category.value?.seoTitle,\n  description: category.value?.seoDescription,\n  ogTitle: category.value?.seoTitle,\n  ogDescription: category.value?.seoDescription,\n  ogImage: category.value?.ogImage,\n}))\n\u003C/script>\n```\n\nAvec `definedOnly`, si `category.value?.seoDescription` est `undefined`, la clé `description` n'est pas passée à `useSeoMeta`. Le layout parent reste maître de cette clé.\n\n### Le composable partagé\n\nPour éviter que chaque page répète cette logique, l'équipe a extrait un composable :\n\n```typescript\n// composables/useSafeSeoMeta.ts\nimport type { UseSeoMetaInput } from '@unhead/schema'\n\nexport function useSafeSeoMeta(input: UseSeoMetaInput) {\n  const cleaned = Object.fromEntries(\n    Object.entries(input).filter(([, v]) => v != null && v !== '')\n  ) as UseSeoMetaInput\n\n  if (Object.keys(cleaned).length > 0) {\n    useSeoMeta(cleaned)\n  }\n}\n```\n\nChaque page remplace désormais `useSeoMeta(...)` par `useSafeSeoMeta(...)`. Le layout continue d'utiliser `useSeoMeta` directement — ses valeurs sont toujours définies en dur.\n\n### Déploiement et invalidation\n\nLe fix a été déployé un lundi à 14h. L'équipe a ensuite :\n\n1. Purgé le cache CDN (Cloudflare) sur le pattern `/categorie/*` pour que le nouveau HTML SSR soit servi immédiatement.\n2. Soumis les 340 URLs impactées via l'API d'indexation Google (quota : 200/jour, complété en deux jours).\n3. Vérifié chaque page avec `curl` et le rapport d'inspection d'URL de Search Console.\n\n```bash\n# Vérification post-déploiement sur un échantillon\nfor slug in vis-inox boulonnerie-acier disques-meulage; do\n  echo \"=== $slug ===\"\n  curl -s \"https://marketplace.example.com/categorie/$slug\" \\\n    | grep -oP '\u003Cmeta name=\"description\" content=\"\\K[^\"]*'\ndone\n```\n\nLe HTML SSR retournait maintenant les valeurs par défaut du layout pour les catégories sans meta custom, et les valeurs CMS pour celles qui en avaient.\n\n### Récupération du trafic\n\nLe ranking a commencé à remonter au bout de 5 jours. Les impressions ont retrouvé leur niveau d'avant incident en 12 jours. Le trafic organique sur les 340 pages catégories a mis 16 jours pour revenir à la normale — un délai cohérent avec la fréquence de recrawl observée pour ce type de pages (crawl budget moyen de 800 pages/jour sur ce site).\n\n### Ajouts au pipeline CI\n\nL'équipe a ajouté deux garde-fous pour que l'incident ne se reproduise pas.\n\n**Test SSR automatisé dans la CI :**\n\n```typescript\n// tests/seo-meta-ssr.spec.ts (Vitest + $fetch Nuxt)\nimport { describe, it, expect } from 'vitest'\nimport { $fetch } from '@nuxt/test-utils'\n\ndescribe('SSR meta defaults', () => {\n  it('category without CMS meta still has layout defaults', async () => {\n    const html = await $fetch('/categorie/vis-inox') // catégorie sans meta CMS\n    \n    expect(html).toContain('\u003Cmeta name=\"description\" content=\"La marketplace')\n    expect(html).toContain('\u003Cmeta property=\"og:description\" content=\"La marketplace')\n    expect(html).not.toContain('content=\"\"')\n  })\n})\n```\n\n**Lint rule custom** : un script de pre-commit scanne les fichiers `pages/**/*.vue` et alerte si `useSeoMeta` est appelé directement (au lieu de `useSafeSeoMeta`).\n\nCe pattern — un composant enfant qui écrase involontairement les meta du parent — n'est pas spécifique aux meta. On retrouve la même logique dans les incidents où un [composant heading rend un `div` selon une prop mal configurée](/blog/design-system-composant-heading-qui-rend-div-selon-la-prop-as-mal-configuree), ou quand une [refonte du header remplace un H1 par un div](/blog/refonte-header-le-h1-remplace-par-un-div-title-par-le-design-system). Le point commun : un changement invisible côté navigateur, catastrophique côté crawl.\n\n## Ce qu'on en retient\n\n`useSeoMeta` est un excellent composable. Mais son modèle de composition — dernier appel gagne, clé par clé — transforme un `undefined` innocent en bombe silencieuse. Le layout parent croit poser des defaults solides. Le composant enfant les pulvérise sans le vouloir.\n\nTrois règles à graver :\n\n1. Ne jamais passer de clé `undefined` à `useSeoMeta` dans un composant enfant. Filtrer avant l'appel, ou utiliser un wrapper.\n2. Toujours tester le HTML SSR brut, pas le DOM post-hydratation. `curl` ne ment pas. Le navigateur, si.\n3. Monitorer les meta en continu. Un crawl hebdomadaire avec Screaming Frog ne suffit pas quand 340 pages peuvent basculer sur un seul déploiement. Un monitoring continu type Seogard détecte ce type de divergence SSR/client en quelques minutes — pas dix-neuf jours.\n\nLe bug le plus dangereux en SEO technique reste celui que le navigateur corrige tout seul.\n```","https://seogard.io/blog/nuxt-useseometa-le-child-override-silencieusement-les-meta-du-layout-parent","Framework","2026-06-10T06:01:32.891Z","2026-06-10","Un site Nuxt 3 perd ses meta par défaut sur 340 pages. Récit technique du bug useSeoMeta, diagnostic et fix du fallback layout.","\u003Ch1>Nuxt useSeoMeta : quand le composant enfant efface silencieusement les meta du layout parent\u003C/h1>\n\u003Cp>Jeudi 16h20. L'équipe SEO d'une marketplace B2B française (12 000 références, 2 400 pages catégories) pousse en production un nouveau layout Nuxt 3 avec des meta par défaut centralisés via \u003Ccode>useSeoMeta\u003C/code>. Le navigateur affiche les bonnes balises. Lighthouse score 100 en SEO. Le merge request est approuvé. Personne ne regarde le rendu SSR brut des pages enfants. Dix-neuf jours plus tard, Search Console signale un effondrement des impressions sur 340 URLs catégories. Les \u003Ccode>og:description\u003C/code>, \u003Ccode>og:image\u003C/code> et \u003Ccode>description\u003C/code> ont disparu — remplacés par rien.\u003C/p>\n\u003Ch2>Lundi, T+19 jours — \"C'est quoi ce trou dans les impressions ?\"\u003C/h2>\n\u003Cp>Le lead SEO ouvre Search Console le lundi à 9h15. Le rapport Performances affiche une chute de 34 % des impressions sur les pages catégories. Pas les fiches produit. Pas le blog. Uniquement les catégories.\u003C/p>\n\u003Cp>Premier réflexe : vérifier si Google a signalé un problème d'indexation. Onglet \"Pages\" → aucune alerte. Tout est marqué \"Indexée\". L'équipe suppose d'abord un changement algorithmique. Le dev frontend vérifie la branche \u003Ccode>main\u003C/code> — aucun déploiement depuis quatre jours. Le CTO pense à un problème CDN.\u003C/p>\n\u003Cp>À 10h30, le lead SEO lance un crawl Screaming Frog sur les 2 400 URLs catégories. Résultat : 340 pages retournent une balise \u003Ccode>&#x3C;meta name=\"description\">\u003C/code> vide. Pas absente — vide. \u003Ccode>content=\"\"\u003C/code>. Idem pour \u003Ccode>og:description\u003C/code> et \u003Ccode>og:image\u003C/code>.\u003C/p>\n\u003Cp>Le dev frontend ouvre une de ces pages dans Chrome, inspecte le DOM. La meta description est bien là, remplie. Il rafraîchit. Toujours là. \"Chez moi ça marche.\"\u003C/p>\n\u003Cp>À 11h15, quelqu'un a le réflexe de faire un \u003Ccode>curl\u003C/code> brut :\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://marketplace.example.com/categorie/outillage-electrique\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\"> 'og:description'\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Résultat :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">meta\u003C/span>\u003Cspan style=\"color:#B392F0\"> property\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"og: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>\u003C/code>\u003C/pre>\n\u003Cp>Le contenu est vide dans le HTML SSR. Côté navigateur, l'hydratation côté client corrige la meta après le mount du composant. Googlebot, qui utilise le HTML initial pour extraire les meta, ne voit rien.\u003C/p>\n\u003Cp>À 11h45, l'équipe ouvre le diff Git du déploiement du 19 jours plus tôt. Le layout \u003Ccode>default.vue\u003C/code> a été refactoré. Et un composant enfant — \u003Ccode>CategoryHead.vue\u003C/code> — a été ajouté dans certaines pages catégories. Le lead SEO commence à comprendre. Le problème n'est pas côté serveur, ni côté CDN. C'est un problème de composition de \u003Ccode>useSeoMeta\u003C/code> entre layout parent et page enfant.\u003C/p>\n\u003Cp>L'impact mesuré a posteriori : −18 000 clics sur 19 jours, 340 pages touchées, temps moyen de récupération estimé à 10-14 jours après fix.\u003C/p>\n\u003Ch2>Le bug : useSeoMeta dans le child écrase le layout, même avec des valeurs vides\u003C/h2>\n\u003Cp>Pour comprendre ce qui s'est passé, il faut d'abord voir comment Nuxt 3 gère la composition des meta.\u003C/p>\n\u003Ch3>Le mécanisme attendu\u003C/h3>\n\u003Cp>\u003Ccode>useSeoMeta\u003C/code> est un composable fourni par \u003Ca href=\"https://unhead.unjs.io/\">unhead\u003C/a>, la librairie de gestion du \u003Ccode>&#x3C;head>\u003C/code> utilisée par Nuxt 3. Quand plusieurs composants appellent \u003Ccode>useSeoMeta\u003C/code>, les entrées sont fusionnées selon un principe de \u003Cstrong>dernier appelé gagne\u003C/strong> (\u003Cem>last-in wins\u003C/em>) pour chaque clé.\u003C/p>\n\u003Cp>Le layout parent a été configuré ainsi :\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;!-- layouts/default.vue -->\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:#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:#B392F0\">useSeoMeta\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  title: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Marketplace B2B — Outillage Pro'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  description: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'La marketplace de référence pour les pros du bâtiment. 12 000 références.'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  ogTitle: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Marketplace B2B — Outillage Pro'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  ogDescription: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'La marketplace de référence pour les pros du bâtiment. 12 000 références.'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  ogImage: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'https://marketplace.example.com/og-default.jpg'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  robots: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'index, follow'\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\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">script\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\">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\">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\">AppHeader\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\">slot\u003C/span>\u003Cspan style=\"color:#FDAEB7;font-style:italic\"> /\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\">AppFooter\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\">template\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>L'idée est saine : le layout pose des valeurs par défaut. Chaque page enfant peut les surcharger si besoin. Et c'est exactement ce que fait la page catégorie :\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;!-- pages/categorie/[slug].vue -->\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:#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\">const\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#FFAB70\">data\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">category\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> useFetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`/api/categories/${\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\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">useSeoMeta\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  title: category.value?.seoTitle,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  description: category.value?.seoDescription,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  ogTitle: category.value?.seoTitle,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  ogDescription: category.value?.seoDescription,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  ogImage: category.value?.ogImage,\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\u003Ch3>Le piège\u003C/h3>\n\u003Cp>Le problème est là, en clair : \u003Ccode>category.value?.seoDescription\u003C/code> retourne \u003Ccode>undefined\u003C/code> pour 340 catégories dont le champ \u003Ccode>seoDescription\u003C/code> n'a jamais été renseigné dans le CMS (Strapi).\u003C/p>\n\u003Cp>Or, dans \u003Ccode>useSeoMeta\u003C/code>, passer \u003Ccode>undefined\u003C/code> pour une clé ne signifie pas \"ne touche pas à cette clé, laisse le parent la gérer\". Cela signifie : \u003Cstrong>\"définis cette clé à vide\"\u003C/strong>. Le composable enfant écrase l'entrée du parent — même si la valeur est \u003Ccode>undefined\u003C/code>.\u003C/p>\n\u003Cp>Le rendu SSR final pour ces 340 pages ressemble à :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">head\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">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:#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: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:image\"\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\"> name\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"robots\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> content\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"index, follow\"\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>\u003C/code>\u003C/pre>\n\u003Cp>La clé \u003Ccode>robots\u003C/code> est préservée parce que la page enfant ne la redéfinit pas. Toutes les autres sont écrasées par des chaînes vides.\u003C/p>\n\u003Ch3>Pourquoi personne n'a rien vu\u003C/h3>\n\u003Cp>Trois raisons convergentes.\u003C/p>\n\u003Cp>\u003Cstrong>1. L'hydratation côté client masque le bug.\u003C/strong> Après le rendu SSR, le composant page se monte côté client. À ce moment, \u003Ccode>useFetch\u003C/code> résout potentiellement avec un cache Nuxt payload, et si la catégorie existe, \u003Ccode>category.value\u003C/code> est peuplé. Le composable \u003Ccode>useSeoMeta\u003C/code> est réévalué côté client avec les bonnes données (quand elles existent). Le DOM est patché. L'inspecteur Chrome montre les meta correctes. Mais Googlebot utilise le HTML initial dans la majorité des cas pour extraire les meta, pas le DOM post-hydratation.\u003C/p>\n\u003Cp>\u003Cstrong>2. Les tests E2E vérifient le DOM, pas le HTML SSR.\u003C/strong> L'équipe utilise Playwright. Les assertions \u003Ccode>page.locator('meta[name=\"description\"]').getAttribute('content')\u003C/code> passent parce que Playwright exécute le JavaScript. Le HTML brut n'est jamais vérifié.\u003C/p>\n\u003Cp>\u003Cstrong>3. Le QA humain se fait sur des catégories remplies.\u003C/strong> Les 5 catégories de test dans l'environnement de staging ont toutes leur champ \u003Ccode>seoDescription\u003C/code> rempli. Les 340 catégories \"vides\" sont des sous-catégories secondaires, rarement visitées par l'équipe.\u003C/p>\n\u003Ch3>Reproduction pas à pas\u003C/h3>\n\u003Cp>Pour reproduire le comportement, voici un setup minimal :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// composables/test-override.ts\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { useSeoMeta } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '#imports'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Simule le layout\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">useSeoMeta\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  description: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Valeur par défaut du layout'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  ogDescription: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Valeur par défaut du layout'\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\">// Simule la page enfant avec une valeur undefined\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> apiValue\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#79B8FF\"> undefined\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#79B8FF\"> undefined\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">useSeoMeta\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  description: apiValue,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  ogDescription: apiValue,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">})\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le résultat dans le HTML SSR : \u003Ccode>&#x3C;meta name=\"description\" content=\"\">\u003C/code>. Le fallback du layout est perdu.\u003C/p>\n\u003Cp>Pour vérifier le comportement côté SSR sans navigateur, la commande \u003Ccode>curl\u003C/code> reste le test le plus fiable :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Vérifier le HTML SSR brut d'une page\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -s\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -A\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"Googlebot\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> https://marketplace.example.com/categorie/vis-inox\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  |\u003C/span>\u003Cspan style=\"color:#B392F0\"> npx\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> htmlq\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'meta[name=\"description\"]'\u003C/span>\u003Cspan style=\"color:#79B8FF\"> --attribute\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> content\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Résultat attendu si le bug est présent : une chaîne vide. On peut aussi utiliser l'outil d'inspection d'URL de Search Console, qui affiche le HTML tel que Googlebot le perçoit. Pour ces 340 pages, la description affichée dans l'outil était bien vide.\u003C/p>\n\u003Cp>Ce type de divergence entre rendu SSR et rendu client est un classique des frameworks hydratés. Un incident similaire a touché des sites Next.js où \u003Ca href=\"/blog/next-js-metadata-async-qui-throw-la-page-sert-le-fallback-default-next-js\">les metadata async qui throw servaient un fallback vide\u003C/a>. L'angle diffère, mais le mécanisme est le même : ce que le développeur voit dans le navigateur n'est pas ce que le bot indexe.\u003C/p>\n\u003Ch2>Le fix : filtrer les undefined avant d'appeler useSeoMeta\u003C/h2>\n\u003Ch3>Le patch immédiat\u003C/h3>\n\u003Cp>La correction tient en quelques lignes. L'objectif : ne jamais passer de clé \u003Ccode>undefined\u003C/code> à \u003Ccode>useSeoMeta\u003C/code> dans les pages enfants. Si une valeur n'existe pas, la clé ne doit pas être incluse dans l'appel — pour que le layout parent conserve son default.\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;!-- pages/categorie/[slug].vue — APRÈS CORRECTION -->\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:#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\">const\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#FFAB70\">data\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">category\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> useFetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`/api/categories/${\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\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Utilitaire : supprime les clés undefined/null/vides\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">function\u003C/span>\u003Cspan style=\"color:#B392F0\"> definedOnly\u003C/span>\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#B392F0\">T\u003C/span>\u003Cspan style=\"color:#F97583\"> extends\u003C/span>\u003Cspan style=\"color:#B392F0\"> Record\u003C/span>\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">unknown\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>>(\u003C/span>\u003Cspan style=\"color:#FFAB70\">obj\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> T\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> Partial\u003C/span>\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#B392F0\">T\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\"> Object.\u003C/span>\u003Cspan style=\"color:#B392F0\">fromEntries\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    Object.\u003C/span>\u003Cspan style=\"color:#B392F0\">entries\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(obj).\u003C/span>\u003Cspan style=\"color:#B392F0\">filter\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(([, \u003C/span>\u003Cspan style=\"color:#FFAB70\">v\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> v \u003C/span>\u003Cspan style=\"color:#F97583\">!=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> null\u003C/span>\u003Cspan style=\"color:#F97583\"> &#x26;&#x26;\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> v \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>\u003Cspan style=\"color:#F97583\">as\u003C/span>\u003Cspan style=\"color:#B392F0\"> Partial\u003C/span>\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#B392F0\">T\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\">useSeoMeta\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#B392F0\">definedOnly\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  title: category.value?.seoTitle,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  description: category.value?.seoDescription,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  ogTitle: category.value?.seoTitle,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  ogDescription: category.value?.seoDescription,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  ogImage: category.value?.ogImage,\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>Avec \u003Ccode>definedOnly\u003C/code>, si \u003Ccode>category.value?.seoDescription\u003C/code> est \u003Ccode>undefined\u003C/code>, la clé \u003Ccode>description\u003C/code> n'est pas passée à \u003Ccode>useSeoMeta\u003C/code>. Le layout parent reste maître de cette clé.\u003C/p>\n\u003Ch3>Le composable partagé\u003C/h3>\n\u003Cp>Pour éviter que chaque page répète cette logique, l'équipe a extrait un composable :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// composables/useSafeSeoMeta.ts\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#F97583\"> type\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { UseSeoMetaInput } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '@unhead/schema'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> function\u003C/span>\u003Cspan style=\"color:#B392F0\"> useSafeSeoMeta\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">input\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> UseSeoMetaInput\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\"> cleaned\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> Object.\u003C/span>\u003Cspan style=\"color:#B392F0\">fromEntries\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    Object.\u003C/span>\u003Cspan style=\"color:#B392F0\">entries\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(input).\u003C/span>\u003Cspan style=\"color:#B392F0\">filter\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(([, \u003C/span>\u003Cspan style=\"color:#FFAB70\">v\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> v \u003C/span>\u003Cspan style=\"color:#F97583\">!=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> null\u003C/span>\u003Cspan style=\"color:#F97583\"> &#x26;&#x26;\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> v \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>\u003Cspan style=\"color:#F97583\">as\u003C/span>\u003Cspan style=\"color:#B392F0\"> UseSeoMetaInput\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\"> (Object.\u003C/span>\u003Cspan style=\"color:#B392F0\">keys\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(cleaned).\u003C/span>\u003Cspan style=\"color:#79B8FF\">length\u003C/span>\u003Cspan style=\"color:#F97583\"> >\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    useSeoMeta\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(cleaned)\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>Chaque page remplace désormais \u003Ccode>useSeoMeta(...)\u003C/code> par \u003Ccode>useSafeSeoMeta(...)\u003C/code>. Le layout continue d'utiliser \u003Ccode>useSeoMeta\u003C/code> directement — ses valeurs sont toujours définies en dur.\u003C/p>\n\u003Ch3>Déploiement et invalidation\u003C/h3>\n\u003Cp>Le fix a été déployé un lundi à 14h. L'équipe a ensuite :\u003C/p>\n\u003Col>\n\u003Cli>Purgé le cache CDN (Cloudflare) sur le pattern \u003Ccode>/categorie/*\u003C/code> pour que le nouveau HTML SSR soit servi immédiatement.\u003C/li>\n\u003Cli>Soumis les 340 URLs impactées via l'API d'indexation Google (quota : 200/jour, complété en deux jours).\u003C/li>\n\u003Cli>Vérifié chaque page avec \u003Ccode>curl\u003C/code> et le rapport d'inspection d'URL de Search Console.\u003C/li>\n\u003C/ol>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Vérification post-déploiement sur un échantillon\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> slug \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> vis-inox\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> boulonnerie-acier\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> disques-meulage\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; \u003C/span>\u003Cspan style=\"color:#F97583\">do\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  echo\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"=== \u003C/span>\u003Cspan style=\"color:#E1E4E8\">$slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> ===\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -s\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"https://marketplace.example.com/categorie/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\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;meta name=\"description\" content=\"\\K[^\"]*'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">done\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le HTML SSR retournait maintenant les valeurs par défaut du layout pour les catégories sans meta custom, et les valeurs CMS pour celles qui en avaient.\u003C/p>\n\u003Ch3>Récupération du trafic\u003C/h3>\n\u003Cp>Le ranking a commencé à remonter au bout de 5 jours. Les impressions ont retrouvé leur niveau d'avant incident en 12 jours. Le trafic organique sur les 340 pages catégories a mis 16 jours pour revenir à la normale — un délai cohérent avec la fréquence de recrawl observée pour ce type de pages (crawl budget moyen de 800 pages/jour sur ce site).\u003C/p>\n\u003Ch3>Ajouts au pipeline CI\u003C/h3>\n\u003Cp>L'équipe a ajouté deux garde-fous pour que l'incident ne se reproduise pas.\u003C/p>\n\u003Cp>\u003Cstrong>Test SSR automatisé dans la CI :\u003C/strong>\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// tests/seo-meta-ssr.spec.ts (Vitest + $fetch Nuxt)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { describe, it, expect } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'vitest'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { $fetch } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '@nuxt/test-utils'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">describe\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'SSR meta defaults'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, () \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  it\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'category without CMS meta still has layout defaults'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#F97583\">async\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> () \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> html\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\">'/categorie/vis-inox'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#6A737D\">// catégorie sans meta CMS\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(html).\u003C/span>\u003Cspan style=\"color:#B392F0\">toContain\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'&#x3C;meta name=\"description\" content=\"La marketplace'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(html).\u003C/span>\u003Cspan style=\"color:#B392F0\">toContain\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'&#x3C;meta property=\"og:description\" content=\"La marketplace'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(html).not.\u003C/span>\u003Cspan style=\"color:#B392F0\">toContain\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'content=\"\"'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  })\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">})\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>\u003Cstrong>Lint rule custom\u003C/strong> : un script de pre-commit scanne les fichiers \u003Ccode>pages/**/*.vue\u003C/code> et alerte si \u003Ccode>useSeoMeta\u003C/code> est appelé directement (au lieu de \u003Ccode>useSafeSeoMeta\u003C/code>).\u003C/p>\n\u003Cp>Ce pattern — un composant enfant qui écrase involontairement les meta du parent — n'est pas spécifique aux meta. On retrouve la même logique dans les incidents où un \u003Ca href=\"/blog/design-system-composant-heading-qui-rend-div-selon-la-prop-as-mal-configuree\">composant heading rend un \u003Ccode>div\u003C/code> selon une prop mal configurée\u003C/a>, ou quand une \u003Ca href=\"/blog/refonte-header-le-h1-remplace-par-un-div-title-par-le-design-system\">refonte du header remplace un H1 par un div\u003C/a>. Le point commun : un changement invisible côté navigateur, catastrophique côté crawl.\u003C/p>\n\u003Ch2>Ce qu'on en retient\u003C/h2>\n\u003Cp>\u003Ccode>useSeoMeta\u003C/code> est un excellent composable. Mais son modèle de composition — dernier appel gagne, clé par clé — transforme un \u003Ccode>undefined\u003C/code> innocent en bombe silencieuse. Le layout parent croit poser des defaults solides. Le composant enfant les pulvérise sans le vouloir.\u003C/p>\n\u003Cp>Trois règles à graver :\u003C/p>\n\u003Col>\n\u003Cli>Ne jamais passer de clé \u003Ccode>undefined\u003C/code> à \u003Ccode>useSeoMeta\u003C/code> dans un composant enfant. Filtrer avant l'appel, ou utiliser un wrapper.\u003C/li>\n\u003Cli>Toujours tester le HTML SSR brut, pas le DOM post-hydratation. \u003Ccode>curl\u003C/code> ne ment pas. Le navigateur, si.\u003C/li>\n\u003Cli>Monitorer les meta en continu. Un crawl hebdomadaire avec Screaming Frog ne suffit pas quand 340 pages peuvent basculer sur un seul déploiement. Un monitoring continu type Seogard détecte ce type de divergence SSR/client en quelques minutes — pas dix-neuf jours.\u003C/li>\n\u003C/ol>\n\u003Cp>Le bug le plus dangereux en SEO technique reste celui que le navigateur corrige tout seul.\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,12,[18,19,20,21,22],"nuxt","useSeoMeta","layout","override","meta","Nuxt useSeoMeta : le child override les meta du layout","Wed Jun 10 2026 06:01:32 GMT+0000 (Coordinated Universal Time)",[26,41,56,68,79,93],{"_id":27,"slug":28,"__v":6,"author":7,"canonical":29,"category":10,"createdAt":30,"date":31,"description":32,"image":15,"imageAlt":15,"readingTime":16,"tags":33,"title":39,"updatedAt":40},"6a2cf253aa6b273b0c0c9a5f","tanstack-router-ssr-title-pris-du-layout-au-lieu-de-la-leaf-route","https://seogard.io/blog/tanstack-router-ssr-title-pris-du-layout-au-lieu-de-la-leaf-route","2026-06-13T06:01:55.020Z","2026-06-13","Un e-commerce perd 40 % de clics organiques : TanStack Router applique le title du layout parent au lieu de la leaf route. Récit, diagnostic, fix.",[34,35,36,37,38],"tanstack router","react","ssr","title","meta tags","TanStack Router SSR : le title vient du layout, pas de la page","Sat Jun 13 2026 06:01:55 GMT+0000 (Coordinated Universal Time)",{"_id":42,"slug":43,"__v":6,"author":7,"canonical":44,"category":10,"createdAt":45,"date":46,"description":47,"image":15,"imageAlt":15,"readingTime":48,"tags":49,"title":54,"updatedAt":55},"6a2ba0d0aa6b273b0cf5b507","astro-view-transitions-changement-de-route-ne-re-trigge-pas-le-head-update","https://seogard.io/blog/astro-view-transitions-changement-de-route-ne-re-trigge-pas-le-head-update","2026-06-12T06:01:52.619Z","2026-06-12","Un site Astro perd 40% de clics : les View Transitions ne mettent pas à jour les meta SEO lors des changements de route. Récit, diagnostic et fix.",11,[50,51,52,53],"astro","view transitions","head","spa","Astro View Transitions : meta head figées après navigation","Fri Jun 12 2026 06:01:52 GMT+0000 (Coordinated Universal Time)",{"_id":57,"slug":58,"__v":6,"author":7,"canonical":59,"category":10,"createdAt":60,"date":61,"description":62,"image":15,"imageAlt":15,"readingTime":16,"tags":63,"title":66,"updatedAt":67},"6a2adbecaa6b273b0c53007c","remix-meta-async-non-awaited-metas-vides-en-streaming","https://seogard.io/blog/remix-meta-async-non-awaited-metas-vides-en-streaming","2026-06-11T16:01:48.933Z","2026-06-11","Un site Remix perd 30% de trafic organique. La cause : meta() async non awaited, les balises arrivent après la fermeture du head en streaming.",[64,22,65,36],"remix","streaming","Remix meta() async : metas vides en streaming SSR","Thu Jun 11 2026 16:01:48 GMT+0000 (Coordinated Universal Time)",{"_id":69,"slug":70,"__v":6,"author":7,"canonical":71,"category":10,"createdAt":72,"date":12,"description":73,"image":15,"imageAlt":15,"readingTime":16,"tags":74,"title":77,"updatedAt":78},"6a298a64aa6b273b0c3bfcab","sveltekit-layout-ts-title-override-par-page-svelte-vide","https://seogard.io/blog/sveltekit-layout-ts-title-override-par-page-svelte-vide","2026-06-10T16:01:40.148Z","Un +page.svelte sans title écrase le layout parent. Googlebot voit un \u003Ctitle> vide. Récit, diagnostic et fix complet en SvelteKit.",[75,20,37,76],"sveltekit","svelte","SvelteKit : title vide en prod, 0 clic sur 3 semaines","Wed Jun 10 2026 16:01:40 GMT+0000 (Coordinated Universal Time)",{"_id":80,"slug":81,"__v":6,"author":7,"canonical":82,"category":10,"createdAt":83,"date":84,"description":85,"image":15,"imageAlt":15,"readingTime":16,"tags":86,"title":91,"updatedAt":92},"6a27ac47aa6b273b0cb0f6f6","next-js-metadata-async-qui-throw-la-page-sert-le-fallback-default-next-js","https://seogard.io/blog/next-js-metadata-async-qui-throw-la-page-sert-le-fallback-default-next-js","2026-06-09T06:01:43.334Z","2026-06-09","Une promise non gérée dans generateMetadata fait tomber les titles sur 1 200 pages produit. Récit technique, diagnostic et fix complet.",[87,88,89,90],"next.js","metadata","async","error","Next.js metadata async throw : Google indexe \\\"Next.js\\\" en title","Tue Jun 09 2026 06:01:43 GMT+0000 (Coordinated Universal Time)",{"_id":94,"slug":95,"__v":6,"author":7,"canonical":96,"category":10,"createdAt":97,"date":98,"description":99,"image":15,"imageAlt":15,"readingTime":48,"tags":100,"title":104,"updatedAt":105},"6a26e765aa6b273b0c0e5507","astro-content-collections-frontmatter-title-non-passe-apres-refacto","https://seogard.io/blog/astro-content-collections-frontmatter-title-non-passe-apres-refacto","2026-06-08T16:01:41.030Z","2026-06-08","Un upgrade Astro casse le mapping frontmatter → composant. 80 articles perdent leur title. Récit du bug, diagnostic technique et fix complet.",[50,101,102,103],"content collections","frontmatter","refacto","Astro Content Collections : 80 titles vides après refacto","Mon Jun 08 2026 16:01:41 GMT+0000 (Coordinated Universal Time)",{"categories":107},[108,112,116,120,124,126,130,133,136,140,144,147,150,154,157,160,163,166,170],{"category":109,"slug":110,"count":111},"Actualités SEO","actualites-seo",168,{"category":113,"slug":114,"count":115},"Migration","migration",18,{"category":117,"slug":118,"count":119},"Rendering","rendering",9,{"category":121,"slug":122,"count":123},"Performance","performance",8,{"category":10,"slug":125,"count":123},"framework",{"category":127,"slug":128,"count":129},"Crawl","crawl",7,{"category":131,"slug":132,"count":129},"SEO Technique","seo-technique",{"category":134,"slug":135,"count":129},"Meta Tags","meta-tags",{"category":137,"slug":138,"count":139},"Architecture","architecture",6,{"category":141,"slug":142,"count":143},"Structured Data","structured-data",5,{"category":145,"slug":146,"count":143},"JavaScript SEO","javascript-seo",{"category":148,"slug":149,"count":143},"Monitoring","monitoring",{"category":151,"slug":152,"count":153},"E-commerce","e-commerce",4,{"category":155,"slug":156,"count":153},"Avancé","avance",{"category":158,"slug":159,"count":153},"Refonte","refonte",{"category":161,"slug":162,"count":153},"Redirections","redirections",{"category":164,"slug":165,"count":153},"Outils","outils",{"category":167,"slug":168,"count":169},"IA & SEO","ia-seo",3,{"category":171,"slug":172,"count":169},"Contenu","contenu"]