[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$flBLOqBnLj67GRmhf54qQvSG0WlxbRs8EYhO0vJ2Ptqo":3,"$fjA_0LKMC-wX-54LmS1MnWsCYC8ZKCIF3yG58vIPoo6E":25,"$fcTFjTaPPT1sVcnh1wWEGoTWMSaOD4bwmwntkMae6w5s":110},{"_id":4,"slug":5,"__v":6,"author":7,"body":8,"canonical":9,"category":10,"createdAt":11,"date":12,"description":13,"htmlContent":14,"image":15,"imageAlt":15,"readingTime":16,"tags":17,"title":23,"updatedAt":24,"categoryLegacy":22},"6a3567e0aa6b273b0ce66e5a","multi-currency-dropdown-change-le-title-cote-csr-google-voit-le-default-usd",0,"Equipe Seogard","# Un sélecteur de devise réécrit le title côté client — Google indexe USD partout\n\nJeudi 14h. L'équipe front d'une marketplace mode européenne (Nuxt 3, ~12 000 fiches produit, 14 devises) pousse un nouveau dropdown de devise. Le composant est fluide, animé, testé sur trois navigateurs. Vendredi soir, personne ne remarque rien. Lundi matin, l'équipe SEO ouvre la Search Console pour préparer un reporting trimestriel. Sur le rapport Performance, filtre pays = Allemagne : les impressions ont chuté de 31 % en quatre jours. Filtre pays = Suède : −44 %. Le seul marché stable, c'est les États-Unis.\n\n## Lundi 9h12 — « C'est un problème de Core Update ? »\n\nLe premier réflexe du Lead SEO : chercher une annonce d'update Google. Rien sur X, rien sur Search Engine Journal. Hypothèse écartée.\n\nDeuxième réflexe : vérifier les erreurs d'indexation dans la Search Console. Aucune alerte, aucun pic de pages non indexées. Les pages sont indexées. Le problème est ailleurs.\n\nÀ 9h35, le Lead SEO tape `site:example.com \"Robe longue\"` dans Google. Le snippet affiché pour la fiche produit phare en France indique :\n\n> **Robe Longue Lin — $129.00 | BrandName**\n\nDollar. Pas euro. Le titre affiché dans les SERP porte le prix en USD — pour le marché français.\n\nIl teste depuis un VPN allemand. Même résultat : `$129.00`. Suédois : `$129.00`. Partout, le title indexé contient le prix en dollars américains.\n\nÀ 10h02, l'équipe front est convoquée. Le développeur principal ouvre la page dans Chrome, sélectionne EUR dans le dropdown. Le `\u003Ctitle>` se met à jour instantanément :\n\n> Robe Longue Lin — 119,00 € | BrandName\n\n« Ça marche chez moi. » La phrase classique.\n\nLe Lead SEO ouvre alors l'outil d'inspection d'URL de la Search Console. Le HTML capturé par Googlebot apparaît. Le `\u003Ctitle>` dans le `\u003Chead>` :\n\n> Robe Longue Lin — $129.00 | BrandName\n\nDollar. Le prix par défaut. Celui qui est rendu côté serveur avant toute interaction JS.\n\nLes chiffres tombent vite. Sur les 21 derniers jours :\n- Trafic organique hors-US : −18 700 clics (de 61 200 à 42 500).\n- Impressions marchés EUR : −27 %.\n- CTR moyen sur les fiches produit DE, FR, SE, DK : passé de 3.1 % à 1.9 %.\n\nLes titres en dollars sur des SERP en euros détruisent le CTR. Un utilisateur allemand qui voit `$129.00` au lieu de `119,00 €` ne clique pas — ou clique chez le concurrent qui affiche le bon prix dans sa devise.\n\nLe problème n'est pas un bug d'indexation. C'est un bug de rendu.\n\n## Le bug : document.title réécrit au runtime, invisible au crawl\n\n### Ce que voit le développeur\n\nLe composant `CurrencyDropdown.vue` fonctionne comme suit. Au montage, il lit la devise depuis un cookie `preferred_currency` ou depuis le `localStorage`. S'il en trouve une, il met à jour le state Pinia, ce qui déclenche un watcher qui réécrit le `document.title` dynamiquement.\n\nVoici le composant simplifié :\n\n```vue\n\u003Cscript setup lang=\"ts\">\nimport { useCurrencyStore } from '~/stores/currency'\nimport { watch } from 'vue'\n\nconst currencyStore = useCurrencyStore()\n\n// Lecture cookie / localStorage au mount (client-side uniquement)\nonMounted(() => {\n  const saved = localStorage.getItem('preferred_currency')\n  if (saved) {\n    currencyStore.setCurrency(saved)\n  }\n})\n\n// Watcher qui réécrit le title quand la devise change\nwatch(\n  () => currencyStore.currentCurrency,\n  (newCurrency) => {\n    const product = useProductStore().current\n    if (product) {\n      const price = product.prices[newCurrency]\n      const symbol = getCurrencySymbol(newCurrency)\n      document.title = `${product.name} — ${symbol}${price} | BrandName`\n    }\n  }\n)\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cselect @change=\"currencyStore.setCurrency($event.target.value)\">\n    \u003Coption v-for=\"c in currencyStore.availableCurrencies\" :key=\"c\" :value=\"c\">\n      {{ c }}\n    \u003C/option>\n  \u003C/select>\n\u003C/template>\n```\n\nDans le navigateur, le flux est :\n1. Le SSR rend la page avec la devise par défaut (`USD`).\n2. L'hydratation Vue s'exécute.\n3. `onMounted` lit le cookie/localStorage.\n4. Le watcher réécrit `document.title` avec la bonne devise.\n\nL'utilisateur humain ne voit jamais le titre en USD (sauf un flash de quelques millisecondes). Tout semble fonctionner.\n\n### Ce que voit Googlebot\n\nGooglebot effectue un fetch HTTP brut. Il reçoit le HTML SSR. Le `\u003Ctitle>` contient la devise par défaut :\n\n```html\n\u003C!DOCTYPE html>\n\u003Chtml lang=\"fr\">\n\u003Chead>\n  \u003Ctitle>Robe Longue Lin — $129.00 | BrandName\u003C/title>\n  \u003Cmeta name=\"description\" content=\"Robe longue en lin, $129.00. Livraison offerte.\" />\n  \u003C!-- ... -->\n\u003C/head>\n```\n\nMême si Googlebot exécute le JavaScript dans un second passage (le fameux \"two-wave indexing\"), il ne dispose ni de `localStorage` ni de cookie `preferred_currency`. Le `onMounted` ne trouve rien. Le watcher ne se déclenche pas (la devise reste `USD`). Le title reste inchangé.\n\nRésultat : 100 % des pages produit sont indexées avec le prix en USD, quelle que soit la langue ou le pays cible.\n\n### Vérification avec curl et Screaming Frog\n\nL'équipe confirme le diagnostic avec un fetch brut :\n\n```bash\ncurl -s https://www.example.com/fr/robe-longue-lin \\\n  -H \"User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\" \\\n  | grep -oP '\u003Ctitle>.*?\u003C/title>'\n```\n\nSortie :\n\n```\n\u003Ctitle>Robe Longue Lin — $129.00 | BrandName\u003C/title>\n```\n\nUn crawl Screaming Frog en mode \"JavaScript rendering off\" sur les 12 000 fiches produit confirme : 100 % des titles contiennent `$`. Un second crawl en mode \"JavaScript rendering on\" (avec Chrome headless intégré) montre le même résultat — parce que Screaming Frog ne porte pas le cookie `preferred_currency` non plus.\n\nLe diagnostic est clair. Le `\u003Ctitle>` SSR contient la devise par défaut. La réécriture est purement CSR, conditionnée à un état client (cookie/localStorage) que ni Googlebot ni aucun crawler ne possède.\n\n### Pourquoi les tests n'ont rien détecté\n\nL'équipe avait des tests Cypress end-to-end. Mais chaque test démarrait en injectant un cookie `preferred_currency=EUR` dans le `beforeEach`. Le scénario \"aucun cookie, première visite\" n'était pas couvert. Le title SSR n'a jamais été vérifié en isolation.\n\nLe pipeline CI n'avait aucun test sur le HTML statique rendu. Pas de snapshot du `\u003Chead>`. Pas de validation du `\u003Ctitle>` dans la réponse HTTP brute. Ce type de régression — un contenu qui diverge entre le SSR et le CSR — est [un classique documenté dans d'autres frameworks aussi](/blog/lazy-load-du-hero-h1-dans-une-section-v-if-invisible-au-fetch-http-brut). Le pattern est toujours le même : le navigateur compense, le crawler ne compense pas.\n\n### L'aggravation par la meta description\n\nLe même mécanisme touchait la meta description. Le composant réécrivait aussi `document.querySelector('meta[name=\"description\"]').content` au runtime. Google affichait donc dans les snippets :\n\n> Robe longue en lin, $129.00. Livraison offerte.\n\nSur un marché français. La méta-description en dollars sur une SERP française fait chuter le CTR autant que le title. L'impact est double.\n\nCe type de divergence SSR/CSR sur les metas n'est pas limité aux devises. On observe le même schéma quand un [layout parent se fait override silencieusement](/blog/nuxt-useseometa-le-child-override-silencieusement-les-meta-du-layout-parent) ou quand une [meta async qui throw fait servir le fallback](/blog/next-js-metadata-async-qui-throw-la-page-sert-le-fallback-default-next-js).\n\n## Le fix — en deux temps\n\n### Étape 1 : le title SSR doit refléter le marché, pas le client\n\nLa devise affichée dans le `\u003Ctitle>` ne doit pas dépendre d'un état client. Elle doit être déterminée côté serveur, à partir de la locale ou du sous-dossier de l'URL.\n\nL'équipe refactorise le `useHead` dans le composable produit pour calculer le title côté serveur :\n\n```typescript\n// composables/useProductSeo.ts\nexport function useProductSeo(product: Product) {\n  const { locale } = useI18n()\n\n  // Mapping locale → devise par défaut pour le marché\n  const localeCurrencyMap: Record\u003Cstring, string> = {\n    'fr': 'EUR',\n    'de': 'EUR',\n    'se': 'SEK',\n    'dk': 'DKK',\n    'en': 'USD',\n    'en-gb': 'GBP',\n  }\n\n  const marketCurrency = localeCurrencyMap[locale.value] || 'USD'\n  const price = product.prices[marketCurrency]\n  const symbol = getCurrencySymbol(marketCurrency)\n\n  useHead({\n    title: `${product.name} — ${symbol}${formatPrice(price, marketCurrency)} | BrandName`,\n    meta: [\n      {\n        name: 'description',\n        content: `${product.shortDescription}, ${symbol}${formatPrice(price, marketCurrency)}. ${getShippingLabel(locale.value)}`,\n      },\n    ],\n  })\n}\n```\n\nCe composable s'exécute au moment du SSR. Le `useHead` de Nuxt 3 injecte le `\u003Ctitle>` et la `\u003Cmeta name=\"description\">` directement dans le HTML rendu. Googlebot reçoit le bon prix dans la bonne devise, sans exécuter une seule ligne de JavaScript côté client.\n\nLe dropdown de devise continue de fonctionner côté client. Mais il ne touche plus au `document.title`. Il met à jour uniquement les prix affichés dans le `\u003Cbody>` — ce qui est légitime et n'impacte pas l'indexation.\n\n### Étape 2 : validation et déploiement\n\nAvant de déployer, l'équipe ajoute un test de non-régression :\n\n```typescript\n// tests/seo/product-title.test.ts\nimport { describe, it, expect } from 'vitest'\nimport { $fetch } from '@nuxt/test-utils'\n\ndescribe('Product page SSR title', () => {\n  it('renders EUR price for /fr/ locale', async () => {\n    const html = await $fetch('/fr/robe-longue-lin')\n    expect(html).toContain('\u003Ctitle>Robe Longue Lin — 119,00\\u00a0€ | BrandName\u003C/title>')\n    expect(html).not.toContain('$')\n  })\n\n  it('renders SEK price for /se/ locale', async () => {\n    const html = await $fetch('/se/robe-longue-lin')\n    expect(html).toContain('kr')\n    expect(html).not.toContain('$')\n  })\n\n  it('renders USD price for /en/ locale', async () => {\n    const html = await $fetch('/en/robe-longue-lin')\n    expect(html).toContain('$129.00')\n  })\n})\n```\n\nLe déploiement est poussé un mardi à 11h. L'équipe invalide le cache CDN (Cloudflare) sur les routes `/fr/*`, `/de/*`, `/se/*`, `/dk/*` et `/en-gb/*` :\n\n```bash\ncurl -X POST \"https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache\" \\\n  -H \"Authorization: Bearer {api_token}\" \\\n  -H \"Content-Type: application/json\" \\\n  --data '{\"purge_everything\": true}'\n```\n\nPurge complète. Les pages servies depuis le cache portaient encore les titles en USD.\n\n### Récupération\n\nL'équipe force une réindexation via l'API Indexing de Search Console pour les 200 pages produit à plus fort trafic. Le reste est laissé au crawl naturel.\n\nLes résultats observés :\n- **J+3** : les titles EUR apparaissent dans les SERP pour 40 % des fiches FR/DE.\n- **J+7** : 85 % des titles corrigés visibles dans Search Console.\n- **J+14** : le CTR moyen sur les marchés EUR remonte de 1.9 % à 2.8 % (pas encore le 3.1 % d'avant — le ranking met plus de temps à se stabiliser).\n- **J+21** : les impressions hors-US reviennent à 95 % du niveau pré-incident. Le trafic clics est remonté à 57 800 (vs 61 200 avant, vs 42 500 au creux).\n\nLa récupération totale prend environ quatre semaines. Le CTR atteint 3.0 % à J+28.\n\n### Leçons opérationnelles\n\nL'équipe met en place trois garde-fous :\n\n1. **Snapshot HTML du `\u003Chead>` dans le CI** : chaque merge request déclenche un rendu SSR des pages critiques et compare le `\u003Ctitle>` à un pattern attendu (regex par locale).\n2. **Alerte Screaming Frog automatisée** : un crawl hebdomadaire en mode JS-off vérifie qu'aucun title ne contient `$` sur les locales non-USD.\n3. **Règle ESLint custom** : interdiction de `document.title =` dans les fichiers composants. Le title ne passe que par `useHead`.\n\nL'équipe a aussi documenté un principe dans leur wiki interne : « Tout ce qui apparaît dans le `\u003Chead>` doit être déterministe au SSR. Si ça dépend d'un cookie, d'un localStorage ou d'un state client, ça ne doit pas toucher le `\u003Chead>`. »\n\nCe principe rejoint ce que d'autres équipes découvrent dans des contextes différents — quand un [CMS comme Contentful ne synchronise pas le champ SEO title](/blog/contentful-le-champ-seo-title-non-sync-vers-next-js-fallback-first-h1-genere) ou quand des [hreflang pointent vers des domaines supprimés](/blog/hreflang-generated-pointent-vers-des-domaines-supprimes) parce que la source de vérité n'est pas la bonne.\n\n## Ce qu'on en retient\n\nLe `\u003Ctitle>` est le premier signal que Google lit et le premier texte que l'utilisateur voit dans les SERP. Le réécrire côté client, c'est parier que le crawler exécutera le même code que le navigateur, dans le même contexte. Ce pari est perdu d'avance quand le contenu dépend d'un état utilisateur (cookie, localStorage, géolocalisation IP côté client).\n\nLa règle est simple : le `\u003Chead>` est le territoire du SSR. Le dropdown de devise peut transformer le DOM visible — pas les metas.\n\nUn monitoring de rendu type Seogard détecte cette divergence SSR/CSR en comparant le HTML brut et le rendu post-JS sur chaque crawl. L'alerte arrive en minutes, pas en trois semaines.\n\nLa devise dans le title, c'est un détail. Perdre 18 700 clics en 21 jours parce qu'un `document.title =` a glissé dans un composant Vue, c'est un incident.\n```","https://seogard.io/blog/multi-currency-dropdown-change-le-title-cote-csr-google-voit-le-default-usd","SSR / CSR","2026-06-19T16:01:36.079Z","2026-06-19","Un sélecteur de devise JS réécrit le title au runtime. Google indexe la version USD sur tous les marchés. Récit, diagnostic et correctif.","\u003Ch1>Un sélecteur de devise réécrit le title côté client — Google indexe USD partout\u003C/h1>\n\u003Cp>Jeudi 14h. L'équipe front d'une marketplace mode européenne (Nuxt 3, ~12 000 fiches produit, 14 devises) pousse un nouveau dropdown de devise. Le composant est fluide, animé, testé sur trois navigateurs. Vendredi soir, personne ne remarque rien. Lundi matin, l'équipe SEO ouvre la Search Console pour préparer un reporting trimestriel. Sur le rapport Performance, filtre pays = Allemagne : les impressions ont chuté de 31 % en quatre jours. Filtre pays = Suède : −44 %. Le seul marché stable, c'est les États-Unis.\u003C/p>\n\u003Ch2>Lundi 9h12 — « C'est un problème de Core Update ? »\u003C/h2>\n\u003Cp>Le premier réflexe du Lead SEO : chercher une annonce d'update Google. Rien sur X, rien sur Search Engine Journal. Hypothèse écartée.\u003C/p>\n\u003Cp>Deuxième réflexe : vérifier les erreurs d'indexation dans la Search Console. Aucune alerte, aucun pic de pages non indexées. Les pages sont indexées. Le problème est ailleurs.\u003C/p>\n\u003Cp>À 9h35, le Lead SEO tape \u003Ccode>site:example.com \"Robe longue\"\u003C/code> dans Google. Le snippet affiché pour la fiche produit phare en France indique :\u003C/p>\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>Robe Longue Lin — $129.00 | BrandName\u003C/strong>\u003C/p>\n\u003C/blockquote>\n\u003Cp>Dollar. Pas euro. Le titre affiché dans les SERP porte le prix en USD — pour le marché français.\u003C/p>\n\u003Cp>Il teste depuis un VPN allemand. Même résultat : \u003Ccode>$129.00\u003C/code>. Suédois : \u003Ccode>$129.00\u003C/code>. Partout, le title indexé contient le prix en dollars américains.\u003C/p>\n\u003Cp>À 10h02, l'équipe front est convoquée. Le développeur principal ouvre la page dans Chrome, sélectionne EUR dans le dropdown. Le \u003Ccode>&#x3C;title>\u003C/code> se met à jour instantanément :\u003C/p>\n\u003Cblockquote>\n\u003Cp>Robe Longue Lin — 119,00 € | BrandName\u003C/p>\n\u003C/blockquote>\n\u003Cp>« Ça marche chez moi. » La phrase classique.\u003C/p>\n\u003Cp>Le Lead SEO ouvre alors l'outil d'inspection d'URL de la Search Console. Le HTML capturé par Googlebot apparaît. Le \u003Ccode>&#x3C;title>\u003C/code> dans le \u003Ccode>&#x3C;head>\u003C/code> :\u003C/p>\n\u003Cblockquote>\n\u003Cp>Robe Longue Lin — $129.00 | BrandName\u003C/p>\n\u003C/blockquote>\n\u003Cp>Dollar. Le prix par défaut. Celui qui est rendu côté serveur avant toute interaction JS.\u003C/p>\n\u003Cp>Les chiffres tombent vite. Sur les 21 derniers jours :\u003C/p>\n\u003Cul>\n\u003Cli>Trafic organique hors-US : −18 700 clics (de 61 200 à 42 500).\u003C/li>\n\u003Cli>Impressions marchés EUR : −27 %.\u003C/li>\n\u003Cli>CTR moyen sur les fiches produit DE, FR, SE, DK : passé de 3.1 % à 1.9 %.\u003C/li>\n\u003C/ul>\n\u003Cp>Les titres en dollars sur des SERP en euros détruisent le CTR. Un utilisateur allemand qui voit \u003Ccode>$129.00\u003C/code> au lieu de \u003Ccode>119,00 €\u003C/code> ne clique pas — ou clique chez le concurrent qui affiche le bon prix dans sa devise.\u003C/p>\n\u003Cp>Le problème n'est pas un bug d'indexation. C'est un bug de rendu.\u003C/p>\n\u003Ch2>Le bug : document.title réécrit au runtime, invisible au crawl\u003C/h2>\n\u003Ch3>Ce que voit le développeur\u003C/h3>\n\u003Cp>Le composant \u003Ccode>CurrencyDropdown.vue\u003C/code> fonctionne comme suit. Au montage, il lit la devise depuis un cookie \u003Ccode>preferred_currency\u003C/code> ou depuis le \u003Ccode>localStorage\u003C/code>. S'il en trouve une, il met à jour le state Pinia, ce qui déclenche un watcher qui réécrit le \u003Ccode>document.title\u003C/code> dynamiquement.\u003C/p>\n\u003Cp>Voici le composant simplifié :\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\">script\u003C/span>\u003Cspan style=\"color:#B392F0\"> setup\u003C/span>\u003Cspan style=\"color:#B392F0\"> lang\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"ts\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { useCurrencyStore } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '~/stores/currency'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { watch } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'vue'\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\"> currencyStore\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> useCurrencyStore\u003C/span>\u003Cspan style=\"color:#E1E4E8\">()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Lecture cookie / localStorage au mount (client-side uniquement)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">onMounted\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\"> saved\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> localStorage.\u003C/span>\u003Cspan style=\"color:#B392F0\">getItem\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'preferred_currency'\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\"> (saved) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    currencyStore.\u003C/span>\u003Cspan style=\"color:#B392F0\">setCurrency\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(saved)\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:#6A737D\">// Watcher qui réécrit le title quand la devise change\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">watch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  () \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> currencyStore.currentCurrency,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  (\u003C/span>\u003Cspan style=\"color:#FFAB70\">newCurrency\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\"> product\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> useProductStore\u003C/span>\u003Cspan style=\"color:#E1E4E8\">().current\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (product) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> price\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> product.prices[newCurrency]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> symbol\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> getCurrencySymbol\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(newCurrency)\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:#9ECBFF\"> `${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">product\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">name\u003C/span>\u003Cspan style=\"color:#9ECBFF\">} — ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">symbol\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">price\u003C/span>\u003Cspan style=\"color:#9ECBFF\">} | BrandName`\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">script\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">template\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">select\u003C/span>\u003Cspan style=\"color:#B392F0\"> @change\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"currencyStore.setCurrency($event.target.value)\"\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\">option\u003C/span>\u003Cspan style=\"color:#B392F0\"> v-for\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"c in currencyStore.availableCurrencies\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> :key\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"c\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> :value\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"c\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      {{ c }}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">option\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\">select\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">template\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Dans le navigateur, le flux est :\u003C/p>\n\u003Col>\n\u003Cli>Le SSR rend la page avec la devise par défaut (\u003Ccode>USD\u003C/code>).\u003C/li>\n\u003Cli>L'hydratation Vue s'exécute.\u003C/li>\n\u003Cli>\u003Ccode>onMounted\u003C/code> lit le cookie/localStorage.\u003C/li>\n\u003Cli>Le watcher réécrit \u003Ccode>document.title\u003C/code> avec la bonne devise.\u003C/li>\n\u003C/ol>\n\u003Cp>L'utilisateur humain ne voit jamais le titre en USD (sauf un flash de quelques millisecondes). Tout semble fonctionner.\u003C/p>\n\u003Ch3>Ce que voit Googlebot\u003C/h3>\n\u003Cp>Googlebot effectue un fetch HTTP brut. Il reçoit le HTML SSR. Le \u003Ccode>&#x3C;title>\u003C/code> contient la devise par défaut :\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\">title\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Robe Longue Lin — $129.00 | BrandName&#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\">\"Robe longue en lin, $129.00. Livraison offerte.\"\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\u003Cp>Même si Googlebot exécute le JavaScript dans un second passage (le fameux \"two-wave indexing\"), il ne dispose ni de \u003Ccode>localStorage\u003C/code> ni de cookie \u003Ccode>preferred_currency\u003C/code>. Le \u003Ccode>onMounted\u003C/code> ne trouve rien. Le watcher ne se déclenche pas (la devise reste \u003Ccode>USD\u003C/code>). Le title reste inchangé.\u003C/p>\n\u003Cp>Résultat : 100 % des pages produit sont indexées avec le prix en USD, quelle que soit la langue ou le pays cible.\u003C/p>\n\u003Ch3>Vérification avec curl et Screaming Frog\u003C/h3>\n\u003Cp>L'équipe confirme le diagnostic avec un fetch brut :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -s\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> https://www.example.com/fr/robe-longue-lin\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  -H\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  |\u003C/span>\u003Cspan style=\"color:#B392F0\"> grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -oP\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '&#x3C;title>.*?&#x3C;/title>'\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Sortie :\u003C/p>\n\u003Cpre>\u003Ccode>&#x3C;title>Robe Longue Lin — $129.00 | BrandName&#x3C;/title>\n\u003C/code>\u003C/pre>\n\u003Cp>Un crawl Screaming Frog en mode \"JavaScript rendering off\" sur les 12 000 fiches produit confirme : 100 % des titles contiennent \u003Ccode>$\u003C/code>. Un second crawl en mode \"JavaScript rendering on\" (avec Chrome headless intégré) montre le même résultat — parce que Screaming Frog ne porte pas le cookie \u003Ccode>preferred_currency\u003C/code> non plus.\u003C/p>\n\u003Cp>Le diagnostic est clair. Le \u003Ccode>&#x3C;title>\u003C/code> SSR contient la devise par défaut. La réécriture est purement CSR, conditionnée à un état client (cookie/localStorage) que ni Googlebot ni aucun crawler ne possède.\u003C/p>\n\u003Ch3>Pourquoi les tests n'ont rien détecté\u003C/h3>\n\u003Cp>L'équipe avait des tests Cypress end-to-end. Mais chaque test démarrait en injectant un cookie \u003Ccode>preferred_currency=EUR\u003C/code> dans le \u003Ccode>beforeEach\u003C/code>. Le scénario \"aucun cookie, première visite\" n'était pas couvert. Le title SSR n'a jamais été vérifié en isolation.\u003C/p>\n\u003Cp>Le pipeline CI n'avait aucun test sur le HTML statique rendu. Pas de snapshot du \u003Ccode>&#x3C;head>\u003C/code>. Pas de validation du \u003Ccode>&#x3C;title>\u003C/code> dans la réponse HTTP brute. Ce type de régression — un contenu qui diverge entre le SSR et le CSR — est \u003Ca href=\"/blog/lazy-load-du-hero-h1-dans-une-section-v-if-invisible-au-fetch-http-brut\">un classique documenté dans d'autres frameworks aussi\u003C/a>. Le pattern est toujours le même : le navigateur compense, le crawler ne compense pas.\u003C/p>\n\u003Ch3>L'aggravation par la meta description\u003C/h3>\n\u003Cp>Le même mécanisme touchait la meta description. Le composant réécrivait aussi \u003Ccode>document.querySelector('meta[name=\"description\"]').content\u003C/code> au runtime. Google affichait donc dans les snippets :\u003C/p>\n\u003Cblockquote>\n\u003Cp>Robe longue en lin, $129.00. Livraison offerte.\u003C/p>\n\u003C/blockquote>\n\u003Cp>Sur un marché français. La méta-description en dollars sur une SERP française fait chuter le CTR autant que le title. L'impact est double.\u003C/p>\n\u003Cp>Ce type de divergence SSR/CSR sur les metas n'est pas limité aux devises. On observe le même schéma quand un \u003Ca href=\"/blog/nuxt-useseometa-le-child-override-silencieusement-les-meta-du-layout-parent\">layout parent se fait override silencieusement\u003C/a> ou quand une \u003Ca href=\"/blog/next-js-metadata-async-qui-throw-la-page-sert-le-fallback-default-next-js\">meta async qui throw fait servir le fallback\u003C/a>.\u003C/p>\n\u003Ch2>Le fix — en deux temps\u003C/h2>\n\u003Ch3>Étape 1 : le title SSR doit refléter le marché, pas le client\u003C/h3>\n\u003Cp>La devise affichée dans le \u003Ccode>&#x3C;title>\u003C/code> ne doit pas dépendre d'un état client. Elle doit être déterminée côté serveur, à partir de la locale ou du sous-dossier de l'URL.\u003C/p>\n\u003Cp>L'équipe refactorise le \u003Ccode>useHead\u003C/code> dans le composable produit pour calculer le title côté serveur :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// composables/useProductSeo.ts\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> function\u003C/span>\u003Cspan style=\"color:#B392F0\"> useProductSeo\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">product\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> Product\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#79B8FF\">locale\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#B392F0\"> useI18n\u003C/span>\u003Cspan style=\"color:#E1E4E8\">()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  // Mapping locale → devise par défaut pour le marché\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> localeCurrencyMap\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\">    'fr'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'EUR'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'de'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'EUR'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'se'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'SEK'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'dk'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'DKK'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'en'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'USD'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'en-gb'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'GBP'\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:#79B8FF\"> marketCurrency\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> localeCurrencyMap[locale.value] \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'USD'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> price\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> product.prices[marketCurrency]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> symbol\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> getCurrencySymbol\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(marketCurrency)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  useHead\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    title: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">`${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">product\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">name\u003C/span>\u003Cspan style=\"color:#9ECBFF\">} — ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">symbol\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}${\u003C/span>\u003Cspan style=\"color:#B392F0\">formatPrice\u003C/span>\u003Cspan style=\"color:#9ECBFF\">(\u003C/span>\u003Cspan style=\"color:#E1E4E8\">price\u003C/span>\u003Cspan style=\"color:#9ECBFF\">, \u003C/span>\u003Cspan style=\"color:#E1E4E8\">marketCurrency\u003C/span>\u003Cspan style=\"color:#9ECBFF\">)\u003C/span>\u003Cspan style=\"color:#9ECBFF\">} | BrandName`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    meta: [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        name: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'description'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        content: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">`${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">product\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">shortDescription\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}, ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">symbol\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}${\u003C/span>\u003Cspan style=\"color:#B392F0\">formatPrice\u003C/span>\u003Cspan style=\"color:#9ECBFF\">(\u003C/span>\u003Cspan style=\"color:#E1E4E8\">price\u003C/span>\u003Cspan style=\"color:#9ECBFF\">, \u003C/span>\u003Cspan style=\"color:#E1E4E8\">marketCurrency\u003C/span>\u003Cspan style=\"color:#9ECBFF\">)\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}. ${\u003C/span>\u003Cspan style=\"color:#B392F0\">getShippingLabel\u003C/span>\u003Cspan style=\"color:#9ECBFF\">(\u003C/span>\u003Cspan style=\"color:#E1E4E8\">locale\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">value\u003C/span>\u003Cspan style=\"color:#9ECBFF\">)\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    ],\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  })\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Ce composable s'exécute au moment du SSR. Le \u003Ccode>useHead\u003C/code> de Nuxt 3 injecte le \u003Ccode>&#x3C;title>\u003C/code> et la \u003Ccode>&#x3C;meta name=\"description\">\u003C/code> directement dans le HTML rendu. Googlebot reçoit le bon prix dans la bonne devise, sans exécuter une seule ligne de JavaScript côté client.\u003C/p>\n\u003Cp>Le dropdown de devise continue de fonctionner côté client. Mais il ne touche plus au \u003Ccode>document.title\u003C/code>. Il met à jour uniquement les prix affichés dans le \u003Ccode>&#x3C;body>\u003C/code> — ce qui est légitime et n'impacte pas l'indexation.\u003C/p>\n\u003Ch3>Étape 2 : validation et déploiement\u003C/h3>\n\u003Cp>Avant de déployer, l'équipe ajoute un test de non-régression :\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/product-title.test.ts\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { describe, it, expect } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'vitest'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { $fetch } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '@nuxt/test-utils'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">describe\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Product page SSR title'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, () \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  it\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'renders EUR price for /fr/ locale'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#F97583\">async\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> () \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> html\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> $fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/fr/robe-longue-lin'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(html).\u003C/span>\u003Cspan style=\"color:#B392F0\">toContain\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'&#x3C;title>Robe Longue Lin — 119,00\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\u00a0\u003C/span>\u003Cspan style=\"color:#9ECBFF\">€ | BrandName&#x3C;/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\">(html).not.\u003C/span>\u003Cspan style=\"color:#B392F0\">toContain\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'$'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  })\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  it\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'renders SEK price for /se/ locale'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#F97583\">async\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> () \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> html\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> $fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/se/robe-longue-lin'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(html).\u003C/span>\u003Cspan style=\"color:#B392F0\">toContain\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'kr'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(html).not.\u003C/span>\u003Cspan style=\"color:#B392F0\">toContain\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'$'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  })\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  it\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'renders USD price for /en/ locale'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#F97583\">async\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> () \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> html\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> $fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/en/robe-longue-lin'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(html).\u003C/span>\u003Cspan style=\"color:#B392F0\">toContain\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'$129.00'\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>Le déploiement est poussé un mardi à 11h. L'équipe invalide le cache CDN (Cloudflare) sur les routes \u003Ccode>/fr/*\u003C/code>, \u003Ccode>/de/*\u003C/code>, \u003Ccode>/se/*\u003C/code>, \u003Ccode>/dk/*\u003C/code> et \u003Ccode>/en-gb/*\u003C/code> :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -X\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> POST\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  -H\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"Authorization: Bearer {api_token}\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  -H\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"Content-Type: application/json\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  --data\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '{\"purge_everything\": true}'\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Purge complète. Les pages servies depuis le cache portaient encore les titles en USD.\u003C/p>\n\u003Ch3>Récupération\u003C/h3>\n\u003Cp>L'équipe force une réindexation via l'API Indexing de Search Console pour les 200 pages produit à plus fort trafic. Le reste est laissé au crawl naturel.\u003C/p>\n\u003Cp>Les résultats observés :\u003C/p>\n\u003Cul>\n\u003Cli>\u003Cstrong>J+3\u003C/strong> : les titles EUR apparaissent dans les SERP pour 40 % des fiches FR/DE.\u003C/li>\n\u003Cli>\u003Cstrong>J+7\u003C/strong> : 85 % des titles corrigés visibles dans Search Console.\u003C/li>\n\u003Cli>\u003Cstrong>J+14\u003C/strong> : le CTR moyen sur les marchés EUR remonte de 1.9 % à 2.8 % (pas encore le 3.1 % d'avant — le ranking met plus de temps à se stabiliser).\u003C/li>\n\u003Cli>\u003Cstrong>J+21\u003C/strong> : les impressions hors-US reviennent à 95 % du niveau pré-incident. Le trafic clics est remonté à 57 800 (vs 61 200 avant, vs 42 500 au creux).\u003C/li>\n\u003C/ul>\n\u003Cp>La récupération totale prend environ quatre semaines. Le CTR atteint 3.0 % à J+28.\u003C/p>\n\u003Ch3>Leçons opérationnelles\u003C/h3>\n\u003Cp>L'équipe met en place trois garde-fous :\u003C/p>\n\u003Col>\n\u003Cli>\u003Cstrong>Snapshot HTML du \u003Ccode>&#x3C;head>\u003C/code> dans le CI\u003C/strong> : chaque merge request déclenche un rendu SSR des pages critiques et compare le \u003Ccode>&#x3C;title>\u003C/code> à un pattern attendu (regex par locale).\u003C/li>\n\u003Cli>\u003Cstrong>Alerte Screaming Frog automatisée\u003C/strong> : un crawl hebdomadaire en mode JS-off vérifie qu'aucun title ne contient \u003Ccode>$\u003C/code> sur les locales non-USD.\u003C/li>\n\u003Cli>\u003Cstrong>Règle ESLint custom\u003C/strong> : interdiction de \u003Ccode>document.title =\u003C/code> dans les fichiers composants. Le title ne passe que par \u003Ccode>useHead\u003C/code>.\u003C/li>\n\u003C/ol>\n\u003Cp>L'équipe a aussi documenté un principe dans leur wiki interne : « Tout ce qui apparaît dans le \u003Ccode>&#x3C;head>\u003C/code> doit être déterministe au SSR. Si ça dépend d'un cookie, d'un localStorage ou d'un state client, ça ne doit pas toucher le \u003Ccode>&#x3C;head>\u003C/code>. »\u003C/p>\n\u003Cp>Ce principe rejoint ce que d'autres équipes découvrent dans des contextes différents — quand un \u003Ca href=\"/blog/contentful-le-champ-seo-title-non-sync-vers-next-js-fallback-first-h1-genere\">CMS comme Contentful ne synchronise pas le champ SEO title\u003C/a> ou quand des \u003Ca href=\"/blog/hreflang-generated-pointent-vers-des-domaines-supprimes\">hreflang pointent vers des domaines supprimés\u003C/a> parce que la source de vérité n'est pas la bonne.\u003C/p>\n\u003Ch2>Ce qu'on en retient\u003C/h2>\n\u003Cp>Le \u003Ccode>&#x3C;title>\u003C/code> est le premier signal que Google lit et le premier texte que l'utilisateur voit dans les SERP. Le réécrire côté client, c'est parier que le crawler exécutera le même code que le navigateur, dans le même contexte. Ce pari est perdu d'avance quand le contenu dépend d'un état utilisateur (cookie, localStorage, géolocalisation IP côté client).\u003C/p>\n\u003Cp>La règle est simple : le \u003Ccode>&#x3C;head>\u003C/code> est le territoire du SSR. Le dropdown de devise peut transformer le DOM visible — pas les metas.\u003C/p>\n\u003Cp>Un monitoring de rendu type Seogard détecte cette divergence SSR/CSR en comparant le HTML brut et le rendu post-JS sur chaque crawl. L'alerte arrive en minutes, pas en trois semaines.\u003C/p>\n\u003Cp>La devise dans le title, c'est un détail. Perdre 18 700 clics en 21 jours parce qu'un \u003Ccode>document.title =\u003C/code> a glissé dans un composant Vue, c'est un incident.\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,11,[18,19,20,21,22],"multi currency","csr","title","spa","i18n","Multi-currency dropdown réécrit le title côté CSR : fix","Fri Jun 19 2026 16:01:36 GMT+0000 (Coordinated Universal Time)",[26,43,57,70,84,98],{"_id":27,"slug":28,"__v":6,"author":7,"canonical":29,"category":10,"createdAt":30,"date":31,"description":32,"image":15,"imageAlt":15,"readingTime":33,"tags":34,"title":40,"updatedAt":41,"categoryLegacy":42},"6a317369aa6b273b0cb03aa2","strapi-public-role-bloque-l-api-lecture-ssr-void-googlebot-voit-page-blanche","https://seogard.io/blog/strapi-public-role-bloque-l-api-lecture-ssr-void-googlebot-voit-page-blanche","2026-06-16T16:01:45.497Z","2026-06-16","Un seed Strapi écrase le rôle Public. L'API renvoie 403, le SSR sert du vide. Récit complet : diagnostic, fix, récupération SEO en 19 jours.",12,[35,36,37,38,39],"strapi","permissions","ssr","api","headless-cms","Strapi public role 403 : SSR vide, Googlebot indexe du blanc","Tue Jun 16 2026 16:01:45 GMT+0000 (Coordinated Universal Time)","Headless",{"_id":44,"slug":45,"__v":6,"author":7,"canonical":46,"category":10,"createdAt":47,"date":48,"description":49,"image":15,"imageAlt":15,"readingTime":33,"tags":50,"title":54,"updatedAt":55,"categoryLegacy":56},"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.",[51,52,37,20,53],"tanstack router","react","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)","Framework",{"_id":58,"slug":59,"__v":6,"author":7,"canonical":60,"category":10,"createdAt":61,"date":62,"description":63,"image":15,"imageAlt":15,"readingTime":33,"tags":64,"title":68,"updatedAt":69,"categoryLegacy":56},"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.",[65,66,67,37],"remix","meta","streaming","Remix meta() async : metas vides en streaming SSR","Thu Jun 11 2026 16:01:48 GMT+0000 (Coordinated Universal Time)",{"_id":71,"slug":72,"__v":6,"author":7,"canonical":73,"category":10,"createdAt":74,"date":75,"description":76,"image":15,"imageAlt":15,"readingTime":33,"tags":77,"title":81,"updatedAt":82,"categoryLegacy":83},"6a23b7d0aa6b273b0c6c840f","splash-screen-noscript-mal-place-qui-contient-le-vrai-contenu-pour-googlebot-sansjs","https://seogard.io/blog/splash-screen-noscript-mal-place-qui-contient-le-vrai-contenu-pour-googlebot-sansjs","2026-06-06T06:01:52.615Z","2026-06-06","Un e-commerce SPA cache son contenu dans une balise noscript pour les bots. Google détecte du cloaking. Récit, diagnostic et fix complet.",[78,79,21,80],"noscript","cloaking","splash","noscript cloaking : splash screen SPA piège Google","Sat Jun 06 2026 06:01:52 GMT+0000 (Coordinated Universal Time)","Rendering",{"_id":85,"slug":86,"__v":6,"author":7,"canonical":87,"category":10,"createdAt":88,"date":89,"description":90,"image":15,"imageAlt":15,"readingTime":33,"tags":91,"title":95,"updatedAt":96,"categoryLegacy":97},"6a141e10aa6b273b0c8a4eb7","react-17-vers-react-18-suspense-ssr-fait-crasher-next-head-en-streaming","https://seogard.io/blog/react-17-vers-react-18-suspense-ssr-fait-crasher-next-head-en-streaming","2026-05-25T10:01:52.337Z","2026-05-25","Migration React 17→18 : le streaming SSR réordonne les chunks et supprime les meta tags. Récit d'incident, diagnostic complet et patch Next.js.",[92,93,37,94,67],"react 18","suspense","next/head","React 18 Suspense SSR : next/head cassé par le streaming","Mon May 25 2026 10:01:52 GMT+0000 (Coordinated Universal Time)","Migration",{"_id":99,"slug":100,"__v":6,"author":7,"canonical":101,"category":10,"createdAt":102,"date":89,"description":103,"image":15,"imageAlt":15,"readingTime":33,"tags":104,"title":108,"updatedAt":109,"categoryLegacy":97},"6a148e95aa6b273b0ce72f4a","migration-angular-17-vers-ssr-provideserverrendering-mal-configure-et-hydration-mismatch-invisible","https://seogard.io/blog/migration-angular-17-vers-ssr-provideserverrendering-mal-configure-et-hydration-mismatch-invisible","2026-05-25T18:01:57.093Z","Migration Angular 17 vers SSR : provideServerRendering mal configuré cause un hydration mismatch invisible. Récit, diagnostic Lighthouse, fix précis.",[105,37,106,107],"angular 17","hydration","provideServerRendering","Angular 17 SSR : hydration mismatch invisible, −34 % trafic","Mon May 25 2026 18:01:57 GMT+0000 (Coordinated Universal Time)",{"categories":111},[112,116,120,124,127,131,135],{"category":113,"slug":114,"count":115},"Régressions SEO","regressions-seo",137,{"category":117,"slug":118,"count":119},"GEO / IA","geo-ia",98,{"category":121,"slug":122,"count":123},"Indexation & crawl","indexation-crawl",32,{"category":10,"slug":125,"count":126},"ssr-csr",22,{"category":128,"slug":129,"count":130},"Données structurées","donnees-structurees",6,{"category":132,"slug":133,"count":134},"CI/CD SEO","ci-cd-seo",4,{"category":136,"slug":137,"count":138},"Monitoring continu","monitoring-continu",3]