[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$ftjiU6ZI76KyzbZhDyE-gOycAVEKBbwGQI4vqckCpyfQ":3,"$fUK7tGmGvwsm-THTVR1TXihEIWaieaIrAPvM3z9n2t7I":24,"$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":22,"updatedAt":23},"6a298a64aa6b273b0c3bfcab","sveltekit-layout-ts-title-override-par-page-svelte-vide",0,"Equipe Seogard","# SvelteKit : quand un +page.svelte vide efface le title pour Googlebot\n\nJeudi 14h. Un studio web français livre la refonte complète d'un site vitrine B2B SaaS — 320 pages, 18 000 visites organiques par mois. Stack : SvelteKit 2.5, adapter-node, déployé sur Fly.io. Le title de chaque page est centralisé dans `+layout.ts`. Propre, DRY, élégant. Le navigateur affiche les bons titres partout. Lighthouse passe au vert. Personne ne regarde le HTML brut servi en SSR. Trois semaines plus tard, la home a perdu 73 % de son trafic organique. Le `\u003Ctitle>` que voit Googlebot est une chaîne vide.\n\n## Lundi, T+18 jours — \"On a un problème avec la home\"\n\n9h12. La responsable marketing ouvre Search Console pour préparer le reporting mensuel. Le rapport de performance montre un effondrement net de la query principale — le nom de marque. De 420 clics par jour à 110. La courbe a décroché le lendemain du déploiement, il y a dix-huit jours.\n\n9h31. Elle envoie un screenshot au lead dev. Première hypothèse : un problème d'indexation post-migration, peut-être un canonical cassé. L'équipe vérifie. Le canonical est correct. Le sitemap est présent. Le robots.txt autorise tout.\n\n9h58. Un dev ouvre l'inspecteur Chrome sur la home. L'onglet du navigateur affiche bien \"Acme — Plateforme de gestion d'équipe\". Rien d'anormal. Il ouvre le code source (`Ctrl+U`). Le `\u003Ctitle>` est là, dans le `\u003Chead>`. Hypothèse écartée — le title semble correct.\n\n10h14. La responsable marketing insiste. Elle montre le rapport \"Pages\" de Search Console. La home est listée avec le titre affiché : rien. Littéralement une cellule vide dans la colonne \"Title\". Le dev relance une inspection d'URL dans Search Console. Le HTML rendu par Googlebot contient :\n\n```html\n\u003Ctitle>\u003C/title>\n```\n\nVide. Pas de fallback, pas de texte partiel. Une balise title ouverte et fermée sans contenu.\n\n10h33. L'équipe lance un crawl Screaming Frog en mode \"JavaScript rendering\" sur les 320 URLs du site. Résultat : la home et 11 pages \"hub\" (celles sans `+page.svelte` dédié, qui héritent du layout) ont un `\u003Ctitle>` vide. Les 308 autres pages — celles avec un composant `+page.svelte` explicite qui définit leur propre title via `\u003Csvelte:head>` — sont correctes.\n\n11h02. Le pattern est clair. Le problème touche exclusivement les pages dont le title provient du layout parent et qui n'ont pas de `\u003Csvelte:head>` dans leur propre `+page.svelte`. Le lead dev comprend que ce n'est pas un bug mineur. Les 12 pages affectées représentent 41 % du trafic organique total — la home à elle seule en représente 22 %.\n\nLes chiffres consolidés sur les 18 jours écoulés : −4 200 clics sur la home, −1 800 clics sur les pages hub. Impressions en chute de 38 %. Le CTR de la home est passé de 12 % à 3,4 % — Google affiche l'URL brute à la place du title manquant, ou un extrait incohérent tiré du body.\n\n## Le bug : comment +layout.ts perd la bataille contre +page.svelte\n\nPour comprendre ce qui s'est passé, il faut remonter à l'architecture initiale du projet.\n\n### La stratégie de titles centralisée\n\nL'équipe avait choisi de gérer tous les titles dans `+layout.ts` à la racine, via la fonction `load` :\n\n```typescript\n// src/routes/+layout.ts\nexport const load = async ({ url }) => {\n  const titles: Record\u003Cstring, string> = {\n    '/': 'Acme — Plateforme de gestion d\\'équipe',\n    '/produit': 'Produit — Fonctionnalités Acme',\n    '/tarifs': 'Tarifs — Acme',\n    '/blog': 'Blog — Acme',\n    '/a-propos': 'À propos — Acme',\n    // ... 7 autres hubs\n  };\n\n  return {\n    title: titles[url.pathname] ?? 'Acme — Gestion d\\'équipe',\n  };\n};\n```\n\nEt dans le composant layout correspondant :\n\n```svelte\n\u003C!-- src/routes/+layout.svelte -->\n\u003Cscript>\n  export let data;\n\u003C/script>\n\n\u003Csvelte:head>\n  \u003Ctitle>{data.title}\u003C/title>\n  \u003Cmeta name=\"description\" content=\"Acme simplifie la gestion d'équipe.\" />\n\u003C/svelte:head>\n\n\u003Cslot />\n```\n\nÀ ce stade, tout fonctionne. Le layout définit le `\u003Ctitle>`, les pages enfants héritent. Dans le navigateur, le SSR produit le bon HTML. Googlebot voit le bon title.\n\n### L'arrivée du problème : le +page.svelte \"vide\"\n\nPendant la refonte, un développeur crée un `+page.svelte` pour la home afin d'y ajouter un composant hero. Le fichier est minimal :\n\n```svelte\n\u003C!-- src/routes/+page.svelte -->\n\u003Cscript>\n  export let data;\n\u003C/script>\n\n\u003CHero />\n\u003CFeatures />\n\u003CTestimonials />\n```\n\nPas de `\u003Csvelte:head>`. Pas de title. Le développeur considère que le layout s'en charge — ce qui est vrai dans la plupart des frameworks. Mais SvelteKit a un comportement spécifique documenté dans une [section discrète de la doc officielle](https://kit.svelte.dev/docs/seo#manual-setup) : **quand un `+page.svelte` existe pour une route, SvelteKit considère que cette page \"possède\" son propre `\u003Chead>`**.\n\n### Le mécanisme de résolution du `\u003Csvelte:head>`\n\nVoici ce que SvelteKit fait réellement lors du rendu SSR :\n\n1. Le layout rend son `\u003Csvelte:head>`, qui injecte `\u003Ctitle>Acme — Plateforme de gestion d'équipe\u003C/title>`.\n2. Le `+page.svelte` est rendu à l'intérieur du `\u003Cslot />`.\n3. SvelteKit détecte que la page enfant a été rendue. Même sans `\u003Csvelte:head>` explicite, le framework applique un mécanisme de \"reset\" des balises head gérées par le parent **si la page enfant est considérée comme active**.\n\nLe résultat en SSR — le HTML brut envoyé au client (et à Googlebot) :\n\n```html\n\u003C!DOCTYPE html>\n\u003Chtml lang=\"fr\">\n\u003Chead>\n  \u003Cmeta charset=\"utf-8\" />\n  \u003Ctitle>\u003C/title>\n  \u003Cmeta name=\"description\" content=\"Acme simplifie la gestion d'équipe.\" />\n\u003C/head>\n\u003Cbody>\n  \u003C!-- contenu du hero, features, etc. -->\n\u003C/body>\n\u003C/html>\n```\n\nLe `\u003Ctitle>` est vide. La meta description, elle, survit — parce qu'elle est définie dans le layout et n'entre pas en collision avec un composant enfant.\n\nLe piège est subtil : **côté client, après hydratation, le title s'affiche correctement dans l'onglet du navigateur**. Le JavaScript du layout s'exécute, met à jour le DOM, et le title apparaît. Mais Googlebot, même s'il exécute du JavaScript, reçoit d'abord le HTML SSR. Et dans de nombreux cas de pages légères comme celle-ci, le renderer de Google s'appuie sur le HTML initial sans ré-exécuter le cycle complet de hydratation côté client.\n\n### La preuve par curl\n\nUne simple commande suffit à reproduire le problème :\n\n```bash\ncurl -s https://acme.example.com/ | grep -oP '\u003Ctitle>.*?\u003C/title>'\n```\n\nRésultat :\n\n```\n\u003Ctitle>\u003C/title>\n```\n\nLa même commande sur une page avec un `\u003Csvelte:head>` explicite, par exemple `/blog/premiers-pas` :\n\n```\n\u003Ctitle>Premiers pas avec Acme — Blog\u003C/title>\n```\n\n### Pourquoi les tests n'ont rien vu\n\nTrois raisons convergentes :\n\n**1. Les tests E2E vérifient le DOM après hydratation.** L'équipe utilise Playwright. Leurs assertions portent sur `page.title()`, qui renvoie le title visible dans l'onglet — donc le title post-hydratation, correct. Aucun test ne vérifie le HTML brut de la réponse SSR.\n\n**2. Lighthouse et les audits navigateur voient le rendu final.** Lighthouse exécute la page dans un Chromium complet. Le JavaScript tourne, le title est injecté côté client. Score SEO : 100.\n\n**3. Le preview Vercel/Fly.io est identique.** L'environnement de staging sert le même comportement. Le QA visuel ne détecte rien parce que l'onglet du navigateur affiche le bon title.\n\nLe problème n'est visible que dans deux contextes : le HTML brut (curl, `View Source`) et le rendu Googlebot (Search Console > Inspection d'URL). L'équipe ne vérifie ni l'un ni l'autre avant de déployer.\n\nCe pattern — une divergence entre le rendu navigateur et le rendu SSR/HTTP — est un classique des frameworks modernes. On l'a documenté dans des contextes similaires avec [Nuxt et le useSeometa écrasé par un composant enfant](/blog/nuxt-useseometa-le-child-override-silencieusement-les-meta-du-layout-parent) et avec [Next.js et les metadata async qui servent un fallback vide](/blog/next-js-metadata-async-qui-throw-la-page-sert-le-fallback-default-next-js).\n\n## Le fix : trois niveaux de correction\n\n### Niveau 1 — Le patch immédiat\n\nAjouter un `\u003Csvelte:head>` explicite dans chaque `+page.svelte` qui n'en a pas :\n\n```svelte\n\u003C!-- src/routes/+page.svelte (corrigé) -->\n\u003Cscript>\n  export let data;\n\u003C/script>\n\n\u003Csvelte:head>\n  \u003Ctitle>{data.title}\u003C/title>\n\u003C/svelte:head>\n\n\u003CHero />\n\u003CFeatures />\n\u003CTestimonials />\n```\n\nLe `data.title` provient toujours du `+layout.ts` parent — il est accessible dans `data` car SvelteKit merge les données du layout et de la page. Le `\u003Csvelte:head>` de la page prend explicitement le relais.\n\nL'équipe patche les 12 fichiers concernés en 20 minutes. Commit, déploiement.\n\n### Niveau 2 — La vérification post-déploiement\n\nAprès déploiement, validation immédiate :\n\n```bash\n# Vérifier les 12 URLs patchées\nfor url in \"/\" \"/produit\" \"/tarifs\" \"/blog\" \"/a-propos\" \"/contact\" \\\n  \"/cas-clients\" \"/integrations\" \"/securite\" \"/api\" \"/changelog\" \"/presse\"; do\n  title=$(curl -s \"https://acme.example.com${url}\" | grep -oP '(?\u003C=\u003Ctitle>).*?(?=\u003C/title>)')\n  echo \"${url} → ${title}\"\ndone\n```\n\nRésultat attendu :\n\n```\n/ → Acme — Plateforme de gestion d'équipe\n/produit → Produit — Fonctionnalités Acme\n/tarifs → Tarifs — Acme\n...\n```\n\nEnsuite, demande de ré-indexation dans Search Console pour chacune des 12 URLs. L'équipe utilise l'API Indexing pour accélérer — mais celle-ci est réservée aux pages `JobPosting` et `BroadcastEvent`. Pour les autres, la demande manuelle via l'interface reste la seule option.\n\n### Niveau 3 — La protection contre la récurrence\n\nL'équipe met en place trois garde-fous :\n\n**1. Un test Playwright qui vérifie le HTML SSR brut :**\n\n```typescript\n// tests/seo/title-ssr.spec.ts\nimport { test, expect } from '@playwright/test';\n\nconst criticalPages = [\n  { url: '/', expected: 'Acme — Plateforme de gestion d\\'équipe' },\n  { url: '/produit', expected: 'Produit — Fonctionnalités Acme' },\n  { url: '/tarifs', expected: 'Tarifs — Acme' },\n];\n\nfor (const page of criticalPages) {\n  test(`SSR title for ${page.url}`, async ({ request }) => {\n    const response = await request.get(page.url);\n    const html = await response.text();\n    const match = html.match(/\u003Ctitle>(.*?)\u003C\\/title>/);\n    expect(match).not.toBeNull();\n    expect(match![1]).toBe(page.expected);\n  });\n}\n```\n\nCe test utilise `request.get` — pas de navigateur, pas d'hydratation. Il vérifie le HTML brut, exactement comme curl ou Googlebot en première passe.\n\n**2. Un hook SvelteKit qui logge un warning en dev si le title SSR est vide :**\n\n```typescript\n// src/hooks.server.ts\nimport type { Handle } from '@sveltejs/kit';\n\nexport const handle: Handle = async ({ event, resolve }) => {\n  const response = await resolve(event);\n\n  if (event.url.pathname !== '/__data.json' && response.headers.get('content-type')?.includes('text/html')) {\n    const body = await response.clone().text();\n    const titleMatch = body.match(/\u003Ctitle>(.*?)\u003C\\/title>/);\n\n    if (!titleMatch || titleMatch[1].trim() === '') {\n      console.warn(`[SEO] Empty \u003Ctitle> detected on ${event.url.pathname}`);\n    }\n  }\n\n  return response;\n};\n```\n\nEn développement, ce warning apparaît dans la console du terminal. Impossible de le rater.\n\n**3. Un crawl Screaming Frog hebdomadaire en mode JavaScript rendering**, avec une alerte custom sur les pages dont le title SSR diffère du title JS. L'export CSV est comparé via un script CI qui échoue si une divergence apparaît.\n\n### Temps de récupération\n\nLe déploiement du fix a lieu un mardi à 11h. Voici la timeline de récupération observée dans Search Console :\n\n- **J+2** : Googlebot recrawle la home. Le nouveau title apparaît dans l'inspection d'URL.\n- **J+5** : Le snippet de la home dans les SERPs affiche de nouveau le vrai title.\n- **J+8** : Les impressions de la home remontent à 80 % du niveau pré-incident.\n- **J+14** : Le CTR de la home revient à 11,6 % (vs 12 % avant l'incident). Quasi-retour à la normale.\n- **J+23** : Les 12 pages affectées ont toutes retrouvé leur niveau de trafic antérieur, à ±5 %.\n\nTotal des pertes estimées sur la période de 23 jours d'incident + 14 jours de récupération : environ 8 400 clics organiques. Pour un site B2B SaaS dont le coût d'acquisition payant est à 4,20 € le clic, l'équivalent en budget ads perdu dépasse 35 000 €.\n\nLe parallèle est direct avec d'autres incidents de refonte où un élément SEO critique disparaît sans que personne ne le voie — comme un [H1 remplacé par un div lors d'une refonte header](/blog/refonte-header-le-h1-remplace-par-un-div-title-par-le-design-system) ou un [composant heading du design system qui rend un div au lieu d'un Hn](/blog/design-system-composant-heading-qui-rend-div-selon-la-prop-as-mal-configuree).\n\n## Ce qu'on en retient\n\nLe modèle mental \"le layout gère le title, les pages héritent\" ne tient pas dans SvelteKit. Dès qu'un `+page.svelte` existe, il doit déclarer son propre `\u003Csvelte:head>` — même si le contenu vient du layout. C'est un contrat implicite du framework que ni la documentation ni les outils de dev ne rendent évident.\n\nLe vrai problème n'est pas le bug. C'est le délai de détection : 18 jours. Un test SSR en CI aurait détecté la régression en 30 secondes. Un monitoring continu type Seogard aurait alerté sur la divergence SSR/navigateur dans les minutes suivant le déploiement — pas trois semaines après.\n\nChaque framework a ses pièges de résolution `\u003Chead>`. Les connaître ne suffit pas. Il faut les tester comme du code métier.\n```","https://seogard.io/blog/sveltekit-layout-ts-title-override-par-page-svelte-vide","Framework","2026-06-10T16:01:40.148Z","2026-06-10","Un +page.svelte sans title écrase le layout parent. Googlebot voit un \u003Ctitle> vide. Récit, diagnostic et fix complet en SvelteKit.","\u003Ch1>SvelteKit : quand un +page.svelte vide efface le title pour Googlebot\u003C/h1>\n\u003Cp>Jeudi 14h. Un studio web français livre la refonte complète d'un site vitrine B2B SaaS — 320 pages, 18 000 visites organiques par mois. Stack : SvelteKit 2.5, adapter-node, déployé sur Fly.io. Le title de chaque page est centralisé dans \u003Ccode>+layout.ts\u003C/code>. Propre, DRY, élégant. Le navigateur affiche les bons titres partout. Lighthouse passe au vert. Personne ne regarde le HTML brut servi en SSR. Trois semaines plus tard, la home a perdu 73 % de son trafic organique. Le \u003Ccode>&#x3C;title>\u003C/code> que voit Googlebot est une chaîne vide.\u003C/p>\n\u003Ch2>Lundi, T+18 jours — \"On a un problème avec la home\"\u003C/h2>\n\u003Cp>9h12. La responsable marketing ouvre Search Console pour préparer le reporting mensuel. Le rapport de performance montre un effondrement net de la query principale — le nom de marque. De 420 clics par jour à 110. La courbe a décroché le lendemain du déploiement, il y a dix-huit jours.\u003C/p>\n\u003Cp>9h31. Elle envoie un screenshot au lead dev. Première hypothèse : un problème d'indexation post-migration, peut-être un canonical cassé. L'équipe vérifie. Le canonical est correct. Le sitemap est présent. Le robots.txt autorise tout.\u003C/p>\n\u003Cp>9h58. Un dev ouvre l'inspecteur Chrome sur la home. L'onglet du navigateur affiche bien \"Acme — Plateforme de gestion d'équipe\". Rien d'anormal. Il ouvre le code source (\u003Ccode>Ctrl+U\u003C/code>). Le \u003Ccode>&#x3C;title>\u003C/code> est là, dans le \u003Ccode>&#x3C;head>\u003C/code>. Hypothèse écartée — le title semble correct.\u003C/p>\n\u003Cp>10h14. La responsable marketing insiste. Elle montre le rapport \"Pages\" de Search Console. La home est listée avec le titre affiché : rien. Littéralement une cellule vide dans la colonne \"Title\". Le dev relance une inspection d'URL dans Search Console. Le HTML rendu par Googlebot contient :\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. Pas de fallback, pas de texte partiel. Une balise title ouverte et fermée sans contenu.\u003C/p>\n\u003Cp>10h33. L'équipe lance un crawl Screaming Frog en mode \"JavaScript rendering\" sur les 320 URLs du site. Résultat : la home et 11 pages \"hub\" (celles sans \u003Ccode>+page.svelte\u003C/code> dédié, qui héritent du layout) ont un \u003Ccode>&#x3C;title>\u003C/code> vide. Les 308 autres pages — celles avec un composant \u003Ccode>+page.svelte\u003C/code> explicite qui définit leur propre title via \u003Ccode>&#x3C;svelte:head>\u003C/code> — sont correctes.\u003C/p>\n\u003Cp>11h02. Le pattern est clair. Le problème touche exclusivement les pages dont le title provient du layout parent et qui n'ont pas de \u003Ccode>&#x3C;svelte:head>\u003C/code> dans leur propre \u003Ccode>+page.svelte\u003C/code>. Le lead dev comprend que ce n'est pas un bug mineur. Les 12 pages affectées représentent 41 % du trafic organique total — la home à elle seule en représente 22 %.\u003C/p>\n\u003Cp>Les chiffres consolidés sur les 18 jours écoulés : −4 200 clics sur la home, −1 800 clics sur les pages hub. Impressions en chute de 38 %. Le CTR de la home est passé de 12 % à 3,4 % — Google affiche l'URL brute à la place du title manquant, ou un extrait incohérent tiré du body.\u003C/p>\n\u003Ch2>Le bug : comment +layout.ts perd la bataille contre +page.svelte\u003C/h2>\n\u003Cp>Pour comprendre ce qui s'est passé, il faut remonter à l'architecture initiale du projet.\u003C/p>\n\u003Ch3>La stratégie de titles centralisée\u003C/h3>\n\u003Cp>L'équipe avait choisi de gérer tous les titles dans \u003Ccode>+layout.ts\u003C/code> à la racine, via la fonction \u003Ccode>load\u003C/code> :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// src/routes/+layout.ts\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> const\u003C/span>\u003Cspan style=\"color:#B392F0\"> load\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> async\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ({ \u003C/span>\u003Cspan style=\"color:#FFAB70\">url\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\"> titles\u003C/span>\u003Cspan style=\"color:#F97583\">:\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\">string\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:#9ECBFF\">    '/'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Acme — Plateforme de gestion d\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\'\u003C/span>\u003Cspan style=\"color:#9ECBFF\">équipe'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    '/produit'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Produit — Fonctionnalités Acme'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    '/tarifs'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Tarifs — Acme'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    '/blog'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Blog — Acme'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    '/a-propos'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'À propos — Acme'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // ... 7 autres hubs\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\">  return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    title: titles[url.pathname] \u003C/span>\u003Cspan style=\"color:#F97583\">??\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'Acme — Gestion d\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\'\u003C/span>\u003Cspan style=\"color:#9ECBFF\">équipe'\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>Et dans le composant layout correspondant :\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;!-- src/routes/+layout.svelte -->\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\">\u003Cspan style=\"color:#F97583\">  export\u003C/span>\u003Cspan style=\"color:#F97583\"> let\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> data;\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:#F97583\">svelte\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\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\">>{data.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\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"Acme simplifie la gestion d'équipe.\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#F97583\">svelte\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003Cspan style=\"color:#85E89D\">head\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:#F97583\">slot\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>À ce stade, tout fonctionne. Le layout définit le \u003Ccode>&#x3C;title>\u003C/code>, les pages enfants héritent. Dans le navigateur, le SSR produit le bon HTML. Googlebot voit le bon title.\u003C/p>\n\u003Ch3>L'arrivée du problème : le +page.svelte \"vide\"\u003C/h3>\n\u003Cp>Pendant la refonte, un développeur crée un \u003Ccode>+page.svelte\u003C/code> pour la home afin d'y ajouter un composant hero. Le fichier est 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\">&#x3C;!-- src/routes/+page.svelte -->\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\">\u003Cspan style=\"color:#F97583\">  export\u003C/span>\u003Cspan style=\"color:#F97583\"> let\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> data;\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:#79B8FF\">Hero\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">Features\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\">Testimonials\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Pas de \u003Ccode>&#x3C;svelte:head>\u003C/code>. Pas de title. Le développeur considère que le layout s'en charge — ce qui est vrai dans la plupart des frameworks. Mais SvelteKit a un comportement spécifique documenté dans une \u003Ca href=\"https://kit.svelte.dev/docs/seo#manual-setup\">section discrète de la doc officielle\u003C/a> : \u003Cstrong>quand un \u003Ccode>+page.svelte\u003C/code> existe pour une route, SvelteKit considère que cette page \"possède\" son propre \u003Ccode>&#x3C;head>\u003C/code>\u003C/strong>.\u003C/p>\n\u003Ch3>Le mécanisme de résolution du \u003Ccode>&#x3C;svelte:head>\u003C/code>\u003C/h3>\n\u003Cp>Voici ce que SvelteKit fait réellement lors du rendu SSR :\u003C/p>\n\u003Col>\n\u003Cli>Le layout rend son \u003Ccode>&#x3C;svelte:head>\u003C/code>, qui injecte \u003Ccode>&#x3C;title>Acme — Plateforme de gestion d'équipe&#x3C;/title>\u003C/code>.\u003C/li>\n\u003Cli>Le \u003Ccode>+page.svelte\u003C/code> est rendu à l'intérieur du \u003Ccode>&#x3C;slot />\u003C/code>.\u003C/li>\n\u003Cli>SvelteKit détecte que la page enfant a été rendue. Même sans \u003Ccode>&#x3C;svelte:head>\u003C/code> explicite, le framework applique un mécanisme de \"reset\" des balises head gérées par le parent \u003Cstrong>si la page enfant est considérée comme active\u003C/strong>.\u003C/li>\n\u003C/ol>\n\u003Cp>Le résultat en SSR — le HTML brut envoyé au client (et à Googlebot) :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;!\u003C/span>\u003Cspan style=\"color:#85E89D\">DOCTYPE\u003C/span>\u003Cspan style=\"color:#B392F0\"> html\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">html\u003C/span>\u003Cspan style=\"color:#B392F0\"> lang\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"fr\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">head\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">meta\u003C/span>\u003Cspan style=\"color:#B392F0\"> charset\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"utf-8\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">title\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">title\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">meta\u003C/span>\u003Cspan style=\"color:#B392F0\"> name\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"description\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> content\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"Acme simplifie la gestion d'équipe.\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">head\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">body\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  &#x3C;!-- contenu du hero, features, etc. -->\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;title>\u003C/code> est vide. La meta description, elle, survit — parce qu'elle est définie dans le layout et n'entre pas en collision avec un composant enfant.\u003C/p>\n\u003Cp>Le piège est subtil : \u003Cstrong>côté client, après hydratation, le title s'affiche correctement dans l'onglet du navigateur\u003C/strong>. Le JavaScript du layout s'exécute, met à jour le DOM, et le title apparaît. Mais Googlebot, même s'il exécute du JavaScript, reçoit d'abord le HTML SSR. Et dans de nombreux cas de pages légères comme celle-ci, le renderer de Google s'appuie sur le HTML initial sans ré-exécuter le cycle complet de hydratation côté client.\u003C/p>\n\u003Ch3>La preuve par curl\u003C/h3>\n\u003Cp>Une simple commande suffit à reproduire le problème :\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://acme.example.com/\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -oP\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '&#x3C;title>.*?&#x3C;/title>'\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Résultat :\u003C/p>\n\u003Cpre>\u003Ccode>&#x3C;title>&#x3C;/title>\n\u003C/code>\u003C/pre>\n\u003Cp>La même commande sur une page avec un \u003Ccode>&#x3C;svelte:head>\u003C/code> explicite, par exemple \u003Ccode>/blog/premiers-pas\u003C/code> :\u003C/p>\n\u003Cpre>\u003Ccode>&#x3C;title>Premiers pas avec Acme — Blog&#x3C;/title>\n\u003C/code>\u003C/pre>\n\u003Ch3>Pourquoi les tests n'ont rien vu\u003C/h3>\n\u003Cp>Trois raisons convergentes :\u003C/p>\n\u003Cp>\u003Cstrong>1. Les tests E2E vérifient le DOM après hydratation.\u003C/strong> L'équipe utilise Playwright. Leurs assertions portent sur \u003Ccode>page.title()\u003C/code>, qui renvoie le title visible dans l'onglet — donc le title post-hydratation, correct. Aucun test ne vérifie le HTML brut de la réponse SSR.\u003C/p>\n\u003Cp>\u003Cstrong>2. Lighthouse et les audits navigateur voient le rendu final.\u003C/strong> Lighthouse exécute la page dans un Chromium complet. Le JavaScript tourne, le title est injecté côté client. Score SEO : 100.\u003C/p>\n\u003Cp>\u003Cstrong>3. Le preview Vercel/Fly.io est identique.\u003C/strong> L'environnement de staging sert le même comportement. Le QA visuel ne détecte rien parce que l'onglet du navigateur affiche le bon title.\u003C/p>\n\u003Cp>Le problème n'est visible que dans deux contextes : le HTML brut (curl, \u003Ccode>View Source\u003C/code>) et le rendu Googlebot (Search Console > Inspection d'URL). L'équipe ne vérifie ni l'un ni l'autre avant de déployer.\u003C/p>\n\u003Cp>Ce pattern — une divergence entre le rendu navigateur et le rendu SSR/HTTP — est un classique des frameworks modernes. On l'a documenté dans des contextes similaires avec \u003Ca href=\"/blog/nuxt-useseometa-le-child-override-silencieusement-les-meta-du-layout-parent\">Nuxt et le useSeometa écrasé par un composant enfant\u003C/a> et avec \u003Ca href=\"/blog/next-js-metadata-async-qui-throw-la-page-sert-le-fallback-default-next-js\">Next.js et les metadata async qui servent un fallback vide\u003C/a>.\u003C/p>\n\u003Ch2>Le fix : trois niveaux de correction\u003C/h2>\n\u003Ch3>Niveau 1 — Le patch immédiat\u003C/h3>\n\u003Cp>Ajouter un \u003Ccode>&#x3C;svelte:head>\u003C/code> explicite dans chaque \u003Ccode>+page.svelte\u003C/code> qui n'en a pas :\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;!-- src/routes/+page.svelte (corrigé) -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">script\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  export\u003C/span>\u003Cspan style=\"color:#F97583\"> let\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> data;\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:#F97583\">svelte\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\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\">>{data.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:#F97583\">svelte\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003Cspan style=\"color:#85E89D\">head\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:#79B8FF\">Hero\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">Features\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\">Testimonials\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le \u003Ccode>data.title\u003C/code> provient toujours du \u003Ccode>+layout.ts\u003C/code> parent — il est accessible dans \u003Ccode>data\u003C/code> car SvelteKit merge les données du layout et de la page. Le \u003Ccode>&#x3C;svelte:head>\u003C/code> de la page prend explicitement le relais.\u003C/p>\n\u003Cp>L'équipe patche les 12 fichiers concernés en 20 minutes. Commit, déploiement.\u003C/p>\n\u003Ch3>Niveau 2 — La vérification post-déploiement\u003C/h3>\n\u003Cp>Après déploiement, validation immédiate :\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 les 12 URLs patchées\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> url \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"/\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"/produit\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"/tarifs\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"/blog\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"/a-propos\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"/contact\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  \"/cas-clients\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"/integrations\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"/securite\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"/api\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"/changelog\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"/presse\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; \u003C/span>\u003Cspan style=\"color:#F97583\">do\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  title\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$(\u003C/span>\u003Cspan style=\"color:#B392F0\">curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -s\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"https://acme.example.com${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">url\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}\"\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -oP\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '(?&#x3C;=&#x3C;title>).*?(?=&#x3C;/title>)'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\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\">url\u003C/span>\u003Cspan style=\"color:#9ECBFF\">} → ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">title\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">done\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Résultat attendu :\u003C/p>\n\u003Cpre>\u003Ccode>/ → Acme — Plateforme de gestion d'équipe\n/produit → Produit — Fonctionnalités Acme\n/tarifs → Tarifs — Acme\n...\n\u003C/code>\u003C/pre>\n\u003Cp>Ensuite, demande de ré-indexation dans Search Console pour chacune des 12 URLs. L'équipe utilise l'API Indexing pour accélérer — mais celle-ci est réservée aux pages \u003Ccode>JobPosting\u003C/code> et \u003Ccode>BroadcastEvent\u003C/code>. Pour les autres, la demande manuelle via l'interface reste la seule option.\u003C/p>\n\u003Ch3>Niveau 3 — La protection contre la récurrence\u003C/h3>\n\u003Cp>L'équipe met en place trois garde-fous :\u003C/p>\n\u003Cp>\u003Cstrong>1. Un test Playwright qui vérifie le HTML SSR brut :\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/title-ssr.spec.ts\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { test, expect } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '@playwright/test'\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\"> criticalPages\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  { url: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, expected: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Acme — Plateforme de gestion d\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\'\u003C/span>\u003Cspan style=\"color:#9ECBFF\">équipe'\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  { url: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/produit'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, expected: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Produit — Fonctionnalités Acme'\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  { url: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/tarifs'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, expected: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Tarifs — Acme'\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">];\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> page\u003C/span>\u003Cspan style=\"color:#F97583\"> of\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> criticalPages) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  test\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`SSR title for ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">page\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">url\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#F97583\">async\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ({ \u003C/span>\u003Cspan style=\"color:#FFAB70\">request\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\"> response\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> request.\u003C/span>\u003Cspan style=\"color:#B392F0\">get\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(page.url);\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:#E1E4E8\"> response.\u003C/span>\u003Cspan style=\"color:#B392F0\">text\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\"> match\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> html.\u003C/span>\u003Cspan style=\"color:#B392F0\">match\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\u003C/span>\u003Cspan style=\"color:#DBEDFF\">&#x3C;title>(\u003C/span>\u003Cspan style=\"color:#79B8FF\">.\u003C/span>\u003Cspan style=\"color:#F97583\">*?\u003C/span>\u003Cspan style=\"color:#DBEDFF\">)&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D;font-weight:bold\">\\/\u003C/span>\u003Cspan style=\"color:#DBEDFF\">title>\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\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\">(match).not.\u003C/span>\u003Cspan style=\"color:#B392F0\">toBeNull\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(match\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">[\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]).\u003C/span>\u003Cspan style=\"color:#B392F0\">toBe\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(page.expected);\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>Ce test utilise \u003Ccode>request.get\u003C/code> — pas de navigateur, pas d'hydratation. Il vérifie le HTML brut, exactement comme curl ou Googlebot en première passe.\u003C/p>\n\u003Cp>\u003Cstrong>2. Un hook SvelteKit qui logge un warning en dev si le title SSR est vide :\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\">// src/hooks.server.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\"> { Handle } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '@sveltejs/kit'\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\"> const\u003C/span>\u003Cspan style=\"color:#B392F0\"> handle\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> Handle\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> async\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ({ \u003C/span>\u003Cspan style=\"color:#FFAB70\">event\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">resolve\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\"> response\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> resolve\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(event);\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\"> (event.url.pathname \u003C/span>\u003Cspan style=\"color:#F97583\">!==\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '/__data.json'\u003C/span>\u003Cspan style=\"color:#F97583\"> &#x26;&#x26;\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> response.headers.\u003C/span>\u003Cspan style=\"color:#B392F0\">get\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'content-type'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)?.\u003C/span>\u003Cspan style=\"color:#B392F0\">includes\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'text/html'\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\"> body\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> response.\u003C/span>\u003Cspan style=\"color:#B392F0\">clone\u003C/span>\u003Cspan style=\"color:#E1E4E8\">().\u003C/span>\u003Cspan style=\"color:#B392F0\">text\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\"> titleMatch\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> body.\u003C/span>\u003Cspan style=\"color:#B392F0\">match\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\u003C/span>\u003Cspan style=\"color:#DBEDFF\">&#x3C;title>(\u003C/span>\u003Cspan style=\"color:#79B8FF\">.\u003C/span>\u003Cspan style=\"color:#F97583\">*?\u003C/span>\u003Cspan style=\"color:#DBEDFF\">)&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D;font-weight:bold\">\\/\u003C/span>\u003Cspan style=\"color:#DBEDFF\">title>\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:#F97583\">    if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">titleMatch \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> titleMatch[\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].\u003C/span>\u003Cspan style=\"color:#B392F0\">trim\u003C/span>\u003Cspan style=\"color:#E1E4E8\">() \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\">      console.\u003C/span>\u003Cspan style=\"color:#B392F0\">warn\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`[SEO] Empty &#x3C;title> detected on ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">event\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">url\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">pathname\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> response;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">};\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>En développement, ce warning apparaît dans la console du terminal. Impossible de le rater.\u003C/p>\n\u003Cp>\u003Cstrong>3. Un crawl Screaming Frog hebdomadaire en mode JavaScript rendering\u003C/strong>, avec une alerte custom sur les pages dont le title SSR diffère du title JS. L'export CSV est comparé via un script CI qui échoue si une divergence apparaît.\u003C/p>\n\u003Ch3>Temps de récupération\u003C/h3>\n\u003Cp>Le déploiement du fix a lieu un mardi à 11h. Voici la timeline de récupération observée dans Search Console :\u003C/p>\n\u003Cul>\n\u003Cli>\u003Cstrong>J+2\u003C/strong> : Googlebot recrawle la home. Le nouveau title apparaît dans l'inspection d'URL.\u003C/li>\n\u003Cli>\u003Cstrong>J+5\u003C/strong> : Le snippet de la home dans les SERPs affiche de nouveau le vrai title.\u003C/li>\n\u003Cli>\u003Cstrong>J+8\u003C/strong> : Les impressions de la home remontent à 80 % du niveau pré-incident.\u003C/li>\n\u003Cli>\u003Cstrong>J+14\u003C/strong> : Le CTR de la home revient à 11,6 % (vs 12 % avant l'incident). Quasi-retour à la normale.\u003C/li>\n\u003Cli>\u003Cstrong>J+23\u003C/strong> : Les 12 pages affectées ont toutes retrouvé leur niveau de trafic antérieur, à ±5 %.\u003C/li>\n\u003C/ul>\n\u003Cp>Total des pertes estimées sur la période de 23 jours d'incident + 14 jours de récupération : environ 8 400 clics organiques. Pour un site B2B SaaS dont le coût d'acquisition payant est à 4,20 € le clic, l'équivalent en budget ads perdu dépasse 35 000 €.\u003C/p>\n\u003Cp>Le parallèle est direct avec d'autres incidents de refonte où un élément SEO critique disparaît sans que personne ne le voie — comme un \u003Ca href=\"/blog/refonte-header-le-h1-remplace-par-un-div-title-par-le-design-system\">H1 remplacé par un div lors d'une refonte header\u003C/a> ou un \u003Ca href=\"/blog/design-system-composant-heading-qui-rend-div-selon-la-prop-as-mal-configuree\">composant heading du design system qui rend un div au lieu d'un Hn\u003C/a>.\u003C/p>\n\u003Ch2>Ce qu'on en retient\u003C/h2>\n\u003Cp>Le modèle mental \"le layout gère le title, les pages héritent\" ne tient pas dans SvelteKit. Dès qu'un \u003Ccode>+page.svelte\u003C/code> existe, il doit déclarer son propre \u003Ccode>&#x3C;svelte:head>\u003C/code> — même si le contenu vient du layout. C'est un contrat implicite du framework que ni la documentation ni les outils de dev ne rendent évident.\u003C/p>\n\u003Cp>Le vrai problème n'est pas le bug. C'est le délai de détection : 18 jours. Un test SSR en CI aurait détecté la régression en 30 secondes. Un monitoring continu type Seogard aurait alerté sur la divergence SSR/navigateur dans les minutes suivant le déploiement — pas trois semaines après.\u003C/p>\n\u003Cp>Chaque framework a ses pièges de résolution \u003Ccode>&#x3C;head>\u003C/code>. Les connaître ne suffit pas. Il faut les tester comme du code métier.\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,12,[18,19,20,21],"sveltekit","layout","title","svelte","SvelteKit : title vide en prod, 0 clic sur 3 semaines","Wed Jun 10 2026 16:01:40 GMT+0000 (Coordinated Universal Time)",[25,39,54,67,79,93],{"_id":26,"slug":27,"__v":6,"author":7,"canonical":28,"category":10,"createdAt":29,"date":30,"description":31,"image":15,"imageAlt":15,"readingTime":16,"tags":32,"title":37,"updatedAt":38},"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.",[33,34,35,20,36],"tanstack router","react","ssr","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":40,"slug":41,"__v":6,"author":7,"canonical":42,"category":10,"createdAt":43,"date":44,"description":45,"image":15,"imageAlt":15,"readingTime":46,"tags":47,"title":52,"updatedAt":53},"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,[48,49,50,51],"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":55,"slug":56,"__v":6,"author":7,"canonical":57,"category":10,"createdAt":58,"date":59,"description":60,"image":15,"imageAlt":15,"readingTime":16,"tags":61,"title":65,"updatedAt":66},"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.",[62,63,64,35],"remix","meta","streaming","Remix meta() async : metas vides en streaming SSR","Thu Jun 11 2026 16:01:48 GMT+0000 (Coordinated Universal Time)",{"_id":68,"slug":69,"__v":6,"author":7,"canonical":70,"category":10,"createdAt":71,"date":12,"description":72,"image":15,"imageAlt":15,"readingTime":16,"tags":73,"title":77,"updatedAt":78},"6a28fdbcaa6b273b0cc7b544","nuxt-useseometa-le-child-override-silencieusement-les-meta-du-layout-parent","https://seogard.io/blog/nuxt-useseometa-le-child-override-silencieusement-les-meta-du-layout-parent","2026-06-10T06:01:32.891Z","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.",[74,75,19,76,63],"nuxt","useSeoMeta","override","Nuxt useSeoMeta : le child override les meta du layout","Wed Jun 10 2026 06:01:32 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":46,"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.",[48,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},"JavaScript SEO","javascript-seo",5,{"category":145,"slug":146,"count":143},"Monitoring","monitoring",{"category":148,"slug":149,"count":143},"Structured Data","structured-data",{"category":151,"slug":152,"count":153},"Refonte","refonte",4,{"category":155,"slug":156,"count":153},"Redirections","redirections",{"category":158,"slug":159,"count":153},"Outils","outils",{"category":161,"slug":162,"count":153},"E-commerce","e-commerce",{"category":164,"slug":165,"count":153},"Avancé","avance",{"category":167,"slug":168,"count":169},"Contenu","contenu",3,{"category":171,"slug":172,"count":169},"IA & SEO","ia-seo"]