[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$f6QKFr8JIAtgvfu8zSkVntpZL2stV8ETxPF94mZSymas":3,"$fjOTwPOicCZiXY-NY0Pyg2J2Q9txD49U89faIgyh7fOM":24,"$fcTFjTaPPT1sVcnh1wWEGoTWMSaOD4bwmwntkMae6w5s":111},{"_id":4,"slug":5,"__v":6,"author":7,"body":8,"canonical":9,"category":10,"createdAt":11,"date":12,"description":13,"htmlContent":14,"image":15,"imageAlt":15,"readingTime":16,"tags":17,"title":22,"updatedAt":23},"6a26e765aa6b273b0c0e5507","astro-content-collections-frontmatter-title-non-passe-apres-refacto",0,"Equipe Seogard","# 80 articles sans title : quand Astro Content Collections cassent en silence\n\nJeudi 14h20. L'équipe technique d'une agence SaaS B2B pousse un upgrade d'Astro 4.x vers 5.x sur un blog de 80 articles. Le build passe. Le déploiement passe. Lighthouse affiche un 98 en performance. Le designer valide le rendu. Personne ne regarde le `\u003Chead>`. Dix-huit jours plus tard, Search Console envoie un mail : \"Amélioration des titres — 80 pages avec un titre manquant\". Le trafic organique du blog a déjà perdu 34 % de ses clics.\n\n## Lundi matin, T+18 jours — L'alerte qui réveille\n\nLe lead SEO ouvre Search Console à 8h52. Le rapport \"Améliorations\" affiche un bandeau jaune inédit. Il clique. 80 URLs du blog sont signalées avec le problème \"Balise title manquante ou vide\". Il rafraîchit. Même résultat.\n\nPremier réflexe : ouvrir une page dans le navigateur. Le titre s'affiche dans l'onglet. Tout semble normal. Il inspecte le DOM avec Chrome DevTools. Le `\u003Ctitle>` est bien là, injecté côté client par un composant JavaScript. Mais DevTools montre le DOM *après* hydratation. Pas le HTML source.\n\nIl passe à `curl` :\n\n```bash\ncurl -s https://blog.example.com/posts/guide-onboarding-2026 | grep -i '\u003Ctitle>'\n```\n\nRésultat :\n\n```html\n\u003Ctitle>\u003C/title>\n```\n\nVide. Quatre caractères. Pas d'erreur. Pas de 500. Juste un `\u003Ctitle>` creux servi à chaque crawler de la planète depuis dix-huit jours.\n\nIl lance un crawl Screaming Frog sur les 80 URLs du répertoire `/posts/`. Résultat : 80 pages avec `\u003Ctitle>` vide. 80 pages avec `\u003Cmeta name=\"description\">` vide. 80 pages avec `\u003Cmeta property=\"og:title\">` absent. Le trifecta.\n\nLe lead SEO remonte dans GA4. Le segment \"Organic Search\" sur le scope `/posts/` montre une chute progressive. Pas un cliff — une glissade. −12 % la première semaine. −22 % la deuxième. −34 % au moment de l'alerte. Environ 4 200 clics perdus sur la période. Le blog génère 40 % des MQLs de l'entreprise. La réunion de crise est convoquée à 9h15.\n\nL'hypothèse initiale du dev lead : \"C'est peut-être un problème de cache CDN.\" L'équipe purge le cache Cloudflare. Nouveau `curl`. Toujours vide. Deuxième hypothèse : \"Le composant `\u003Chead>` a un bug d'import.\" Le dev ouvre le layout principal, vérifie les imports. Tout compile. Troisième hypothèse : \"C'est un problème de données — le CMS ne renvoie plus les titres.\" L'équipe vérifie les fichiers Markdown sources. Les frontmatters sont intacts. Chaque fichier contient bien un champ `title`.\n\nC'est à 9h47 que le dev frontend junior lâche la phrase qui déclenche le vrai diagnostic : \"On a changé la façon de récupérer les articles quand on est passé à Astro 5, non ?\"\n\n## Le bug : Content Collections v2 et le fantôme de `entry.data`\n\nLe blog utilise Astro avec des Content Collections. Chaque article est un fichier `.md` dans `src/content/posts/`. Le frontmatter ressemble à ceci :\n\n```markdown\n---\ntitle: \"Guide complet de l'onboarding client en 2026\"\ndescription: \"Les 7 étapes clés pour un onboarding qui réduit le churn de 40%.\"\npubDate: 2026-05-12\nauthor: \"Marie Dupont\"\ntags: [\"onboarding\", \"saas\", \"product\"]\n---\n\nLe contenu de l'article commence ici...\n```\n\nAvant l'upgrade, en Astro 4.x, le schéma de collection dans `src/content/config.ts` était déclaré ainsi :\n\n```typescript\n// src/content/config.ts — Astro 4.x\nimport { defineCollection, z } from 'astro:content';\n\nconst posts = defineCollection({\n  schema: z.object({\n    title: z.string(),\n    description: z.string(),\n    pubDate: z.date(),\n    author: z.string(),\n    tags: z.array(z.string()),\n  }),\n});\n\nexport const collections = { posts };\n```\n\nEt le layout de page article (`src/layouts/PostLayout.astro`) récupérait les données comme ceci :\n\n```astro\n---\n// src/layouts/PostLayout.astro — Astro 4.x\nconst { frontmatter } = Astro.props;\n---\n\u003Chtml>\n\u003Chead>\n  \u003Ctitle>{frontmatter.title}\u003C/title>\n  \u003Cmeta name=\"description\" content={frontmatter.description} />\n  \u003Cmeta property=\"og:title\" content={frontmatter.title} />\n\u003C/head>\n\u003Cbody>\n  \u003Ch1>{frontmatter.title}\u003C/h1>\n  \u003Cslot />\n\u003C/body>\n\u003C/html>\n```\n\nCe pattern fonctionnait parfaitement. Quand Astro rendait une page `.md`, il passait le frontmatter directement via `Astro.props.frontmatter` au layout déclaré dans le fichier Markdown.\n\nLors de l'upgrade vers Astro 5.x, l'équipe a migré vers la nouvelle API Content Collections. La page dynamique `src/pages/posts/[...slug].astro` a été refactorisée :\n\n```astro\n---\n// src/pages/posts/[...slug].astro — Astro 5.x (après refacto)\nimport { getCollection } from 'astro:content';\nimport PostLayout from '../../layouts/PostLayout.astro';\n\nexport async function getStaticPaths() {\n  const posts = await getCollection('posts');\n  return posts.map((entry) => ({\n    params: { slug: entry.slug },\n    props: { entry },\n  }));\n}\n\nconst { entry } = Astro.props;\nconst { Content } = await entry.render();\n---\n\n\u003CPostLayout>\n  \u003CContent />\n\u003C/PostLayout>\n```\n\nLe problème est ici, en trois lignes.\n\nL'ancien pattern passait `frontmatter` comme prop au layout. Le nouveau pattern passe `entry` entier comme prop — mais **ne transmet rien au layout**. Le composant `PostLayout` reçoit un `Astro.props` qui contient... un `slot`. Pas de `frontmatter`. Pas de `entry.data`. Rien.\n\nLe layout cherche `Astro.props.frontmatter.title`. Il obtient `undefined`. Astro ne lève aucune erreur. En mode SSG, `undefined` dans une expression `{frontmatter.title}` produit une chaîne vide. Le build passe. TypeScript, si configuré en mode lax (ce qui était le cas ici), ne signale rien.\n\nLe HTML rendu côté serveur — celui que Googlebot reçoit — ressemble à ceci :\n\n```html\n\u003Chtml>\n\u003Chead>\n  \u003Ctitle>\u003C/title>\n  \u003Cmeta name=\"description\" content=\"\" />\n  \u003C!-- og:title absent car content=\"\" est filtré -->\n\u003C/head>\n\u003Cbody>\n  \u003Ch1>\u003C/h1>\n  \u003Carticle>\n    \u003Cp>Le contenu de l'article commence ici...\u003C/p>\n  \u003C/article>\n\u003C/body>\n\u003C/html>\n```\n\nLe `\u003Ch1>` est également vide. Le contenu brut de l'article est bien rendu (via `\u003CContent />`), mais toute la couche de métadonnées a disparu.\n\nCe que voit le développeur dans le navigateur est différent, et c'est le piège. Le design system de l'équipe utilise un composant React hydraté côté client qui récupère le titre depuis le premier `\u003Ch1>` ou depuis un `\u003Cscript type=\"application/json\">` embarqué. Ce composant injecte le titre dans l'onglet du navigateur via `document.title`. L'inspection DOM dans DevTools montre donc un `\u003Ctitle>` rempli — mais uniquement après exécution JavaScript.\n\nGooglebot, en 2026, exécute JavaScript. Mais il ne le fait pas systématiquement, et surtout pas immédiatement. Le [rendu différé de Google](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics) signifie que la première passe d'indexation se base sur le HTML brut. Si le `\u003Ctitle>` est vide dans le HTML source, Google enregistre \"titre manquant\" avant même d'envisager un rendu JS.\n\nPour 80 articles, Google a donc généré ses propres titres — souvent la première phrase du contenu, ou le nom de domaine. Résultat : des SERP avec des snippets incohérents, des CTR en chute libre, et un signal clair de dégradation qualitative.\n\nLe diagnostic complet est confirmé par un test avec l'outil \"Inspection d'URL\" de Search Console. L'onglet \"HTML rendu\" montre le `\u003Ctitle>` vide sur le HTML initial. L'onglet \"Capture d'écran\" montre le titre visible (grâce au JS client). Deux réalités parallèles.\n\nPour comprendre pourquoi les tests unitaires et le CI n'ont rien détecté, il faut regarder la pipeline. L'équipe testait le build Astro avec un simple `astro build && echo \"OK\"`. Le build ne crashe pas quand une expression template retourne `undefined`. Ils avaient un test Playwright qui vérifiait la présence d'un `\u003Ch1>` visible — mais le test s'exécute dans un vrai navigateur avec JS, donc il voit le titre injecté côté client. Zéro test sur le HTML statique brut. C'est un pattern que l'on retrouve fréquemment, et que l'on a déjà documenté lors d'une [migration Astro où le mapping frontmatter title avait cassé de la même manière](/blog/astro-v5-vers-v6-content-collections-cassent-le-mapping-frontmatter-title).\n\n## Le fix : reconnecter `entry.data` au layout\n\nLe correctif tient en deux modifications. La première, dans la page dynamique, consiste à passer explicitement les données du frontmatter au layout :\n\n```astro\n---\n// src/pages/posts/[...slug].astro — CORRIGÉ\nimport { getCollection } from 'astro:content';\nimport PostLayout from '../../layouts/PostLayout.astro';\n\nexport async function getStaticPaths() {\n  const posts = await getCollection('posts');\n  return posts.map((entry) => ({\n    params: { slug: entry.slug },\n    props: { entry },\n  }));\n}\n\nconst { entry } = Astro.props;\nconst { Content } = await entry.render();\n---\n\n\u003CPostLayout frontmatter={entry.data}>\n  \u003CContent />\n\u003C/PostLayout>\n```\n\nLa clé : `entry.data` contient exactement le même objet que l'ancien `frontmatter`. La seule différence est qu'il faut le passer explicitement comme prop.\n\nLa deuxième modification, dans le layout, consiste à typer la prop pour éviter toute régression future :\n\n```astro\n---\n// src/layouts/PostLayout.astro — CORRIGÉ avec typage\ninterface Props {\n  frontmatter: {\n    title: string;\n    description: string;\n    pubDate: Date;\n    author: string;\n    tags: string[];\n  };\n}\n\nconst { frontmatter } = Astro.props;\n---\n\u003Chtml>\n\u003Chead>\n  \u003Ctitle>{frontmatter.title}\u003C/title>\n  \u003Cmeta name=\"description\" content={frontmatter.description} />\n  \u003Cmeta property=\"og:title\" content={frontmatter.title} />\n\u003C/head>\n\u003Cbody>\n  \u003Ch1>{frontmatter.title}\u003C/h1>\n  \u003Cslot />\n\u003C/body>\n\u003C/html>\n```\n\nAvec ce typage, si `frontmatter` est `undefined` ou si `title` manque, le build Astro échoue en mode strict (`astro check`). L'erreur est détectée au CI, pas en production.\n\nL'équipe a ajouté un troisième filet de sécurité : un test dans la pipeline CI qui vérifie le HTML statique généré, sans navigateur :\n\n```bash\n# scripts/check-titles.sh\nastro build\n\nMISSING=0\nfor file in dist/posts/*/index.html; do\n  TITLE=$(grep -oP '(?\u003C=\u003Ctitle>).*(?=\u003C/title>)' \"$file\")\n  if [ -z \"$TITLE\" ]; then\n    echo \"ERREUR: title vide dans $file\"\n    MISSING=$((MISSING + 1))\n  fi\ndone\n\nif [ \"$MISSING\" -gt 0 ]; then\n  echo \"$MISSING page(s) avec title vide. Build rejeté.\"\n  exit 1\nfi\n\necho \"Toutes les pages ont un title. OK.\"\n```\n\nCe script parcourt les fichiers HTML générés par `astro build` et vérifie que chaque `\u003Ctitle>` contient du texte. Il s'exécute après le build, avant le déploiement. Coût : 3 secondes sur une pipeline de 45 secondes.\n\nLe déploiement du fix a été effectué le jour même, à 11h30. Le cache Cloudflare a été purgé dans la foulée. L'équipe a ensuite soumis les 80 URLs à la réindexation via Search Console (par lots de 10, la limite quotidienne étant ce qu'elle est).\n\nLa récupération a pris du temps. Voici la chronologie observée :\n\n- **J+2** : Google a recrawlé 31 des 80 pages. Les titles corrects apparaissent dans le cache Google.\n- **J+5** : 68 pages recrawlées. Les snippets commencent à afficher les vrais titres dans les SERP.\n- **J+12** : 80/80 pages recrawlées. Le trafic organique remonte à −15 % par rapport au niveau pré-incident.\n- **J+21** : Le trafic organique retrouve son niveau initial. Certaines pages ont même gagné des positions, probablement parce que les titres corrects matchent mieux l'intention de recherche que les titres auto-générés par Google.\n\nAu total : 18 jours de dégradation silencieuse, 3 semaines de récupération, environ 8 400 clics perdus cumulés.\n\nL'équipe a mis en place deux garde-fous supplémentaires. Le premier : activer `astro check` en mode strict dans le CI, avec le flag `--tsconfig tsconfig.json` configuré en `\"strict\": true`. Le second : un crawl Screaming Frog hebdomadaire automatisé qui alerte sur tout `\u003Ctitle>` vide ou dupliqué, configuré via l'API Screaming Frog.\n\nCe type de régression silencieuse est spécialement vicieux sur les stacks SSG. Le build passe, le site s'affiche, les tests visuels ne détectent rien. Un incident similaire a été documenté dans un contexte différent lors d'une [refonte header où le H1 a été remplacé par un div](/blog/refonte-header-le-h1-remplace-par-un-div-title-par-le-design-system), et lors d'une [migration Gatsby vers Astro où le RSS feed est resté orphelin pendant 6 semaines](/blog/migration-gatsby-vers-astro-rss-feed-orphelin-pendant-6-semaines). Le pattern est toujours le même : un changement structurel dans le wiring des composants, aucune erreur visible, et une dégradation SEO progressive que personne ne surveille.\n\n## Ce qu'on en retient\n\nLe frontmatter ne se transmet pas par magie. Quand un framework change son API de data-fetching — même subtilement — le contrat entre la source de données et le template se brise sans bruit. Les tests navigateur ne protègent pas contre les régressions de HTML statique. Seul un contrôle sur le HTML brut généré, exécuté à chaque build, peut détecter un `\u003Ctitle>` vide avant que Googlebot ne le fasse à notre place.\n\nUn monitoring continu comme Seogard détecte ce type de divergence entre HTML source et DOM hydraté en quelques minutes — pas en dix-huit jours. Mais même sans outil externe, un script shell de 12 lignes dans le CI suffit à éviter 8 400 clics perdus. Il n'y a aucune excuse pour ne pas l'avoir.\n```","https://seogard.io/blog/astro-content-collections-frontmatter-title-non-passe-apres-refacto","Framework","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.","\u003Ch1>80 articles sans title : quand Astro Content Collections cassent en silence\u003C/h1>\n\u003Cp>Jeudi 14h20. L'équipe technique d'une agence SaaS B2B pousse un upgrade d'Astro 4.x vers 5.x sur un blog de 80 articles. Le build passe. Le déploiement passe. Lighthouse affiche un 98 en performance. Le designer valide le rendu. Personne ne regarde le \u003Ccode>&#x3C;head>\u003C/code>. Dix-huit jours plus tard, Search Console envoie un mail : \"Amélioration des titres — 80 pages avec un titre manquant\". Le trafic organique du blog a déjà perdu 34 % de ses clics.\u003C/p>\n\u003Ch2>Lundi matin, T+18 jours — L'alerte qui réveille\u003C/h2>\n\u003Cp>Le lead SEO ouvre Search Console à 8h52. Le rapport \"Améliorations\" affiche un bandeau jaune inédit. Il clique. 80 URLs du blog sont signalées avec le problème \"Balise title manquante ou vide\". Il rafraîchit. Même résultat.\u003C/p>\n\u003Cp>Premier réflexe : ouvrir une page dans le navigateur. Le titre s'affiche dans l'onglet. Tout semble normal. Il inspecte le DOM avec Chrome DevTools. Le \u003Ccode>&#x3C;title>\u003C/code> est bien là, injecté côté client par un composant JavaScript. Mais DevTools montre le DOM \u003Cem>après\u003C/em> hydratation. Pas le HTML source.\u003C/p>\n\u003Cp>Il passe à \u003Ccode>curl\u003C/code> :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -s\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> https://blog.example.com/posts/guide-onboarding-2026\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -i\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '&#x3C;title>'\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\">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>\u003C/code>\u003C/pre>\n\u003Cp>Vide. Quatre caractères. Pas d'erreur. Pas de 500. Juste un \u003Ccode>&#x3C;title>\u003C/code> creux servi à chaque crawler de la planète depuis dix-huit jours.\u003C/p>\n\u003Cp>Il lance un crawl Screaming Frog sur les 80 URLs du répertoire \u003Ccode>/posts/\u003C/code>. Résultat : 80 pages avec \u003Ccode>&#x3C;title>\u003C/code> vide. 80 pages avec \u003Ccode>&#x3C;meta name=\"description\">\u003C/code> vide. 80 pages avec \u003Ccode>&#x3C;meta property=\"og:title\">\u003C/code> absent. Le trifecta.\u003C/p>\n\u003Cp>Le lead SEO remonte dans GA4. Le segment \"Organic Search\" sur le scope \u003Ccode>/posts/\u003C/code> montre une chute progressive. Pas un cliff — une glissade. −12 % la première semaine. −22 % la deuxième. −34 % au moment de l'alerte. Environ 4 200 clics perdus sur la période. Le blog génère 40 % des MQLs de l'entreprise. La réunion de crise est convoquée à 9h15.\u003C/p>\n\u003Cp>L'hypothèse initiale du dev lead : \"C'est peut-être un problème de cache CDN.\" L'équipe purge le cache Cloudflare. Nouveau \u003Ccode>curl\u003C/code>. Toujours vide. Deuxième hypothèse : \"Le composant \u003Ccode>&#x3C;head>\u003C/code> a un bug d'import.\" Le dev ouvre le layout principal, vérifie les imports. Tout compile. Troisième hypothèse : \"C'est un problème de données — le CMS ne renvoie plus les titres.\" L'équipe vérifie les fichiers Markdown sources. Les frontmatters sont intacts. Chaque fichier contient bien un champ \u003Ccode>title\u003C/code>.\u003C/p>\n\u003Cp>C'est à 9h47 que le dev frontend junior lâche la phrase qui déclenche le vrai diagnostic : \"On a changé la façon de récupérer les articles quand on est passé à Astro 5, non ?\"\u003C/p>\n\u003Ch2>Le bug : Content Collections v2 et le fantôme de \u003Ccode>entry.data\u003C/code>\u003C/h2>\n\u003Cp>Le blog utilise Astro avec des Content Collections. Chaque article est un fichier \u003Ccode>.md\u003C/code> dans \u003Ccode>src/content/posts/\u003C/code>. Le frontmatter ressemble à ceci :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">---\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">title\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"Guide complet de l'onboarding client en 2026\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">description\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"Les 7 étapes clés pour un onboarding qui réduit le churn de 40%.\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">pubDate\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">2026-05-12\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">author\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"Marie Dupont\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">tags\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: [\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"onboarding\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"saas\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"product\"\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:#E1E4E8\">Le contenu de l'article commence ici...\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Avant l'upgrade, en Astro 4.x, le schéma de collection dans \u003Ccode>src/content/config.ts\u003C/code> était déclaré 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\">// src/content/config.ts — Astro 4.x\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { defineCollection, z } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'astro:content'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> posts\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> defineCollection\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  schema: z.\u003C/span>\u003Cspan style=\"color:#B392F0\">object\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    title: z.\u003C/span>\u003Cspan style=\"color:#B392F0\">string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    description: z.\u003C/span>\u003Cspan style=\"color:#B392F0\">string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    pubDate: z.\u003C/span>\u003Cspan style=\"color:#B392F0\">date\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    author: z.\u003C/span>\u003Cspan style=\"color:#B392F0\">string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    tags: z.\u003C/span>\u003Cspan style=\"color:#B392F0\">array\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(z.\u003C/span>\u003Cspan style=\"color:#B392F0\">string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">()),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  }),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">});\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> collections\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { posts };\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Et le layout de page article (\u003Ccode>src/layouts/PostLayout.astro\u003C/code>) récupérait les données comme ceci :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">---\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// src/layouts/PostLayout.astro — Astro 4.x\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#79B8FF\">frontmatter\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> Astro.props;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">---\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>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">head\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">title\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>{frontmatter.title}&#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\">={frontmatter.description} />\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\">={frontmatter.title} />\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\">h1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>{frontmatter.title}&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">h1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">slot\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>Ce pattern fonctionnait parfaitement. Quand Astro rendait une page \u003Ccode>.md\u003C/code>, il passait le frontmatter directement via \u003Ccode>Astro.props.frontmatter\u003C/code> au layout déclaré dans le fichier Markdown.\u003C/p>\n\u003Cp>Lors de l'upgrade vers Astro 5.x, l'équipe a migré vers la nouvelle API Content Collections. La page dynamique \u003Ccode>src/pages/posts/[...slug].astro\u003C/code> a été refactorisée :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">---\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// src/pages/posts/[...slug].astro — Astro 5.x (après refacto)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { getCollection } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'astro:content'\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\"> PostLayout \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '../../layouts/PostLayout.astro'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> async\u003C/span>\u003Cspan style=\"color:#F97583\"> function\u003C/span>\u003Cspan style=\"color:#B392F0\"> getStaticPaths\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\"> posts\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> getCollection\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'posts'\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\"> posts.\u003C/span>\u003Cspan style=\"color:#B392F0\">map\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">entry\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    params: { slug: entry.slug },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    props: { entry },\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:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#79B8FF\">entry\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> Astro.props;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#79B8FF\">Content\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> entry.\u003C/span>\u003Cspan style=\"color:#B392F0\">render\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">---\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">PostLayout\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">Content\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#79B8FF\">PostLayout\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le problème est ici, en trois lignes.\u003C/p>\n\u003Cp>L'ancien pattern passait \u003Ccode>frontmatter\u003C/code> comme prop au layout. Le nouveau pattern passe \u003Ccode>entry\u003C/code> entier comme prop — mais \u003Cstrong>ne transmet rien au layout\u003C/strong>. Le composant \u003Ccode>PostLayout\u003C/code> reçoit un \u003Ccode>Astro.props\u003C/code> qui contient... un \u003Ccode>slot\u003C/code>. Pas de \u003Ccode>frontmatter\u003C/code>. Pas de \u003Ccode>entry.data\u003C/code>. Rien.\u003C/p>\n\u003Cp>Le layout cherche \u003Ccode>Astro.props.frontmatter.title\u003C/code>. Il obtient \u003Ccode>undefined\u003C/code>. Astro ne lève aucune erreur. En mode SSG, \u003Ccode>undefined\u003C/code> dans une expression \u003Ccode>{frontmatter.title}\u003C/code> produit une chaîne vide. Le build passe. TypeScript, si configuré en mode lax (ce qui était le cas ici), ne signale rien.\u003C/p>\n\u003Cp>Le HTML rendu côté serveur — celui que Googlebot reçoit — ressemble à ceci :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">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\">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:#6A737D\">  &#x3C;!-- og:title absent car content=\"\" est filtré -->\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\">h1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">h1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">article\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">p\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Le contenu de l'article commence ici...&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">p\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">article\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 \u003Ccode>&#x3C;h1>\u003C/code> est également vide. Le contenu brut de l'article est bien rendu (via \u003Ccode>&#x3C;Content />\u003C/code>), mais toute la couche de métadonnées a disparu.\u003C/p>\n\u003Cp>Ce que voit le développeur dans le navigateur est différent, et c'est le piège. Le design system de l'équipe utilise un composant React hydraté côté client qui récupère le titre depuis le premier \u003Ccode>&#x3C;h1>\u003C/code> ou depuis un \u003Ccode>&#x3C;script type=\"application/json\">\u003C/code> embarqué. Ce composant injecte le titre dans l'onglet du navigateur via \u003Ccode>document.title\u003C/code>. L'inspection DOM dans DevTools montre donc un \u003Ccode>&#x3C;title>\u003C/code> rempli — mais uniquement après exécution JavaScript.\u003C/p>\n\u003Cp>Googlebot, en 2026, exécute JavaScript. Mais il ne le fait pas systématiquement, et surtout pas immédiatement. Le \u003Ca href=\"https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics\">rendu différé de Google\u003C/a> signifie que la première passe d'indexation se base sur le HTML brut. Si le \u003Ccode>&#x3C;title>\u003C/code> est vide dans le HTML source, Google enregistre \"titre manquant\" avant même d'envisager un rendu JS.\u003C/p>\n\u003Cp>Pour 80 articles, Google a donc généré ses propres titres — souvent la première phrase du contenu, ou le nom de domaine. Résultat : des SERP avec des snippets incohérents, des CTR en chute libre, et un signal clair de dégradation qualitative.\u003C/p>\n\u003Cp>Le diagnostic complet est confirmé par un test avec l'outil \"Inspection d'URL\" de Search Console. L'onglet \"HTML rendu\" montre le \u003Ccode>&#x3C;title>\u003C/code> vide sur le HTML initial. L'onglet \"Capture d'écran\" montre le titre visible (grâce au JS client). Deux réalités parallèles.\u003C/p>\n\u003Cp>Pour comprendre pourquoi les tests unitaires et le CI n'ont rien détecté, il faut regarder la pipeline. L'équipe testait le build Astro avec un simple \u003Ccode>astro build &#x26;&#x26; echo \"OK\"\u003C/code>. Le build ne crashe pas quand une expression template retourne \u003Ccode>undefined\u003C/code>. Ils avaient un test Playwright qui vérifiait la présence d'un \u003Ccode>&#x3C;h1>\u003C/code> visible — mais le test s'exécute dans un vrai navigateur avec JS, donc il voit le titre injecté côté client. Zéro test sur le HTML statique brut. C'est un pattern que l'on retrouve fréquemment, et que l'on a déjà documenté lors d'une \u003Ca href=\"/blog/astro-v5-vers-v6-content-collections-cassent-le-mapping-frontmatter-title\">migration Astro où le mapping frontmatter title avait cassé de la même manière\u003C/a>.\u003C/p>\n\u003Ch2>Le fix : reconnecter \u003Ccode>entry.data\u003C/code> au layout\u003C/h2>\n\u003Cp>Le correctif tient en deux modifications. La première, dans la page dynamique, consiste à passer explicitement les données du frontmatter au layout :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">---\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// src/pages/posts/[...slug].astro — CORRIGÉ\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { getCollection } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'astro:content'\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\"> PostLayout \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '../../layouts/PostLayout.astro'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> async\u003C/span>\u003Cspan style=\"color:#F97583\"> function\u003C/span>\u003Cspan style=\"color:#B392F0\"> getStaticPaths\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\"> posts\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> getCollection\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'posts'\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\"> posts.\u003C/span>\u003Cspan style=\"color:#B392F0\">map\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">entry\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    params: { slug: entry.slug },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    props: { entry },\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:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#79B8FF\">entry\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> Astro.props;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#79B8FF\">Content\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> entry.\u003C/span>\u003Cspan style=\"color:#B392F0\">render\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">---\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">PostLayout\u003C/span>\u003Cspan style=\"color:#B392F0\"> frontmatter\u003C/span>\u003Cspan style=\"color:#E1E4E8\">={entry.data}>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">Content\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#79B8FF\">PostLayout\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>La clé : \u003Ccode>entry.data\u003C/code> contient exactement le même objet que l'ancien \u003Ccode>frontmatter\u003C/code>. La seule différence est qu'il faut le passer explicitement comme prop.\u003C/p>\n\u003Cp>La deuxième modification, dans le layout, consiste à typer la prop pour éviter toute régression future :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">---\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// src/layouts/PostLayout.astro — CORRIGÉ avec typage\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">interface\u003C/span>\u003Cspan style=\"color:#B392F0\"> Props\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  frontmatter\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">    title\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">    description\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">    pubDate\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> Date\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">    author\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">    tags\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">[];\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  };\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#79B8FF\">frontmatter\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> Astro.props;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">---\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>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">head\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">title\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>{frontmatter.title}&#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\">={frontmatter.description} />\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\">={frontmatter.title} />\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\">h1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>{frontmatter.title}&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">h1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">slot\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>Avec ce typage, si \u003Ccode>frontmatter\u003C/code> est \u003Ccode>undefined\u003C/code> ou si \u003Ccode>title\u003C/code> manque, le build Astro échoue en mode strict (\u003Ccode>astro check\u003C/code>). L'erreur est détectée au CI, pas en production.\u003C/p>\n\u003Cp>L'équipe a ajouté un troisième filet de sécurité : un test dans la pipeline CI qui vérifie le HTML statique généré, sans navigateur :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># scripts/check-titles.sh\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">astro\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> build\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">MISSING\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">0\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> file \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> dist/posts/*/index.html\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\">grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -oP\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '(?&#x3C;=&#x3C;title>).*(?=&#x3C;/title>)'\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$file\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [ \u003C/span>\u003Cspan style=\"color:#F97583\">-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\"> \"ERREUR: title vide dans \u003C/span>\u003Cspan style=\"color:#E1E4E8\">$file\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    MISSING\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$((\u003C/span>\u003Cspan style=\"color:#B392F0\">MISSING\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> +\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">))\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:#F97583\">if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [ \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$MISSING\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#F97583\"> -gt\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0\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\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$MISSING\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> page(s) avec title vide. Build rejeté.\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  exit\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 1\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">fi\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">echo\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"Toutes les pages ont un title. OK.\"\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Ce script parcourt les fichiers HTML générés par \u003Ccode>astro build\u003C/code> et vérifie que chaque \u003Ccode>&#x3C;title>\u003C/code> contient du texte. Il s'exécute après le build, avant le déploiement. Coût : 3 secondes sur une pipeline de 45 secondes.\u003C/p>\n\u003Cp>Le déploiement du fix a été effectué le jour même, à 11h30. Le cache Cloudflare a été purgé dans la foulée. L'équipe a ensuite soumis les 80 URLs à la réindexation via Search Console (par lots de 10, la limite quotidienne étant ce qu'elle est).\u003C/p>\n\u003Cp>La récupération a pris du temps. Voici la chronologie observée :\u003C/p>\n\u003Cul>\n\u003Cli>\u003Cstrong>J+2\u003C/strong> : Google a recrawlé 31 des 80 pages. Les titles corrects apparaissent dans le cache Google.\u003C/li>\n\u003Cli>\u003Cstrong>J+5\u003C/strong> : 68 pages recrawlées. Les snippets commencent à afficher les vrais titres dans les SERP.\u003C/li>\n\u003Cli>\u003Cstrong>J+12\u003C/strong> : 80/80 pages recrawlées. Le trafic organique remonte à −15 % par rapport au niveau pré-incident.\u003C/li>\n\u003Cli>\u003Cstrong>J+21\u003C/strong> : Le trafic organique retrouve son niveau initial. Certaines pages ont même gagné des positions, probablement parce que les titres corrects matchent mieux l'intention de recherche que les titres auto-générés par Google.\u003C/li>\n\u003C/ul>\n\u003Cp>Au total : 18 jours de dégradation silencieuse, 3 semaines de récupération, environ 8 400 clics perdus cumulés.\u003C/p>\n\u003Cp>L'équipe a mis en place deux garde-fous supplémentaires. Le premier : activer \u003Ccode>astro check\u003C/code> en mode strict dans le CI, avec le flag \u003Ccode>--tsconfig tsconfig.json\u003C/code> configuré en \u003Ccode>\"strict\": true\u003C/code>. Le second : un crawl Screaming Frog hebdomadaire automatisé qui alerte sur tout \u003Ccode>&#x3C;title>\u003C/code> vide ou dupliqué, configuré via l'API Screaming Frog.\u003C/p>\n\u003Cp>Ce type de régression silencieuse est spécialement vicieux sur les stacks SSG. Le build passe, le site s'affiche, les tests visuels ne détectent rien. Un incident similaire a été documenté dans un contexte différent lors d'une \u003Ca href=\"/blog/refonte-header-le-h1-remplace-par-un-div-title-par-le-design-system\">refonte header où le H1 a été remplacé par un div\u003C/a>, et lors d'une \u003Ca href=\"/blog/migration-gatsby-vers-astro-rss-feed-orphelin-pendant-6-semaines\">migration Gatsby vers Astro où le RSS feed est resté orphelin pendant 6 semaines\u003C/a>. Le pattern est toujours le même : un changement structurel dans le wiring des composants, aucune erreur visible, et une dégradation SEO progressive que personne ne surveille.\u003C/p>\n\u003Ch2>Ce qu'on en retient\u003C/h2>\n\u003Cp>Le frontmatter ne se transmet pas par magie. Quand un framework change son API de data-fetching — même subtilement — le contrat entre la source de données et le template se brise sans bruit. Les tests navigateur ne protègent pas contre les régressions de HTML statique. Seul un contrôle sur le HTML brut généré, exécuté à chaque build, peut détecter un \u003Ccode>&#x3C;title>\u003C/code> vide avant que Googlebot ne le fasse à notre place.\u003C/p>\n\u003Cp>Un monitoring continu comme Seogard détecte ce type de divergence entre HTML source et DOM hydraté en quelques minutes — pas en dix-huit jours. Mais même sans outil externe, un script shell de 12 lignes dans le CI suffit à éviter 8 400 clics perdus. Il n'y a aucune excuse pour ne pas l'avoir.\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,11,[18,19,20,21],"astro","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)",[25,38,54,68,83,97],{"_id":26,"slug":27,"__v":6,"author":7,"canonical":28,"category":10,"createdAt":29,"date":30,"description":31,"image":15,"imageAlt":15,"readingTime":16,"tags":32,"title":36,"updatedAt":37},"6a2595ecaa6b273b0cf78369","astro-site-config-build-local-le-sitemap-entier-pointe-vers-un-domaine-inexistant","https://seogard.io/blog/astro-site-config-build-local-le-sitemap-entier-pointe-vers-un-domaine-inexistant","2026-06-07T16:01:48.153Z","2026-06-07","Un site Astro envoie un sitemap.xml avec 4 000 URLs build.local à Google. Récit de l'incident, diagnostic technique et fix complet.",[18,33,34,35],"sitemap","site config","build","Astro sitemap pointe vers build.local : 4 000 URLs perdues","Sun Jun 07 2026 16:01:48 GMT+0000 (Coordinated Universal Time)",{"_id":39,"slug":40,"__v":6,"author":7,"canonical":41,"category":42,"createdAt":43,"date":12,"description":44,"image":15,"imageAlt":15,"readingTime":45,"tags":46,"title":52,"updatedAt":53},"6a265b17aa6b273b0c9a6fff","your-next-ai-visitor-will-know-who-sent-it-via-sejournal-slobodanmanic","https://seogard.io/blog/your-next-ai-visitor-will-know-who-sent-it-via-sejournal-slobodanmanic","Actualités SEO","2026-06-08T06:03:03.429Z","Les agents AI arrivent avec le contexte utilisateur. Comment adapter votre contenu pour rester utile face au blended retrieval.",12,[47,48,49,50,51],"AI agents","blended retrieval","SEO technique","crawl AI","structured data","AI Visitors contextuels : préparer vos pages au blended retrieval","Mon Jun 08 2026 06:03:03 GMT+0000 (Coordinated Universal Time)",{"_id":55,"slug":56,"__v":6,"author":7,"canonical":57,"category":58,"createdAt":59,"date":30,"description":60,"image":15,"imageAlt":15,"readingTime":45,"tags":61,"title":66,"updatedAt":67},"6a250954aa6b273b0c8358fe","refonte-typo-variable-font-lazy-load-qui-degrade-les-core-web-vitals","https://seogard.io/blog/refonte-typo-variable-font-lazy-load-qui-degrade-les-core-web-vitals","Performance","2026-06-07T06:01:56.599Z","Une refonte typo charge la police en lazy. Le LCP passe de 1.8s à 3.0s. Aucune meta ne bouge. Le trafic chute de 18%. Récit, diagnostic, fix.",[62,63,64,65],"core web vitals","lcp","font","performance","Variable font lazy-load : LCP dégradé de 1.2s, ranking en chute","Sun Jun 07 2026 06:01:56 GMT+0000 (Coordinated Universal Time)",{"_id":69,"slug":70,"__v":6,"author":7,"canonical":71,"category":72,"createdAt":73,"date":74,"description":75,"image":15,"imageAlt":15,"readingTime":45,"tags":76,"title":81,"updatedAt":82},"6a23b7d0aa6b273b0c6c840f","splash-screen-noscript-mal-place-qui-contient-le-vrai-contenu-pour-googlebot-sansjs","https://seogard.io/blog/splash-screen-noscript-mal-place-qui-contient-le-vrai-contenu-pour-googlebot-sansjs","Rendering","2026-06-06T06:01:52.615Z","2026-06-06","Un e-commerce SPA cache son contenu dans une balise noscript pour les bots. Google détecte du cloaking. Récit, diagnostic et fix complet.",[77,78,79,80],"noscript","cloaking","spa","splash","noscript cloaking : splash screen SPA piège Google","Sat Jun 06 2026 06:01:52 GMT+0000 (Coordinated Universal Time)",{"_id":84,"slug":85,"__v":6,"author":7,"canonical":86,"category":42,"createdAt":87,"date":74,"description":88,"image":15,"imageAlt":15,"readingTime":45,"tags":89,"title":95,"updatedAt":96},"6a2444b8aa6b273b0ce0dbc7","cloudflare-bots-now-make-up-57-of-webpage-requests","https://seogard.io/blog/cloudflare-bots-now-make-up-57-of-webpage-requests","2026-06-06T16:03:04.236Z","Cloudflare révèle que 57% des requêtes web sont des bots. Analyse technique des impacts SEO et stratégies concrètes pour protéger votre crawl budget.",[90,91,92,93,94],"cloudflare","bots","crawl budget","seo technique","trafic automatisé","57% de bots : impact SEO et stratégies de défense technique","Sat Jun 06 2026 16:03:04 GMT+0000 (Coordinated Universal Time)",{"_id":98,"slug":99,"__v":6,"author":7,"canonical":100,"category":42,"createdAt":101,"date":102,"description":103,"image":15,"imageAlt":15,"readingTime":45,"tags":104,"title":109,"updatedAt":110},"6a226691aa6b273b0c562a67","google-s-may-core-update-favored-pages-that-match-intent-via-sejournal-mattgsouthern","https://seogard.io/blog/google-s-may-core-update-favored-pages-that-match-intent-via-sejournal-mattgsouthern","2026-06-05T06:02:57.864Z","2026-06-05","Analyse technique du May 2025 Core Update de Google : comment l'alignement intent/contenu et les signaux techniques déterminent les gagnants et perdants.",[105,106,107,49,108],"core update","search intent","SISTRIX","google algorithm","May 2025 Core Update : intent matching et signaux techniques","Fri Jun 05 2026 06:02:57 GMT+0000 (Coordinated Universal Time)",{"categories":112},[113,116,120,123,125,129,132,135,139,143,146,149,153,156,159,162,165,169],{"category":42,"slug":114,"count":115},"actualites-seo",163,{"category":117,"slug":118,"count":119},"Migration","migration",18,{"category":72,"slug":121,"count":122},"rendering",9,{"category":58,"slug":65,"count":124},8,{"category":126,"slug":127,"count":128},"Meta Tags","meta-tags",7,{"category":130,"slug":131,"count":128},"Crawl","crawl",{"category":133,"slug":134,"count":128},"SEO Technique","seo-technique",{"category":136,"slug":137,"count":138},"Architecture","architecture",6,{"category":140,"slug":141,"count":142},"Structured Data","structured-data",5,{"category":144,"slug":145,"count":142},"JavaScript SEO","javascript-seo",{"category":147,"slug":148,"count":142},"Monitoring","monitoring",{"category":150,"slug":151,"count":152},"E-commerce","e-commerce",4,{"category":154,"slug":155,"count":152},"Avancé","avance",{"category":157,"slug":158,"count":152},"Refonte","refonte",{"category":160,"slug":161,"count":152},"Outils","outils",{"category":163,"slug":164,"count":152},"Redirections","redirections",{"category":166,"slug":167,"count":168},"IA & SEO","ia-seo",3,{"category":170,"slug":171,"count":168},"Contenu","contenu"]