[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fwN9QKJOdA1O-8YIOi2VhCIu6d4RH6n0HhLEkL9Qa8J0":3,"$flA6fNPht6ehJ09z_Xh0LbGzlcagiJn2v2wUAW1wl3wo":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},"6a2ba0d0aa6b273b0cf5b507","astro-view-transitions-changement-de-route-ne-re-trigge-pas-le-head-update",0,"Equipe Seogard","# Astro View Transitions : quand les meta head restent figées après chaque navigation\n\nMercredi 14h. Un développeur front active `\u003CViewTransitions />` sur un site éditorial Astro 4.x de 1 200 pages. Les animations sont fluides, le client valide. Le déploiement part sur Vercel à 15h12. Personne ne regarde le `\u003Chead>` des pages internes. Dix-huit jours plus tard, Search Console affiche −38 000 clics sur les requêtes de longue traîne. Toutes les pages profondes portent le `\u003Ctitle>` et la `\u003Cmeta description>` de la homepage.\n\n## T+18 jours — Lundi 9h03 : le graphique qui décroche\n\nLe responsable SEO ouvre Search Console pour le reporting mensuel. Le graphique de performances montre un décrochage net, pile le 14 du mois précédent. −38 400 clics sur 18 jours. Les impressions tiennent — les positions, non.\n\nIl filtre par page. La homepage est stable. Les catégories aussi. Mais les 847 pages articles, fiches auteur et pages tag affichent toutes le même titre dans le rapport « Apparence dans les résultats » : **\"Accueil — Le Mag Outdoor\"**.\n\nPremier réflexe : vérifier la Search Console pour un éventuel problème d'indexation. Aucune erreur 4xx, aucun `noindex` détecté. Les pages sont indexées, crawlées, servies en 200.\n\nIl ouvre un article dans Chrome. Inspecte le `\u003Chead>`. Le `\u003Ctitle>` est correct : « Les 10 meilleurs sentiers de randonnée en Chartreuse — Le Mag Outdoor ». Il rafraîchit — toujours bon. Il partage l'URL dans Slack : « Je ne vois rien d'anormal côté head. »\n\nLe développeur front jette un œil. Il reproduit le parcours utilisateur normal : il arrive sur la homepage, clique sur un lien catégorie, puis sur un article. Il ouvre les DevTools et regarde l'onglet Elements. Le `\u003Ctitle>` dans le DOM indique : **\"Accueil — Le Mag Outdoor\"**. Il a navigué vers une page article, l'animation de transition s'est jouée, le contenu a changé — mais le `\u003Chead>` n'a pas bougé.\n\nRetour dans Search Console. Outil d'inspection d'URL sur une fiche article. Google montre le HTML rendu : le `\u003Ctitle>` est correct. La page servie en SSR est bonne. Mais le développeur réalise que si Googlebot suit un lien interne via le mécanisme SPA des View Transitions — ce qu'il fait de plus en plus — il voit potentiellement le `\u003Chead>` de la page précédente.\n\nL'hypothèse Googlebot-MPA est vite écartée : les logs serveur montrent que Googlebot a bien crawlé chaque URL individuellement (requêtes HTTP distinctes). Le SSR renvoie les bonnes meta. Le problème n'est pas côté Googlebot en crawl initial.\n\nC'est côté **rendu JavaScript post-navigation** que ça casse. Et côté utilisateurs qui partagent des URLs après navigation SPA — les previews OpenGraph affichent le titre de la homepage. Les équipes social media confirment : les partages sur LinkedIn et Slack depuis deux semaines affichent tous « Accueil — Le Mag Outdoor ».\n\nLe périmètre s'étend : ce n'est pas seulement un problème SEO. C'est un problème de métadonnées globales sur toute navigation client-side.\n\n## Le bug : View Transitions intercepte la navigation sans propager le head\n\n### Comment fonctionnent les View Transitions dans Astro\n\nAstro, par défaut, fonctionne en MPA (Multi-Page Application). Chaque clic sur un lien déclenche une navigation complète : requête HTTP, nouveau document HTML, nouveau `\u003Chead>`. Les balises `\u003Ctitle>`, `\u003Cmeta>`, `\u003Clink rel=\"canonical\">` sont servies par le SSR à chaque page load.\n\nQuand on ajoute `\u003CViewTransitions />` dans le layout, Astro bascule en mode SPA-like. Le composant intercepte les clics sur les liens internes, fetch le HTML de la page cible via `fetch()`, et swap le contenu du `\u003Cbody>` avec une animation CSS. Le résultat : des transitions fluides, sans full page reload.\n\nVoici le layout type qui active le mécanisme :\n\n```astro\n---\n// src/layouts/BaseLayout.astro\nimport { ViewTransitions } from 'astro:transitions';\n\ninterface Props {\n  title: string;\n  description: string;\n  canonical: string;\n}\n\nconst { title, description, canonical } = Astro.props;\n---\n\u003Chtml lang=\"fr\">\n  \u003Chead>\n    \u003Cmeta charset=\"utf-8\" />\n    \u003Cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    \u003Ctitle>{title}\u003C/title>\n    \u003Cmeta name=\"description\" content={description} />\n    \u003Clink rel=\"canonical\" href={canonical} />\n    \u003Cmeta property=\"og:title\" content={title} />\n    \u003Cmeta property=\"og:description\" content={description} />\n    \u003CViewTransitions />\n  \u003C/head>\n  \u003Cbody>\n    \u003Cslot />\n  \u003C/body>\n\u003C/html>\n```\n\nEt une page article typique :\n\n```astro\n---\n// src/pages/articles/[slug].astro\nimport BaseLayout from '../../layouts/BaseLayout.astro';\nimport { getEntry } from 'astro:content';\n\nconst { slug } = Astro.params;\nconst article = await getEntry('articles', slug);\nconst { title, description } = article.data;\nconst canonical = `https://lemag-outdoor.fr/articles/${slug}`;\n---\n\u003CBaseLayout title={title} description={description} canonical={canonical}>\n  \u003Carticle>\n    \u003Ch1>{title}\u003C/h1>\n    \u003Cp>{article.body}\u003C/p>\n  \u003C/article>\n\u003C/BaseLayout>\n```\n\nEn SSR pur (navigation directe vers l'URL), tout fonctionne. Astro render le template complet, `\u003Chead>` inclus. Le HTML reçu par le navigateur (ou par Googlebot) contient les bonnes meta.\n\n### Ce qui casse en navigation client-side\n\nLe problème survient quand Astro effectue le swap SPA. Dans les versions Astro 4.0 à 4.8, le comportement par défaut de `\u003CViewTransitions />` est le suivant :\n\n1. L'utilisateur clique sur un lien interne.\n2. Le composant intercepte le clic, empêche la navigation native.\n3. Il `fetch()` le HTML de la page cible.\n4. Il extrait le contenu du `\u003Cbody>` de la réponse.\n5. Il anime la transition et remplace le `\u003Cbody>` du document courant.\n\nL'étape clé manquante : **le `\u003Chead>` du document courant n'est pas systématiquement mis à jour**. Astro est censé fusionner le `\u003Chead>` de la page cible avec le `\u003Chead>` courant. Mais dans certaines configurations — notamment quand des scripts tiers ou des composants client injectent des éléments dans le head au runtime — le swap du head échoue silencieusement.\n\nLe résultat visible dans le DOM après navigation SPA :\n\n```html\n\u003C!-- L'utilisateur est sur /articles/sentiers-chartreuse -->\n\u003C!-- Mais le \u003Chead> affiche toujours : -->\n\u003Chead>\n  \u003Ctitle>Accueil — Le Mag Outdoor\u003C/title>\n  \u003Cmeta name=\"description\" content=\"Le magazine outdoor pour les passionnés de randonnée et d'alpinisme.\" />\n  \u003Clink rel=\"canonical\" href=\"https://lemag-outdoor.fr/\" />\n  \u003Cmeta property=\"og:title\" content=\"Accueil — Le Mag Outdoor\" />\n  \u003C!-- ... -->\n\u003C/head>\n```\n\n### Pourquoi le bug est passé inaperçu\n\nL'équipe utilise un composant `\u003CHeadSEO />` custom qui injecte un script inline pour mettre à jour `document.title` côté client. Ce script fonctionne sur le premier page load, mais ne se ré-exécute pas après un swap View Transitions.\n\n```astro\n---\n// src/components/HeadSEO.astro\nconst { title } = Astro.props;\n---\n\u003Cscript define:vars={{ title }}>\n  document.title = title;\n\u003C/script>\n```\n\nCe script s'exécute une seule fois, au chargement initial. Après une navigation View Transitions, le nouveau body est injecté, mais les scripts `define:vars` du head ne sont pas ré-exécutés. Le `document.title` reste sur la valeur de la page d'entrée.\n\n### La divergence développeur vs Googlebot\n\nLe développeur teste toujours en accès direct (il tape l'URL, il recharge la page). Il voit le bon `\u003Ctitle>`.\n\nL'utilisateur réel arrive sur la homepage, navigue en mode SPA, et voit le mauvais `\u003Ctitle>` sur les pages internes.\n\nGooglebot, dans son crawl primaire, accède à chaque URL individuellement — il voit les bonnes meta. Mais quand il rend la page avec son moteur Chrome headless et simule des interactions (ce qu'il fait de plus en plus, [selon la documentation Google](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics)), il peut observer le DOM post-navigation et constater la divergence.\n\nLe diagnostic est confirmé avec une commande `curl` comparée au DOM live :\n\n```bash\n# SSR direct — OK\ncurl -s https://lemag-outdoor.fr/articles/sentiers-chartreuse \\\n  | grep -E '\u003Ctitle>|\u003Cmeta name=\"description\"|\u003Clink rel=\"canonical\"'\n\n# Résultat :\n# \u003Ctitle>Les 10 meilleurs sentiers de randonnée en Chartreuse — Le Mag Outdoor\u003C/title>\n# \u003Cmeta name=\"description\" content=\"Découvrez notre sélection des plus beaux sentiers...\" />\n# \u003Clink rel=\"canonical\" href=\"https://lemag-outdoor.fr/articles/sentiers-chartreuse\" />\n```\n\nPuis dans Chrome DevTools, après navigation SPA depuis la homepage :\n\n```javascript\n// Console DevTools après navigation View Transitions\nconsole.log(document.title);\n// \"Accueil — Le Mag Outdoor\"\n\nconsole.log(document.querySelector('meta[name=\"description\"]')?.content);\n// \"Le magazine outdoor pour les passionnés de randonnée et d'alpinisme.\"\n\nconsole.log(document.querySelector('link[rel=\"canonical\"]')?.href);\n// \"https://lemag-outdoor.fr/\"\n```\n\nLa preuve est là. Le DOM live ment. Le serveur dit vrai, mais le client ment — et tout service qui parse le DOM post-navigation (previews sociales, extensions SEO, outils de monitoring client-side) voit les meta de la page d'entrée.\n\nUn audit Screaming Frog en mode « JavaScript rendering » sur les 1 200 URLs confirme : en crawl direct, 100% des meta sont correctes. Mais un crawl en mode « Suivre les liens internes avec JS activé » montre que 73% des pages visitées via navigation interne héritent du `\u003Chead>` de la page d'entrée du crawl.\n\n### Le cas aggravant : les canonicals\n\nLe plus dangereux n'est pas le `\u003Ctitle>`. C'est le `\u003Clink rel=\"canonical\">`. Si Google visite une page article et voit un canonical pointant vers la homepage, il peut décider que la page article est un doublon de la homepage. Ce scénario est similaire aux [problèmes de canonicals mal propagés après migration](/blog/migration-prestashop-vers-bigcommerce-canonicals-pointent-encore-vers-le-staging) — sauf qu'ici, il n'y a pas de migration. Juste un composant de transition qui casse la chaîne de meta.\n\n## Le fix : forcer le head swap et écouter les lifecycle events\n\n### Étape 1 — Supprimer le composant HeadSEO custom\n\nLe script `define:vars` qui force `document.title` est un workaround qui masque le vrai problème. Il est supprimé.\n\n### Étape 2 — S'assurer que le head est swappé par View Transitions\n\nAstro expose des événements de lifecycle pour les View Transitions. L'événement `astro:after-swap` se déclenche après que le nouveau contenu a été injecté dans le DOM. C'est le moment de vérifier — et si nécessaire forcer — la mise à jour du head.\n\nLe correctif dans le layout :\n\n```astro\n---\n// src/layouts/BaseLayout.astro — version corrigée\nimport { ViewTransitions } from 'astro:transitions';\n\ninterface Props {\n  title: string;\n  description: string;\n  canonical: string;\n}\n\nconst { title, description, canonical } = Astro.props;\n---\n\u003Chtml lang=\"fr\">\n  \u003Chead>\n    \u003Cmeta charset=\"utf-8\" />\n    \u003Cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    \u003Ctitle>{title}\u003C/title>\n    \u003Cmeta name=\"description\" content={description} />\n    \u003Clink rel=\"canonical\" href={canonical} />\n    \u003Cmeta property=\"og:title\" content={title} />\n    \u003Cmeta property=\"og:description\" content={description} />\n    \u003CViewTransitions />\n  \u003C/head>\n  \u003Cbody>\n    \u003Cslot />\n    \u003Cscript>\n      document.addEventListener('astro:after-swap', () => {\n        // Astro swap le head par défaut, mais certains éléments\n        // peuvent être ignorés si un script tiers les a modifiés.\n        // Ce listener force la synchronisation du title avec le DOM swappé.\n        const newTitle = document.querySelector('title');\n        if (newTitle) {\n          document.title = newTitle.textContent || '';\n        }\n      });\n    \u003C/script>\n  \u003C/body>\n\u003C/html>\n```\n\n### Étape 3 — Utiliser `transition:persist` avec précaution\n\nL'équipe avait ajouté `transition:persist` sur le header et le footer pour éviter de les recharger. Ce directive indique à Astro de conserver l'élément entre les navigations. Problème : si un composant dans le header lit des meta du `\u003Chead>` au mount et les cache, il affiche des données obsolètes.\n\nLa règle : ne jamais persister un composant qui dépend du `\u003Chead>` de la page courante.\n\n### Étape 4 — Upgrade vers Astro 4.9+\n\nÀ partir d'Astro 4.9, le mécanisme de head swap a été renforcé. Le framework fait un diff plus agressif entre le `\u003Chead>` de la page courante et celui de la page cible, et met à jour chaque élément individuellement. La [documentation Astro sur les View Transitions](https://docs.astro.build/en/guides/view-transitions/) détaille ce comportement.\n\nLa mise à jour :\n\n```bash\n# Upgrade Astro\nnpm install astro@latest\n\n# Vérifier la version\nnpx astro --version\n# astro v4.12.2\n\n# Rebuild et test\nnpm run build\nnpm run preview\n```\n\n### Étape 5 — Ajouter un test automatisé\n\nL'équipe ajoute un test Playwright qui simule une navigation SPA et vérifie le `\u003Chead>` :\n\n```typescript\n// tests/view-transitions-head.spec.ts\nimport { test, expect } from '@playwright/test';\n\ntest('View Transitions met à jour le head sur navigation interne', async ({ page }) => {\n  // Arriver sur la homepage\n  await page.goto('https://lemag-outdoor.fr/');\n  const homeTitle = await page.title();\n  expect(homeTitle).toContain('Accueil');\n\n  // Naviguer vers un article via clic (déclenche View Transitions)\n  await page.click('a[href=\"/articles/sentiers-chartreuse\"]');\n  await page.waitForURL('**/articles/sentiers-chartreuse');\n\n  // Vérifier que le title a changé\n  const articleTitle = await page.title();\n  expect(articleTitle).not.toContain('Accueil');\n  expect(articleTitle).toContain('Chartreuse');\n\n  // Vérifier la meta description\n  const description = await page.getAttribute('meta[name=\"description\"]', 'content');\n  expect(description).not.toContain('magazine outdoor');\n\n  // Vérifier le canonical\n  const canonical = await page.getAttribute('link[rel=\"canonical\"]', 'href');\n  expect(canonical).toBe('https://lemag-outdoor.fr/articles/sentiers-chartreuse');\n});\n```\n\n### Invalidation du cache et redéploiement\n\nLe fix est déployé un mercredi à 11h. Le cache Vercel est purgé manuellement via le dashboard. Les pages les plus touchées sont soumises à la réindexation via l'outil d'inspection d'URL de Search Console — par lots de 50 par jour, en commençant par les pages avec le plus de trafic historique.\n\n### Temps de récupération\n\n- **J+3** : Google recrawle 60% des pages soumises. Les titres dans les SERPs commencent à revenir à la normale.\n- **J+7** : 90% des pages affichent le bon titre dans Search Console.\n- **J+14** : Les clics remontent à 85% du niveau pré-incident.\n- **J+21** : Retour au niveau de trafic normal. Certaines pages longue traîne mettent 4 semaines supplémentaires à retrouver leur position exacte.\n\nCe schéma de récupération est cohérent avec d'autres incidents de meta corrompues. Le même type de timeline a été observé lors d'un [problème similaire sur les content collections Astro](/blog/astro-content-collections-frontmatter-title-non-passe-apres-refacto) où des titres frontmatter ne passaient plus après une refactorisation.\n\n## Ce qu'on en retient\n\nLe mode SPA des frameworks n'est pas une fonctionnalité SEO-neutre. Chaque mécanisme qui intercepte la navigation native du navigateur crée un risque de divergence entre le HTML servi et le DOM visible. Les View Transitions d'Astro, comme les transitions équivalentes dans [Next.js](/blog/next-js-metadata-async-qui-throw-la-page-sert-le-fallback-default-next-js), [Nuxt](/blog/nuxt-useseometa-le-child-override-silencieusement-les-meta-du-layout-parent) ou [SvelteKit](/blog/sveltekit-layout-ts-title-override-par-page-svelte-vide), ajoutent une couche d'abstraction sur la navigation — et cette couche doit être testée spécifiquement pour le `\u003Chead>`.\n\nLes tests manuels ne suffisent pas. Un développeur qui recharge la page verra toujours le bon titre. Il faut tester la navigation client-side, pas le rendu initial. Un monitoring continu type Seogard détecte cette divergence SSR/DOM en quelques minutes, parce qu'il compare systématiquement le HTML brut et le DOM rendu après hydratation — exactement le cas de figure que les tests manuels ratent.\n\nTrois règles à graver : tester le `\u003Chead>` après navigation SPA, pas seulement après page load. Ne jamais persister un composant qui lit les meta de la page. Et traiter le canonical comme la donnée la plus critique du `\u003Chead>` — un canonical incorrect fait plus de dégâts qu'un `\u003Ctitle>` mal formaté.\n```","https://seogard.io/blog/astro-view-transitions-changement-de-route-ne-re-trigge-pas-le-head-update","Framework","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.","\u003Ch1>Astro View Transitions : quand les meta head restent figées après chaque navigation\u003C/h1>\n\u003Cp>Mercredi 14h. Un développeur front active \u003Ccode>&#x3C;ViewTransitions />\u003C/code> sur un site éditorial Astro 4.x de 1 200 pages. Les animations sont fluides, le client valide. Le déploiement part sur Vercel à 15h12. Personne ne regarde le \u003Ccode>&#x3C;head>\u003C/code> des pages internes. Dix-huit jours plus tard, Search Console affiche −38 000 clics sur les requêtes de longue traîne. Toutes les pages profondes portent le \u003Ccode>&#x3C;title>\u003C/code> et la \u003Ccode>&#x3C;meta description>\u003C/code> de la homepage.\u003C/p>\n\u003Ch2>T+18 jours — Lundi 9h03 : le graphique qui décroche\u003C/h2>\n\u003Cp>Le responsable SEO ouvre Search Console pour le reporting mensuel. Le graphique de performances montre un décrochage net, pile le 14 du mois précédent. −38 400 clics sur 18 jours. Les impressions tiennent — les positions, non.\u003C/p>\n\u003Cp>Il filtre par page. La homepage est stable. Les catégories aussi. Mais les 847 pages articles, fiches auteur et pages tag affichent toutes le même titre dans le rapport « Apparence dans les résultats » : \u003Cstrong>\"Accueil — Le Mag Outdoor\"\u003C/strong>.\u003C/p>\n\u003Cp>Premier réflexe : vérifier la Search Console pour un éventuel problème d'indexation. Aucune erreur 4xx, aucun \u003Ccode>noindex\u003C/code> détecté. Les pages sont indexées, crawlées, servies en 200.\u003C/p>\n\u003Cp>Il ouvre un article dans Chrome. Inspecte le \u003Ccode>&#x3C;head>\u003C/code>. Le \u003Ccode>&#x3C;title>\u003C/code> est correct : « Les 10 meilleurs sentiers de randonnée en Chartreuse — Le Mag Outdoor ». Il rafraîchit — toujours bon. Il partage l'URL dans Slack : « Je ne vois rien d'anormal côté head. »\u003C/p>\n\u003Cp>Le développeur front jette un œil. Il reproduit le parcours utilisateur normal : il arrive sur la homepage, clique sur un lien catégorie, puis sur un article. Il ouvre les DevTools et regarde l'onglet Elements. Le \u003Ccode>&#x3C;title>\u003C/code> dans le DOM indique : \u003Cstrong>\"Accueil — Le Mag Outdoor\"\u003C/strong>. Il a navigué vers une page article, l'animation de transition s'est jouée, le contenu a changé — mais le \u003Ccode>&#x3C;head>\u003C/code> n'a pas bougé.\u003C/p>\n\u003Cp>Retour dans Search Console. Outil d'inspection d'URL sur une fiche article. Google montre le HTML rendu : le \u003Ccode>&#x3C;title>\u003C/code> est correct. La page servie en SSR est bonne. Mais le développeur réalise que si Googlebot suit un lien interne via le mécanisme SPA des View Transitions — ce qu'il fait de plus en plus — il voit potentiellement le \u003Ccode>&#x3C;head>\u003C/code> de la page précédente.\u003C/p>\n\u003Cp>L'hypothèse Googlebot-MPA est vite écartée : les logs serveur montrent que Googlebot a bien crawlé chaque URL individuellement (requêtes HTTP distinctes). Le SSR renvoie les bonnes meta. Le problème n'est pas côté Googlebot en crawl initial.\u003C/p>\n\u003Cp>C'est côté \u003Cstrong>rendu JavaScript post-navigation\u003C/strong> que ça casse. Et côté utilisateurs qui partagent des URLs après navigation SPA — les previews OpenGraph affichent le titre de la homepage. Les équipes social media confirment : les partages sur LinkedIn et Slack depuis deux semaines affichent tous « Accueil — Le Mag Outdoor ».\u003C/p>\n\u003Cp>Le périmètre s'étend : ce n'est pas seulement un problème SEO. C'est un problème de métadonnées globales sur toute navigation client-side.\u003C/p>\n\u003Ch2>Le bug : View Transitions intercepte la navigation sans propager le head\u003C/h2>\n\u003Ch3>Comment fonctionnent les View Transitions dans Astro\u003C/h3>\n\u003Cp>Astro, par défaut, fonctionne en MPA (Multi-Page Application). Chaque clic sur un lien déclenche une navigation complète : requête HTTP, nouveau document HTML, nouveau \u003Ccode>&#x3C;head>\u003C/code>. Les balises \u003Ccode>&#x3C;title>\u003C/code>, \u003Ccode>&#x3C;meta>\u003C/code>, \u003Ccode>&#x3C;link rel=\"canonical\">\u003C/code> sont servies par le SSR à chaque page load.\u003C/p>\n\u003Cp>Quand on ajoute \u003Ccode>&#x3C;ViewTransitions />\u003C/code> dans le layout, Astro bascule en mode SPA-like. Le composant intercepte les clics sur les liens internes, fetch le HTML de la page cible via \u003Ccode>fetch()\u003C/code>, et swap le contenu du \u003Ccode>&#x3C;body>\u003C/code> avec une animation CSS. Le résultat : des transitions fluides, sans full page reload.\u003C/p>\n\u003Cp>Voici le layout type qui active le mécanisme :\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/BaseLayout.astro\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { ViewTransitions } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'astro:transitions'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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\">  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\">  canonical\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\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#79B8FF\">title\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">description\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">canonical\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:#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\">meta\u003C/span>\u003Cspan style=\"color:#B392F0\"> name\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"viewport\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> content\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"width=device-width, initial-scale=1\"\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\">>{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\">={description} />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">link\u003C/span>\u003Cspan style=\"color:#B392F0\"> rel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"canonical\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> href\u003C/span>\u003Cspan style=\"color:#E1E4E8\">={canonical} />\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\">={title} />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">meta\u003C/span>\u003Cspan style=\"color:#B392F0\"> property\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"og:description\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> content\u003C/span>\u003Cspan style=\"color:#E1E4E8\">={description} />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">ViewTransitions\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">head\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">body\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">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>Et une page article typique :\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/articles/[slug].astro\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> BaseLayout \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '../../layouts/BaseLayout.astro'\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\"> { getEntry } \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:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#79B8FF\">slug\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> Astro.params;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> article\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> getEntry\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'articles'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, slug);\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\">title\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">description\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> article.data;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> canonical\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> `https://lemag-outdoor.fr/articles/${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">---\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">BaseLayout\u003C/span>\u003Cspan style=\"color:#B392F0\"> title\u003C/span>\u003Cspan style=\"color:#E1E4E8\">={title} \u003C/span>\u003Cspan style=\"color:#B392F0\">description\u003C/span>\u003Cspan style=\"color:#E1E4E8\">={description} \u003C/span>\u003Cspan style=\"color:#B392F0\">canonical\u003C/span>\u003Cspan style=\"color:#E1E4E8\">={canonical}>\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\">h1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>{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\">p\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>{article.body}&#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:#79B8FF\">BaseLayout\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>En SSR pur (navigation directe vers l'URL), tout fonctionne. Astro render le template complet, \u003Ccode>&#x3C;head>\u003C/code> inclus. Le HTML reçu par le navigateur (ou par Googlebot) contient les bonnes meta.\u003C/p>\n\u003Ch3>Ce qui casse en navigation client-side\u003C/h3>\n\u003Cp>Le problème survient quand Astro effectue le swap SPA. Dans les versions Astro 4.0 à 4.8, le comportement par défaut de \u003Ccode>&#x3C;ViewTransitions />\u003C/code> est le suivant :\u003C/p>\n\u003Col>\n\u003Cli>L'utilisateur clique sur un lien interne.\u003C/li>\n\u003Cli>Le composant intercepte le clic, empêche la navigation native.\u003C/li>\n\u003Cli>Il \u003Ccode>fetch()\u003C/code> le HTML de la page cible.\u003C/li>\n\u003Cli>Il extrait le contenu du \u003Ccode>&#x3C;body>\u003C/code> de la réponse.\u003C/li>\n\u003Cli>Il anime la transition et remplace le \u003Ccode>&#x3C;body>\u003C/code> du document courant.\u003C/li>\n\u003C/ol>\n\u003Cp>L'étape clé manquante : \u003Cstrong>le \u003Ccode>&#x3C;head>\u003C/code> du document courant n'est pas systématiquement mis à jour\u003C/strong>. Astro est censé fusionner le \u003Ccode>&#x3C;head>\u003C/code> de la page cible avec le \u003Ccode>&#x3C;head>\u003C/code> courant. Mais dans certaines configurations — notamment quand des scripts tiers ou des composants client injectent des éléments dans le head au runtime — le swap du head échoue silencieusement.\u003C/p>\n\u003Cp>Le résultat visible dans le DOM après navigation SPA :\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;!-- L'utilisateur est sur /articles/sentiers-chartreuse -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">&#x3C;!-- Mais le &#x3C;head> affiche toujours : -->\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\">>Accueil — Le Mag Outdoor&#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\">\"Le magazine outdoor pour les passionnés de randonnée et d'alpinisme.\"\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\">link\u003C/span>\u003Cspan style=\"color:#B392F0\"> rel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"canonical\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> href\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"https://lemag-outdoor.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\">meta\u003C/span>\u003Cspan style=\"color:#B392F0\"> property\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"og:title\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> content\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"Accueil — Le Mag Outdoor\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  &#x3C;!-- ... -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">head\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Ch3>Pourquoi le bug est passé inaperçu\u003C/h3>\n\u003Cp>L'équipe utilise un composant \u003Ccode>&#x3C;HeadSEO />\u003C/code> custom qui injecte un script inline pour mettre à jour \u003Ccode>document.title\u003C/code> côté client. Ce script fonctionne sur le premier page load, mais ne se ré-exécute pas après un swap View Transitions.\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/components/HeadSEO.astro\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\">title\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\">script\u003C/span>\u003Cspan style=\"color:#B392F0\"> define:vars\u003C/span>\u003Cspan style=\"color:#E1E4E8\">={{ title }}>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  document.title \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> title;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">script\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Ce script s'exécute une seule fois, au chargement initial. Après une navigation View Transitions, le nouveau body est injecté, mais les scripts \u003Ccode>define:vars\u003C/code> du head ne sont pas ré-exécutés. Le \u003Ccode>document.title\u003C/code> reste sur la valeur de la page d'entrée.\u003C/p>\n\u003Ch3>La divergence développeur vs Googlebot\u003C/h3>\n\u003Cp>Le développeur teste toujours en accès direct (il tape l'URL, il recharge la page). Il voit le bon \u003Ccode>&#x3C;title>\u003C/code>.\u003C/p>\n\u003Cp>L'utilisateur réel arrive sur la homepage, navigue en mode SPA, et voit le mauvais \u003Ccode>&#x3C;title>\u003C/code> sur les pages internes.\u003C/p>\n\u003Cp>Googlebot, dans son crawl primaire, accède à chaque URL individuellement — il voit les bonnes meta. Mais quand il rend la page avec son moteur Chrome headless et simule des interactions (ce qu'il fait de plus en plus, \u003Ca href=\"https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics\">selon la documentation Google\u003C/a>), il peut observer le DOM post-navigation et constater la divergence.\u003C/p>\n\u003Cp>Le diagnostic est confirmé avec une commande \u003Ccode>curl\u003C/code> comparée au DOM live :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># SSR direct — OK\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -s\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> https://lemag-outdoor.fr/articles/sentiers-chartreuse\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  |\u003C/span>\u003Cspan style=\"color:#B392F0\"> grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -E\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '&#x3C;title>|&#x3C;meta name=\"description\"|&#x3C;link rel=\"canonical\"'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Résultat :\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># &#x3C;title>Les 10 meilleurs sentiers de randonnée en Chartreuse — Le Mag Outdoor&#x3C;/title>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># &#x3C;meta name=\"description\" content=\"Découvrez notre sélection des plus beaux sentiers...\" />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># &#x3C;link rel=\"canonical\" href=\"https://lemag-outdoor.fr/articles/sentiers-chartreuse\" />\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Puis dans Chrome DevTools, après navigation SPA depuis la homepage :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Console DevTools après navigation View Transitions\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">console.\u003C/span>\u003Cspan style=\"color:#B392F0\">log\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(document.title);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// \"Accueil — Le Mag Outdoor\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">console.\u003C/span>\u003Cspan style=\"color:#B392F0\">log\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(document.\u003C/span>\u003Cspan style=\"color:#B392F0\">querySelector\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'meta[name=\"description\"]'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)?.content);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// \"Le magazine outdoor pour les passionnés de randonnée et d'alpinisme.\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">console.\u003C/span>\u003Cspan style=\"color:#B392F0\">log\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(document.\u003C/span>\u003Cspan style=\"color:#B392F0\">querySelector\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'link[rel=\"canonical\"]'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)?.href);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// \"https://lemag-outdoor.fr/\"\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>La preuve est là. Le DOM live ment. Le serveur dit vrai, mais le client ment — et tout service qui parse le DOM post-navigation (previews sociales, extensions SEO, outils de monitoring client-side) voit les meta de la page d'entrée.\u003C/p>\n\u003Cp>Un audit Screaming Frog en mode « JavaScript rendering » sur les 1 200 URLs confirme : en crawl direct, 100% des meta sont correctes. Mais un crawl en mode « Suivre les liens internes avec JS activé » montre que 73% des pages visitées via navigation interne héritent du \u003Ccode>&#x3C;head>\u003C/code> de la page d'entrée du crawl.\u003C/p>\n\u003Ch3>Le cas aggravant : les canonicals\u003C/h3>\n\u003Cp>Le plus dangereux n'est pas le \u003Ccode>&#x3C;title>\u003C/code>. C'est le \u003Ccode>&#x3C;link rel=\"canonical\">\u003C/code>. Si Google visite une page article et voit un canonical pointant vers la homepage, il peut décider que la page article est un doublon de la homepage. Ce scénario est similaire aux \u003Ca href=\"/blog/migration-prestashop-vers-bigcommerce-canonicals-pointent-encore-vers-le-staging\">problèmes de canonicals mal propagés après migration\u003C/a> — sauf qu'ici, il n'y a pas de migration. Juste un composant de transition qui casse la chaîne de meta.\u003C/p>\n\u003Ch2>Le fix : forcer le head swap et écouter les lifecycle events\u003C/h2>\n\u003Ch3>Étape 1 — Supprimer le composant HeadSEO custom\u003C/h3>\n\u003Cp>Le script \u003Ccode>define:vars\u003C/code> qui force \u003Ccode>document.title\u003C/code> est un workaround qui masque le vrai problème. Il est supprimé.\u003C/p>\n\u003Ch3>Étape 2 — S'assurer que le head est swappé par View Transitions\u003C/h3>\n\u003Cp>Astro expose des événements de lifecycle pour les View Transitions. L'événement \u003Ccode>astro:after-swap\u003C/code> se déclenche après que le nouveau contenu a été injecté dans le DOM. C'est le moment de vérifier — et si nécessaire forcer — la mise à jour du head.\u003C/p>\n\u003Cp>Le correctif dans le 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/layouts/BaseLayout.astro — version corrigée\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { ViewTransitions } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'astro:transitions'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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\">  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\">  canonical\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\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#79B8FF\">title\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">description\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">canonical\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:#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\">meta\u003C/span>\u003Cspan style=\"color:#B392F0\"> name\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"viewport\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> content\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"width=device-width, initial-scale=1\"\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\">>{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\">={description} />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">link\u003C/span>\u003Cspan style=\"color:#B392F0\"> rel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"canonical\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> href\u003C/span>\u003Cspan style=\"color:#E1E4E8\">={canonical} />\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\">={title} />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">meta\u003C/span>\u003Cspan style=\"color:#B392F0\"> property\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"og:description\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> content\u003C/span>\u003Cspan style=\"color:#E1E4E8\">={description} />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">ViewTransitions\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">head\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">body\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">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\">script\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      document.\u003C/span>\u003Cspan style=\"color:#B392F0\">addEventListener\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'astro:after-swap'\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:#6A737D\">        // Astro swap le head par défaut, mais certains éléments\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">        // peuvent être ignorés si un script tiers les a modifiés.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">        // Ce listener force la synchronisation du title avec le DOM swappé.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> newTitle\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> document.\u003C/span>\u003Cspan style=\"color:#B392F0\">querySelector\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'title'\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\"> (newTitle) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          document.title \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> newTitle.textContent \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> ''\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">script\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">body\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">html\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Ch3>Étape 3 — Utiliser \u003Ccode>transition:persist\u003C/code> avec précaution\u003C/h3>\n\u003Cp>L'équipe avait ajouté \u003Ccode>transition:persist\u003C/code> sur le header et le footer pour éviter de les recharger. Ce directive indique à Astro de conserver l'élément entre les navigations. Problème : si un composant dans le header lit des meta du \u003Ccode>&#x3C;head>\u003C/code> au mount et les cache, il affiche des données obsolètes.\u003C/p>\n\u003Cp>La règle : ne jamais persister un composant qui dépend du \u003Ccode>&#x3C;head>\u003C/code> de la page courante.\u003C/p>\n\u003Ch3>Étape 4 — Upgrade vers Astro 4.9+\u003C/h3>\n\u003Cp>À partir d'Astro 4.9, le mécanisme de head swap a été renforcé. Le framework fait un diff plus agressif entre le \u003Ccode>&#x3C;head>\u003C/code> de la page courante et celui de la page cible, et met à jour chaque élément individuellement. La \u003Ca href=\"https://docs.astro.build/en/guides/view-transitions/\">documentation Astro sur les View Transitions\u003C/a> détaille ce comportement.\u003C/p>\n\u003Cp>La mise à jour :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Upgrade Astro\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">npm\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> install\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> astro@latest\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Vérifier la version\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">npx\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> astro\u003C/span>\u003Cspan style=\"color:#79B8FF\"> --version\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># astro v4.12.2\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Rebuild et test\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">npm\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> run\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> build\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">npm\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> run\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> preview\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Ch3>Étape 5 — Ajouter un test automatisé\u003C/h3>\n\u003Cp>L'équipe ajoute un test Playwright qui simule une navigation SPA et vérifie le \u003Ccode>&#x3C;head>\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\">// tests/view-transitions-head.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:#B392F0\">test\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'View Transitions met à jour le head sur navigation interne'\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\">page\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:#6A737D\">  // Arriver sur la homepage\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> page.\u003C/span>\u003Cspan style=\"color:#B392F0\">goto\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'https://lemag-outdoor.fr/'\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\"> homeTitle\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> page.\u003C/span>\u003Cspan style=\"color:#B392F0\">title\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\">(homeTitle).\u003C/span>\u003Cspan style=\"color:#B392F0\">toContain\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Accueil'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  // Naviguer vers un article via clic (déclenche View Transitions)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> page.\u003C/span>\u003Cspan style=\"color:#B392F0\">click\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'a[href=\"/articles/sentiers-chartreuse\"]'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> page.\u003C/span>\u003Cspan style=\"color:#B392F0\">waitForURL\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'**/articles/sentiers-chartreuse'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  // Vérifier que le title a changé\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> articleTitle\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> page.\u003C/span>\u003Cspan style=\"color:#B392F0\">title\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\">(articleTitle).not.\u003C/span>\u003Cspan style=\"color:#B392F0\">toContain\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Accueil'\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\">(articleTitle).\u003C/span>\u003Cspan style=\"color:#B392F0\">toContain\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Chartreuse'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  // Vérifier la meta description\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> description\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> page.\u003C/span>\u003Cspan style=\"color:#B392F0\">getAttribute\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'meta[name=\"description\"]'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'content'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(description).not.\u003C/span>\u003Cspan style=\"color:#B392F0\">toContain\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'magazine outdoor'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  // Vérifier le canonical\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> canonical\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> page.\u003C/span>\u003Cspan style=\"color:#B392F0\">getAttribute\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'link[rel=\"canonical\"]'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'href'\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\">(canonical).\u003C/span>\u003Cspan style=\"color:#B392F0\">toBe\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'https://lemag-outdoor.fr/articles/sentiers-chartreuse'\u003C/span>\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\u003Ch3>Invalidation du cache et redéploiement\u003C/h3>\n\u003Cp>Le fix est déployé un mercredi à 11h. Le cache Vercel est purgé manuellement via le dashboard. Les pages les plus touchées sont soumises à la réindexation via l'outil d'inspection d'URL de Search Console — par lots de 50 par jour, en commençant par les pages avec le plus de trafic historique.\u003C/p>\n\u003Ch3>Temps de récupération\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Cstrong>J+3\u003C/strong> : Google recrawle 60% des pages soumises. Les titres dans les SERPs commencent à revenir à la normale.\u003C/li>\n\u003Cli>\u003Cstrong>J+7\u003C/strong> : 90% des pages affichent le bon titre dans Search Console.\u003C/li>\n\u003Cli>\u003Cstrong>J+14\u003C/strong> : Les clics remontent à 85% du niveau pré-incident.\u003C/li>\n\u003Cli>\u003Cstrong>J+21\u003C/strong> : Retour au niveau de trafic normal. Certaines pages longue traîne mettent 4 semaines supplémentaires à retrouver leur position exacte.\u003C/li>\n\u003C/ul>\n\u003Cp>Ce schéma de récupération est cohérent avec d'autres incidents de meta corrompues. Le même type de timeline a été observé lors d'un \u003Ca href=\"/blog/astro-content-collections-frontmatter-title-non-passe-apres-refacto\">problème similaire sur les content collections Astro\u003C/a> où des titres frontmatter ne passaient plus après une refactorisation.\u003C/p>\n\u003Ch2>Ce qu'on en retient\u003C/h2>\n\u003Cp>Le mode SPA des frameworks n'est pas une fonctionnalité SEO-neutre. Chaque mécanisme qui intercepte la navigation native du navigateur crée un risque de divergence entre le HTML servi et le DOM visible. Les View Transitions d'Astro, comme les transitions équivalentes dans \u003Ca href=\"/blog/next-js-metadata-async-qui-throw-la-page-sert-le-fallback-default-next-js\">Next.js\u003C/a>, \u003Ca href=\"/blog/nuxt-useseometa-le-child-override-silencieusement-les-meta-du-layout-parent\">Nuxt\u003C/a> ou \u003Ca href=\"/blog/sveltekit-layout-ts-title-override-par-page-svelte-vide\">SvelteKit\u003C/a>, ajoutent une couche d'abstraction sur la navigation — et cette couche doit être testée spécifiquement pour le \u003Ccode>&#x3C;head>\u003C/code>.\u003C/p>\n\u003Cp>Les tests manuels ne suffisent pas. Un développeur qui recharge la page verra toujours le bon titre. Il faut tester la navigation client-side, pas le rendu initial. Un monitoring continu type Seogard détecte cette divergence SSR/DOM en quelques minutes, parce qu'il compare systématiquement le HTML brut et le DOM rendu après hydratation — exactement le cas de figure que les tests manuels ratent.\u003C/p>\n\u003Cp>Trois règles à graver : tester le \u003Ccode>&#x3C;head>\u003C/code> après navigation SPA, pas seulement après page load. Ne jamais persister un composant qui lit les meta de la page. Et traiter le canonical comme la donnée la plus critique du \u003Ccode>&#x3C;head>\u003C/code> — un canonical incorrect fait plus de dégâts qu'un \u003Ccode>&#x3C;title>\u003C/code> mal formaté.\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,11,[18,19,20,21],"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)",[25,41,54,68,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":32,"tags":33,"title":39,"updatedAt":40},"6a2cf253aa6b273b0c0c9a5f","tanstack-router-ssr-title-pris-du-layout-au-lieu-de-la-leaf-route","https://seogard.io/blog/tanstack-router-ssr-title-pris-du-layout-au-lieu-de-la-leaf-route","2026-06-13T06:01:55.020Z","2026-06-13","Un e-commerce perd 40 % de clics organiques : TanStack Router applique le title du layout parent au lieu de la leaf route. Récit, diagnostic, fix.",12,[34,35,36,37,38],"tanstack router","react","ssr","title","meta tags","TanStack Router SSR : le title vient du layout, pas de la page","Sat Jun 13 2026 06:01:55 GMT+0000 (Coordinated Universal Time)",{"_id":42,"slug":43,"__v":6,"author":7,"canonical":44,"category":10,"createdAt":45,"date":46,"description":47,"image":15,"imageAlt":15,"readingTime":32,"tags":48,"title":52,"updatedAt":53},"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.",[49,50,51,36],"remix","meta","streaming","Remix meta() async : metas vides en streaming SSR","Thu Jun 11 2026 16:01:48 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":32,"tags":61,"title":66,"updatedAt":67},"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","2026-06-10","Un site Nuxt 3 perd ses meta par défaut sur 340 pages. Récit technique du bug useSeoMeta, diagnostic et fix du fallback layout.",[62,63,64,65,50],"nuxt","useSeoMeta","layout","override","Nuxt useSeoMeta : le child override les meta du layout","Wed Jun 10 2026 06:01:32 GMT+0000 (Coordinated Universal Time)",{"_id":69,"slug":70,"__v":6,"author":7,"canonical":71,"category":10,"createdAt":72,"date":59,"description":73,"image":15,"imageAlt":15,"readingTime":32,"tags":74,"title":77,"updatedAt":78},"6a298a64aa6b273b0c3bfcab","sveltekit-layout-ts-title-override-par-page-svelte-vide","https://seogard.io/blog/sveltekit-layout-ts-title-override-par-page-svelte-vide","2026-06-10T16:01:40.148Z","Un +page.svelte sans title écrase le layout parent. Googlebot voit un \u003Ctitle> vide. Récit, diagnostic et fix complet en SvelteKit.",[75,64,37,76],"sveltekit","svelte","SvelteKit : title vide en prod, 0 clic sur 3 semaines","Wed Jun 10 2026 16:01:40 GMT+0000 (Coordinated Universal Time)",{"_id":80,"slug":81,"__v":6,"author":7,"canonical":82,"category":10,"createdAt":83,"date":84,"description":85,"image":15,"imageAlt":15,"readingTime":32,"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":16,"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.",[18,101,102,103],"content collections","frontmatter","refacto","Astro Content Collections : 80 titles vides après refacto","Mon Jun 08 2026 16:01:41 GMT+0000 (Coordinated Universal Time)",{"categories":107},[108,112,116,120,124,126,130,133,136,140,144,147,150,154,157,160,163,166,170],{"category":109,"slug":110,"count":111},"Actualités SEO","actualites-seo",168,{"category":113,"slug":114,"count":115},"Migration","migration",18,{"category":117,"slug":118,"count":119},"Rendering","rendering",9,{"category":121,"slug":122,"count":123},"Performance","performance",8,{"category":10,"slug":125,"count":123},"framework",{"category":127,"slug":128,"count":129},"Crawl","crawl",7,{"category":131,"slug":132,"count":129},"SEO Technique","seo-technique",{"category":134,"slug":135,"count":129},"Meta Tags","meta-tags",{"category":137,"slug":138,"count":139},"Architecture","architecture",6,{"category":141,"slug":142,"count":143},"Structured Data","structured-data",5,{"category":145,"slug":146,"count":143},"JavaScript SEO","javascript-seo",{"category":148,"slug":149,"count":143},"Monitoring","monitoring",{"category":151,"slug":152,"count":153},"E-commerce","e-commerce",4,{"category":155,"slug":156,"count":153},"Avancé","avance",{"category":158,"slug":159,"count":153},"Refonte","refonte",{"category":161,"slug":162,"count":153},"Redirections","redirections",{"category":164,"slug":165,"count":153},"Outils","outils",{"category":167,"slug":168,"count":169},"IA & SEO","ia-seo",3,{"category":171,"slug":172,"count":169},"Contenu","contenu"]