[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$f2R6idiJTJxJO--HSsG2vKvFffHVIYSSK798QbAvDvqI":3,"$fNp-0fST2y8F0arQxqjIT4SlMOV9pn3tS6ELvybBx1vg":25,"$fcTFjTaPPT1sVcnh1wWEGoTWMSaOD4bwmwntkMae6w5s":112},{"_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},"6a30e6c9aa6b273b0c3f8791","sanity-preview-mode-actif-en-prod-indexation-de-drafts-non-publies",0,"Equipe Seogard","# Sanity preview mode actif en production : quand Googlebot indexe vos drafts\n\nMardi 11h. L'équipe éditoriale d'une marketplace mode française — 4 200 fiches produit, 1,8 million de visites organiques par mois — prépare un drop de 47 nouvelles références. Les fiches sont en draft dans Sanity Studio. Aucune n'a été publiée. Pourtant, jeudi matin, une cliente envoie un screenshot au service client. Elle a trouvé sur Google une fiche produit avec un prix barré à \"XXX €\", un titre \"DRAFT — Veste matelassée printemps\", et une photo placeholder grise. La fiche est indexée. Et elle n'est pas la seule.\n\n## Jeudi 9h12 — Le signal d'alarme\n\nLe responsable e-commerce forward le screenshot au lead SEO. Premier réflexe : vérifier manuellement. Une recherche `site:` ciblée sur le nom du produit remonte la page. Le title dans les SERPs affiche le préfixe \"DRAFT —\". Mauvais signe.\n\nLe lead SEO ouvre Search Console. Onglet Pages. Filtre sur les URL contenant `/produit/`. Le nombre de pages indexées a grimpé : 4 547 au lieu des 4 200 attendues. 347 pages supplémentaires. Toutes apparues dans l'index entre mardi et mercredi — exactement la fenêtre du dernier déploiement.\n\nPremier diagnostic (faux) : un problème de publication accidentelle côté Sanity. L'équipe éditoriale vérifie. Les 47 fiches sont bien en statut draft dans Sanity Studio. Aucune n'a été publiée. Le mystère s'épaissit.\n\nLe lead SEO lance un crawl Screaming Frog en mode Googlebot sur le domaine de production. Résultat : les 347 pages \"fantômes\" répondent en 200. Elles servent du contenu. Et pas n'importe lequel : du contenu draft, avec des champs incomplets, des prix placeholder, des descriptions qui commencent par \"TODO :\".\n\nL'hypothèse bascule vers le frontend. Le site tourne sur Next.js 14 (App Router) avec `next-sanity` pour la couche data. Le dev fullstack senior regarde les variables d'environnement du déploiement Vercel de production. Et là, la ligne qui fait mal :\n\n```\nSANITY_API_READ_TOKEN=skRw7g...XXXX\nNEXT_PUBLIC_SANITY_PREVIEW_ENABLED=true\n```\n\nLe token de preview — celui qui donne accès aux drafts via l'API Sanity — est actif en production. Et la variable `NEXT_PUBLIC_SANITY_PREVIEW_ENABLED` est à `true`. Sur le build de prod.\n\nL'équipe comprend que ce n'est pas un bug mineur. Googlebot crawle le site de production, récupère les drafts comme du contenu légitime, et les indexe. Depuis au moins 48 heures. Potentiellement plus : un audit rapide des logs Vercel montre que la variable est présente depuis le déploiement du 2 juin. Soit 12 jours.\n\n## Le bug : quand le client Sanity de production fetche les drafts\n\nPour comprendre la mécanique, il faut regarder comment `next-sanity` configure le client de requête.\n\n### La configuration du client Sanity\n\nLe fichier `sanity/lib/client.ts` du projet :\n\n```typescript\n// sanity/lib/client.ts\nimport { createClient } from 'next-sanity'\n\nexport const client = createClient({\n  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,\n  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,\n  apiVersion: '2024-06-01',\n  useCdn: !process.env.NEXT_PUBLIC_SANITY_PREVIEW_ENABLED,\n  token: process.env.SANITY_API_READ_TOKEN,\n  perspective: process.env.NEXT_PUBLIC_SANITY_PREVIEW_ENABLED\n    ? 'previewDrafts'\n    : 'published',\n})\n```\n\nLe problème est dans les deux dernières propriétés. Quand `NEXT_PUBLIC_SANITY_PREVIEW_ENABLED` vaut `\"true\"` (et en JavaScript, la string `\"true\"` est truthy) :\n\n1. `useCdn` passe à `false` — le client bypasse le CDN Sanity et tape l'API directement.\n2. `perspective` passe à `'previewDrafts'` — le client retourne les documents en statut draft, en plus des documents publiés. Les drafts écrasent même les versions publiées quand elles existent.\n\nEn développement local, c'est le comportement voulu. Le rédacteur voit ses modifications en temps réel avant publication. En production, c'est une catastrophe.\n\n### Ce que voit le développeur vs ce que voit Googlebot\n\nUn développeur qui ouvre la page produit dans son navigateur ne remarque rien d'anormal sur les fiches déjà publiées. Le draft et la version publiée sont identiques pour les anciens produits (le draft est juste une copie de travail). La page s'affiche correctement.\n\nMais pour les 47 nouvelles fiches — celles qui n'existent qu'en draft — le comportement diverge. Côté Sanity, la requête GROQ standard du projet :\n\n```groq\n*[_type == \"product\" && slug.current == $slug][0]{\n  title,\n  price,\n  description,\n  \"imageUrl\": mainImage.asset->url,\n  seoTitle,\n  seoDescription\n}\n```\n\nAvec la perspective `previewDrafts`, cette requête retourne les documents dont l'`_id` commence par `drafts.`. Ces documents n'ont pas de version publiée. Ils existent uniquement dans l'espace draft de Sanity.\n\nLe HTML servi par Next.js pour une de ces fiches fantômes :\n\n```html\n\u003C!DOCTYPE html>\n\u003Chtml lang=\"fr\">\n\u003Chead>\n  \u003Ctitle>DRAFT — Veste matelassée printemps 2026 | BrandName\u003C/title>\n  \u003Cmeta name=\"description\" content=\"TODO : rédiger la description SEO pour ce produit\" />\n  \u003Cmeta name=\"robots\" content=\"index, follow\" />\n  \u003Clink rel=\"canonical\" href=\"https://www.example.com/produit/veste-matelassee-printemps-2026\" />\n\u003C/head>\n\u003Cbody>\n  \u003Ch1>DRAFT — Veste matelassée printemps 2026\u003C/h1>\n  \u003Cspan class=\"price\">XXX €\u003C/span>\n  \u003Cp class=\"description\">Lorem ipsum dolor sit amet, description produit à compléter.\u003C/p>\n  \u003Cimg src=\"https://cdn.sanity.io/images/.../placeholder-gray.png\" alt=\"\" />\n\u003C/body>\n\u003C/html>\n```\n\nPas de `noindex`. Un canonical propre. Un title indexable. Pour Googlebot, c'est une page légitime. Il la crawle, la rend, l'indexe. Les 340 fiches en draft (les 47 nouvelles plus ~293 anciennes fiches en cours de modification éditoriale) sont traitées comme du contenu de production.\n\n### Pourquoi les tests n'ont rien détecté\n\nTrois raisons convergent.\n\n**1. Le pipeline CI ne teste pas les variables d'environnement de production.** Les tests e2e tournent avec un jeu de variables dédié (`.env.test`). La variable `NEXT_PUBLIC_SANITY_PREVIEW_ENABLED` n'y est pas définie — donc les tests passent en mode `published`. Le bug n'existe que dans le contexte de déploiement Vercel.\n\n**2. La preview Vercel et la production partagent le même projet.** L'équipe avait configuré les variables d'environnement dans le dashboard Vercel en cochant \"Production\", \"Preview\" et \"Development\" pour le token et le flag preview. Un raccourci pris lors du setup initial, jamais corrigé.\n\n**3. Aucun monitoring du contenu servi côté SEO.** L'équipe surveille les métriques de performance (Core Web Vitals, TTFB), les erreurs 5xx, le taux de conversion. Personne ne vérifie automatiquement que le contenu servi à un crawler correspond au contenu publié dans le CMS. C'est exactement le type de divergence silencieuse qu'on retrouve dans d'autres stacks headless — [comme quand un champ SEO title Contentful n'est pas synchronisé vers Next.js](/blog/contentful-le-champ-seo-title-non-sync-vers-next-js-fallback-first-h1-genere).\n\n### L'ampleur réelle du dégât\n\nLe lead SEO extrait les données Search Console sur 14 jours. Sur les 340 pages indexées par erreur :\n\n- 127 affichent des titles avec le préfixe \"DRAFT —\".\n- 89 ont des meta descriptions contenant \"TODO\" ou \"Lorem ipsum\".\n- 34 ont des prix affichés comme \"XXX €\" ou \"0 €\".\n- Le tout génère 2 300 impressions parasites et 180 clics vers des pages au contenu incohérent.\n\nPlus grave : les versions draft des fiches existantes (les 293) écrasent les versions publiées. Certaines fiches à fort trafic servent maintenant un title de brouillon au lieu du title optimisé. Le CTR moyen de ces pages chute de 4,2 % à 2,8 % sur la période.\n\nCôté maillage, 12 fiches draft contiennent des liens internes vers des catégories pas encore créées — des 404. Googlebot les suit, les enregistre. Le rapport de couverture Search Console commence à remonter des erreurs soft 404.\n\n## Le fix : trois étapes, zéro compromis\n\n### Étape 1 — Isoler les variables d'environnement\n\nDans le dashboard Vercel, les variables sont reconfigurées immédiatement :\n\n- `SANITY_API_READ_TOKEN` : décoché de \"Production\". Reste uniquement sur \"Preview\" et \"Development\".\n- `NEXT_PUBLIC_SANITY_PREVIEW_ENABLED` : supprimé de \"Production\". Reste sur \"Preview\" uniquement.\n\nMais ça ne suffit pas. Le code du client Sanity repose sur une logique conditionnelle fragile. Si une variable est `undefined`, le comportement par défaut doit être sûr. Le client est refactorisé :\n\n```typescript\n// sanity/lib/client.ts — version corrigée\nimport { createClient } from 'next-sanity'\n\nconst isPreview =\n  process.env.NEXT_PUBLIC_SANITY_PREVIEW_ENABLED === 'true' &&\n  process.env.NODE_ENV !== 'production'\n\nexport const client = createClient({\n  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,\n  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,\n  apiVersion: '2024-06-01',\n  useCdn: !isPreview,\n  token: isPreview ? process.env.SANITY_API_READ_TOKEN : undefined,\n  perspective: isPreview ? 'previewDrafts' : 'published',\n})\n```\n\nLa double condition — flag explicite ET environnement non-production — empêche toute fuite accidentelle du mode preview en production. Si le token est `undefined`, l'API Sanity refuse les requêtes authentifiées et ne retourne que le contenu publié. C'est un fail-safe.\n\n### Étape 2 — Purger les pages indexées\n\nLe redéploiement est déclenché. Les 340 pages qui n'existent qu'en draft retournent maintenant un 404 (le client en perspective `published` ne les trouve plus). Pour les 293 fiches existantes dont le contenu draft écrasait le contenu publié, la version publiée est à nouveau servie.\n\nPour accélérer la désindexation des 47 pages fantômes, l'équipe soumet un batch de demandes de suppression via l'API Search Console :\n\n```bash\n# Soumettre une demande de suppression d'URL via l'API Indexing\n# (les 47 URLs sont listées dans urls-to-remove.txt)\nwhile IFS= read -r url; do\n  curl -s -X POST \\\n    \"https://searchconsole.googleapis.com/webmasters/v3/sites/https%3A%2F%2Fwww.example.com/urlRemovals\" \\\n    -H \"Authorization: Bearer $(gcloud auth print-access-token)\" \\\n    -H \"Content-Type: application/json\" \\\n    -d \"{\\\"siteUrl\\\": \\\"https://www.example.com\\\", \\\"inspectionUrl\\\": \\\"$url\\\"}\" \\\n    | jq '.inspectionResult.indexStatusResult.coverageState'\ndone \u003C urls-to-remove.txt\n```\n\nEn parallèle, l'outil de suppression temporaire de Search Console est utilisé pour les 12 URLs les plus visibles (celles qui apparaissent dans les SERPs avec \"DRAFT\" dans le title).\n\n### Étape 3 — Mettre en place un garde-fou durable\n\nL'équipe ajoute un middleware Next.js qui bloque toute réponse contenant des marqueurs de draft :\n\n```typescript\n// middleware.ts\nimport { NextResponse } from 'next/server'\nimport type { NextRequest } from 'next/server'\n\nexport function middleware(request: NextRequest) {\n  const response = NextResponse.next()\n\n  // En production, vérifier que le mode preview n'est pas actif\n  if (\n    process.env.NODE_ENV === 'production' &&\n    process.env.NEXT_PUBLIC_SANITY_PREVIEW_ENABLED === 'true'\n  ) {\n    // Logger l'alerte et servir quand même (ne pas casser le site)\n    // mais envoyer une alerte Slack\n    console.error(\n      '[SEO-GUARD] CRITICAL: Sanity preview mode is active in production'\n    )\n    // Webhook Slack / PagerDuty\n    fetch(process.env.ALERT_WEBHOOK_URL!, {\n      method: 'POST',\n      body: JSON.stringify({\n        text: '🚨 Sanity preview mode is active on PRODUCTION. Drafts may be served to Googlebot.',\n      }),\n    }).catch(() => {})\n  }\n\n  return response\n}\n\nexport const config = {\n  matcher: ['/produit/:path*', '/categorie/:path*'],\n}\n```\n\nCe middleware ne bloque pas le rendu — casser le site serait pire. Mais il déclenche une alerte immédiate si la variable fuit à nouveau en production. L'équipe ajoute aussi un test dans le pipeline CI :\n\n```yaml\n# .github/workflows/deploy.yml — extrait\n- name: Verify production env safety\n  if: github.ref == 'refs/heads/main'\n  run: |\n    if grep -q 'NEXT_PUBLIC_SANITY_PREVIEW_ENABLED=true' .env.production 2>/dev/null; then\n      echo \"ERROR: Preview mode enabled in production env file\"\n      exit 1\n    fi\n    echo \"✅ Production env is clean — no preview mode\"\n```\n\n### La récupération\n\nLes 47 pages fantômes sont désindexées en 4 à 6 jours. Les demandes de suppression accélèrent le processus pour les plus visibles (24-48 heures).\n\nPour les 293 fiches existantes qui servaient du contenu draft, la récupération est plus lente. Google doit re-crawler les pages, constater que le contenu a changé, et réévaluer le ranking. Le CTR des pages affectées remonte progressivement :\n\n- J+3 : 3,1 % (vs 2,8 % au creux).\n- J+10 : 3,8 %.\n- J+21 : retour à 4,1 %, proche du niveau pré-incident.\n\nLe trafic organique global du site accuse une baisse de 6 % sur la période (environ −108K clics sur 21 jours), en partie due à la dilution de crawl budget causée par les 340 pages parasites. Ce phénomène de dilution est similaire à ce qu'on observe quand [un A/B test sert un noindex à 50 % du trafic](/blog/a-b-test-header-la-variante-b-sert-un-noindex-a-50-du-trafic-pendant-9-jours) — le crawler gaspille des ressources sur des pages qui ne devraient pas exister.\n\n## Ce qu'on en retient\n\nLes architectures headless créent un espace entre le CMS et le rendu. Cet espace est fertile pour les régressions SEO silencieuses. Un flag de preview, une variable d'environnement mal scopée, un token qui fuit — et Googlebot voit un site que personne dans l'équipe ne voit.\n\nLa règle : en production, le client CMS ne doit jamais avoir accès aux drafts. Pas par convention. Par contrainte technique. Un token absent. Un `NODE_ENV` check. Un test CI qui casse le build.\n\nEt si le bug passe quand même ? Un monitoring continu type Seogard détecte la divergence entre contenu CMS publié et contenu servi au crawler en quelques minutes — pas en douze jours.\n\nLa preview, c'est pour les humains. Pas pour les bots. Quand la frontière saute, l'index se remplit de brouillons. Et les brouillons, Google les traite comme des pages. Avec tout ce que ça implique.\n```","https://seogard.io/blog/sanity-preview-mode-actif-en-prod-indexation-de-drafts-non-publies","Headless","2026-06-16T06:01:45.128Z","2026-06-16","Un site Next.js + Sanity garde la preview API key en production. Googlebot indexe 340 drafts non publiés. Récit, diagnostic et fix complet.","\u003Ch1>Sanity preview mode actif en production : quand Googlebot indexe vos drafts\u003C/h1>\n\u003Cp>Mardi 11h. L'équipe éditoriale d'une marketplace mode française — 4 200 fiches produit, 1,8 million de visites organiques par mois — prépare un drop de 47 nouvelles références. Les fiches sont en draft dans Sanity Studio. Aucune n'a été publiée. Pourtant, jeudi matin, une cliente envoie un screenshot au service client. Elle a trouvé sur Google une fiche produit avec un prix barré à \"XXX €\", un titre \"DRAFT — Veste matelassée printemps\", et une photo placeholder grise. La fiche est indexée. Et elle n'est pas la seule.\u003C/p>\n\u003Ch2>Jeudi 9h12 — Le signal d'alarme\u003C/h2>\n\u003Cp>Le responsable e-commerce forward le screenshot au lead SEO. Premier réflexe : vérifier manuellement. Une recherche \u003Ccode>site:\u003C/code> ciblée sur le nom du produit remonte la page. Le title dans les SERPs affiche le préfixe \"DRAFT —\". Mauvais signe.\u003C/p>\n\u003Cp>Le lead SEO ouvre Search Console. Onglet Pages. Filtre sur les URL contenant \u003Ccode>/produit/\u003C/code>. Le nombre de pages indexées a grimpé : 4 547 au lieu des 4 200 attendues. 347 pages supplémentaires. Toutes apparues dans l'index entre mardi et mercredi — exactement la fenêtre du dernier déploiement.\u003C/p>\n\u003Cp>Premier diagnostic (faux) : un problème de publication accidentelle côté Sanity. L'équipe éditoriale vérifie. Les 47 fiches sont bien en statut draft dans Sanity Studio. Aucune n'a été publiée. Le mystère s'épaissit.\u003C/p>\n\u003Cp>Le lead SEO lance un crawl Screaming Frog en mode Googlebot sur le domaine de production. Résultat : les 347 pages \"fantômes\" répondent en 200. Elles servent du contenu. Et pas n'importe lequel : du contenu draft, avec des champs incomplets, des prix placeholder, des descriptions qui commencent par \"TODO :\".\u003C/p>\n\u003Cp>L'hypothèse bascule vers le frontend. Le site tourne sur Next.js 14 (App Router) avec \u003Ccode>next-sanity\u003C/code> pour la couche data. Le dev fullstack senior regarde les variables d'environnement du déploiement Vercel de production. Et là, la ligne qui fait mal :\u003C/p>\n\u003Cpre>\u003Ccode>SANITY_API_READ_TOKEN=skRw7g...XXXX\nNEXT_PUBLIC_SANITY_PREVIEW_ENABLED=true\n\u003C/code>\u003C/pre>\n\u003Cp>Le token de preview — celui qui donne accès aux drafts via l'API Sanity — est actif en production. Et la variable \u003Ccode>NEXT_PUBLIC_SANITY_PREVIEW_ENABLED\u003C/code> est à \u003Ccode>true\u003C/code>. Sur le build de prod.\u003C/p>\n\u003Cp>L'équipe comprend que ce n'est pas un bug mineur. Googlebot crawle le site de production, récupère les drafts comme du contenu légitime, et les indexe. Depuis au moins 48 heures. Potentiellement plus : un audit rapide des logs Vercel montre que la variable est présente depuis le déploiement du 2 juin. Soit 12 jours.\u003C/p>\n\u003Ch2>Le bug : quand le client Sanity de production fetche les drafts\u003C/h2>\n\u003Cp>Pour comprendre la mécanique, il faut regarder comment \u003Ccode>next-sanity\u003C/code> configure le client de requête.\u003C/p>\n\u003Ch3>La configuration du client Sanity\u003C/h3>\n\u003Cp>Le fichier \u003Ccode>sanity/lib/client.ts\u003C/code> du projet :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// sanity/lib/client.ts\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { createClient } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'next-sanity'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> client\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> createClient\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  projectId: process.env.\u003C/span>\u003Cspan style=\"color:#79B8FF\">NEXT_PUBLIC_SANITY_PROJECT_ID\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  dataset: process.env.\u003C/span>\u003Cspan style=\"color:#79B8FF\">NEXT_PUBLIC_SANITY_DATASET\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  apiVersion: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'2024-06-01'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  useCdn: \u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">process.env.\u003C/span>\u003Cspan style=\"color:#79B8FF\">NEXT_PUBLIC_SANITY_PREVIEW_ENABLED\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  token: process.env.\u003C/span>\u003Cspan style=\"color:#79B8FF\">SANITY_API_READ_TOKEN\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  perspective: process.env.\u003C/span>\u003Cspan style=\"color:#79B8FF\">NEXT_PUBLIC_SANITY_PREVIEW_ENABLED\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    ?\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'previewDrafts'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    :\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'published'\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\u003Cp>Le problème est dans les deux dernières propriétés. Quand \u003Ccode>NEXT_PUBLIC_SANITY_PREVIEW_ENABLED\u003C/code> vaut \u003Ccode>\"true\"\u003C/code> (et en JavaScript, la string \u003Ccode>\"true\"\u003C/code> est truthy) :\u003C/p>\n\u003Col>\n\u003Cli>\u003Ccode>useCdn\u003C/code> passe à \u003Ccode>false\u003C/code> — le client bypasse le CDN Sanity et tape l'API directement.\u003C/li>\n\u003Cli>\u003Ccode>perspective\u003C/code> passe à \u003Ccode>'previewDrafts'\u003C/code> — le client retourne les documents en statut draft, en plus des documents publiés. Les drafts écrasent même les versions publiées quand elles existent.\u003C/li>\n\u003C/ol>\n\u003Cp>En développement local, c'est le comportement voulu. Le rédacteur voit ses modifications en temps réel avant publication. En production, c'est une catastrophe.\u003C/p>\n\u003Ch3>Ce que voit le développeur vs ce que voit Googlebot\u003C/h3>\n\u003Cp>Un développeur qui ouvre la page produit dans son navigateur ne remarque rien d'anormal sur les fiches déjà publiées. Le draft et la version publiée sont identiques pour les anciens produits (le draft est juste une copie de travail). La page s'affiche correctement.\u003C/p>\n\u003Cp>Mais pour les 47 nouvelles fiches — celles qui n'existent qu'en draft — le comportement diverge. Côté Sanity, la requête GROQ standard du projet :\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-groq\">*[_type == \"product\" &#x26;&#x26; slug.current == $slug][0]{\n  title,\n  price,\n  description,\n  \"imageUrl\": mainImage.asset->url,\n  seoTitle,\n  seoDescription\n}\n\u003C/code>\u003C/pre>\n\u003Cp>Avec la perspective \u003Ccode>previewDrafts\u003C/code>, cette requête retourne les documents dont l'\u003Ccode>_id\u003C/code> commence par \u003Ccode>drafts.\u003C/code>. Ces documents n'ont pas de version publiée. Ils existent uniquement dans l'espace draft de Sanity.\u003C/p>\n\u003Cp>Le HTML servi par Next.js pour une de ces fiches fantômes :\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\">>DRAFT — Veste matelassée printemps 2026 | 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\">\"TODO : rédiger la description SEO pour ce produit\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">meta\u003C/span>\u003Cspan style=\"color:#B392F0\"> name\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"robots\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> content\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"index, follow\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">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://www.example.com/produit/veste-matelassee-printemps-2026\"\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\">h1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>DRAFT — Veste matelassée printemps 2026&#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\">span\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"price\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>XXX €&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">span\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:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"description\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Lorem ipsum dolor sit amet, description produit à compléter.&#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\">img\u003C/span>\u003Cspan style=\"color:#B392F0\"> src\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"https://cdn.sanity.io/images/.../placeholder-gray.png\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> alt\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">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>Pas de \u003Ccode>noindex\u003C/code>. Un canonical propre. Un title indexable. Pour Googlebot, c'est une page légitime. Il la crawle, la rend, l'indexe. Les 340 fiches en draft (les 47 nouvelles plus ~293 anciennes fiches en cours de modification éditoriale) sont traitées comme du contenu de production.\u003C/p>\n\u003Ch3>Pourquoi les tests n'ont rien détecté\u003C/h3>\n\u003Cp>Trois raisons convergent.\u003C/p>\n\u003Cp>\u003Cstrong>1. Le pipeline CI ne teste pas les variables d'environnement de production.\u003C/strong> Les tests e2e tournent avec un jeu de variables dédié (\u003Ccode>.env.test\u003C/code>). La variable \u003Ccode>NEXT_PUBLIC_SANITY_PREVIEW_ENABLED\u003C/code> n'y est pas définie — donc les tests passent en mode \u003Ccode>published\u003C/code>. Le bug n'existe que dans le contexte de déploiement Vercel.\u003C/p>\n\u003Cp>\u003Cstrong>2. La preview Vercel et la production partagent le même projet.\u003C/strong> L'équipe avait configuré les variables d'environnement dans le dashboard Vercel en cochant \"Production\", \"Preview\" et \"Development\" pour le token et le flag preview. Un raccourci pris lors du setup initial, jamais corrigé.\u003C/p>\n\u003Cp>\u003Cstrong>3. Aucun monitoring du contenu servi côté SEO.\u003C/strong> L'équipe surveille les métriques de performance (Core Web Vitals, TTFB), les erreurs 5xx, le taux de conversion. Personne ne vérifie automatiquement que le contenu servi à un crawler correspond au contenu publié dans le CMS. C'est exactement le type de divergence silencieuse qu'on retrouve dans d'autres stacks headless — \u003Ca href=\"/blog/contentful-le-champ-seo-title-non-sync-vers-next-js-fallback-first-h1-genere\">comme quand un champ SEO title Contentful n'est pas synchronisé vers Next.js\u003C/a>.\u003C/p>\n\u003Ch3>L'ampleur réelle du dégât\u003C/h3>\n\u003Cp>Le lead SEO extrait les données Search Console sur 14 jours. Sur les 340 pages indexées par erreur :\u003C/p>\n\u003Cul>\n\u003Cli>127 affichent des titles avec le préfixe \"DRAFT —\".\u003C/li>\n\u003Cli>89 ont des meta descriptions contenant \"TODO\" ou \"Lorem ipsum\".\u003C/li>\n\u003Cli>34 ont des prix affichés comme \"XXX €\" ou \"0 €\".\u003C/li>\n\u003Cli>Le tout génère 2 300 impressions parasites et 180 clics vers des pages au contenu incohérent.\u003C/li>\n\u003C/ul>\n\u003Cp>Plus grave : les versions draft des fiches existantes (les 293) écrasent les versions publiées. Certaines fiches à fort trafic servent maintenant un title de brouillon au lieu du title optimisé. Le CTR moyen de ces pages chute de 4,2 % à 2,8 % sur la période.\u003C/p>\n\u003Cp>Côté maillage, 12 fiches draft contiennent des liens internes vers des catégories pas encore créées — des 404. Googlebot les suit, les enregistre. Le rapport de couverture Search Console commence à remonter des erreurs soft 404.\u003C/p>\n\u003Ch2>Le fix : trois étapes, zéro compromis\u003C/h2>\n\u003Ch3>Étape 1 — Isoler les variables d'environnement\u003C/h3>\n\u003Cp>Dans le dashboard Vercel, les variables sont reconfigurées immédiatement :\u003C/p>\n\u003Cul>\n\u003Cli>\u003Ccode>SANITY_API_READ_TOKEN\u003C/code> : décoché de \"Production\". Reste uniquement sur \"Preview\" et \"Development\".\u003C/li>\n\u003Cli>\u003Ccode>NEXT_PUBLIC_SANITY_PREVIEW_ENABLED\u003C/code> : supprimé de \"Production\". Reste sur \"Preview\" uniquement.\u003C/li>\n\u003C/ul>\n\u003Cp>Mais ça ne suffit pas. Le code du client Sanity repose sur une logique conditionnelle fragile. Si une variable est \u003Ccode>undefined\u003C/code>, le comportement par défaut doit être sûr. Le client est refactorisé :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// sanity/lib/client.ts — version corrigée\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { createClient } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'next-sanity'\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\"> isPreview\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  process.env.\u003C/span>\u003Cspan style=\"color:#79B8FF\">NEXT_PUBLIC_SANITY_PREVIEW_ENABLED\u003C/span>\u003Cspan style=\"color:#F97583\"> ===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'true'\u003C/span>\u003Cspan style=\"color:#F97583\"> &#x26;&#x26;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  process.env.\u003C/span>\u003Cspan style=\"color:#79B8FF\">NODE_ENV\u003C/span>\u003Cspan style=\"color:#F97583\"> !==\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'production'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> client\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> createClient\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  projectId: process.env.\u003C/span>\u003Cspan style=\"color:#79B8FF\">NEXT_PUBLIC_SANITY_PROJECT_ID\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  dataset: process.env.\u003C/span>\u003Cspan style=\"color:#79B8FF\">NEXT_PUBLIC_SANITY_DATASET\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  apiVersion: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'2024-06-01'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  useCdn: \u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">isPreview,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  token: isPreview \u003C/span>\u003Cspan style=\"color:#F97583\">?\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> process.env.\u003C/span>\u003Cspan style=\"color:#79B8FF\">SANITY_API_READ_TOKEN\u003C/span>\u003Cspan style=\"color:#F97583\"> :\u003C/span>\u003Cspan style=\"color:#79B8FF\"> undefined\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  perspective: isPreview \u003C/span>\u003Cspan style=\"color:#F97583\">?\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'previewDrafts'\u003C/span>\u003Cspan style=\"color:#F97583\"> :\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'published'\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\u003Cp>La double condition — flag explicite ET environnement non-production — empêche toute fuite accidentelle du mode preview en production. Si le token est \u003Ccode>undefined\u003C/code>, l'API Sanity refuse les requêtes authentifiées et ne retourne que le contenu publié. C'est un fail-safe.\u003C/p>\n\u003Ch3>Étape 2 — Purger les pages indexées\u003C/h3>\n\u003Cp>Le redéploiement est déclenché. Les 340 pages qui n'existent qu'en draft retournent maintenant un 404 (le client en perspective \u003Ccode>published\u003C/code> ne les trouve plus). Pour les 293 fiches existantes dont le contenu draft écrasait le contenu publié, la version publiée est à nouveau servie.\u003C/p>\n\u003Cp>Pour accélérer la désindexation des 47 pages fantômes, l'équipe soumet un batch de demandes de suppression via l'API Search Console :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Soumettre une demande de suppression d'URL via l'API Indexing\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># (les 47 URLs sont listées dans urls-to-remove.txt)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">while\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> IFS\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> read\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -r\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> url\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; \u003C/span>\u003Cspan style=\"color:#F97583\">do\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -s\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -X\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> POST\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"https://searchconsole.googleapis.com/webmasters/v3/sites/https%3A%2F%2Fwww.example.com/urlRemovals\"\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 $(\u003C/span>\u003Cspan style=\"color:#B392F0\">gcloud\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> auth print-access-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\">    -d\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"{\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\">siteUrl\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\">https://www.example.com\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\">inspectionUrl\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$url\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    |\u003C/span>\u003Cspan style=\"color:#B392F0\"> jq\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '.inspectionResult.indexStatusResult.coverageState'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">done\u003C/span>\u003Cspan style=\"color:#F97583\"> &#x3C;\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> urls-to-remove.txt\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>En parallèle, l'outil de suppression temporaire de Search Console est utilisé pour les 12 URLs les plus visibles (celles qui apparaissent dans les SERPs avec \"DRAFT\" dans le title).\u003C/p>\n\u003Ch3>Étape 3 — Mettre en place un garde-fou durable\u003C/h3>\n\u003Cp>L'équipe ajoute un middleware Next.js qui bloque toute réponse contenant des marqueurs de draft :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// middleware.ts\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { NextResponse } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'next/server'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#F97583\"> type\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { NextRequest } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'next/server'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> function\u003C/span>\u003Cspan style=\"color:#B392F0\"> middleware\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">request\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> NextRequest\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> response\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> NextResponse.\u003C/span>\u003Cspan style=\"color:#B392F0\">next\u003C/span>\u003Cspan style=\"color:#E1E4E8\">()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  // En production, vérifier que le mode preview n'est pas actif\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    process.env.\u003C/span>\u003Cspan style=\"color:#79B8FF\">NODE_ENV\u003C/span>\u003Cspan style=\"color:#F97583\"> ===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'production'\u003C/span>\u003Cspan style=\"color:#F97583\"> &#x26;&#x26;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    process.env.\u003C/span>\u003Cspan style=\"color:#79B8FF\">NEXT_PUBLIC_SANITY_PREVIEW_ENABLED\u003C/span>\u003Cspan style=\"color:#F97583\"> ===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'true'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  ) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Logger l'alerte et servir quand même (ne pas casser le site)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // mais envoyer une alerte Slack\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    console.\u003C/span>\u003Cspan style=\"color:#B392F0\">error\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">      '[SEO-GUARD] CRITICAL: Sanity preview mode is active in production'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    )\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Webhook Slack / PagerDuty\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(process.env.\u003C/span>\u003Cspan style=\"color:#79B8FF\">ALERT_WEBHOOK_URL\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      method: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'POST'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      body: \u003C/span>\u003Cspan style=\"color:#79B8FF\">JSON\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#B392F0\">stringify\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        text: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'🚨 Sanity preview mode is active on PRODUCTION. Drafts may be served to Googlebot.'\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>\u003Cspan style=\"color:#B392F0\">catch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(() \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {})\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> response\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> config\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  matcher: [\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/produit/:path*'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/categorie/:path*'\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\u003Cp>Ce middleware ne bloque pas le rendu — casser le site serait pire. Mais il déclenche une alerte immédiate si la variable fuit à nouveau en production. L'équipe ajoute aussi un test dans le pipeline CI :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># .github/workflows/deploy.yml — extrait\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">- \u003C/span>\u003Cspan style=\"color:#85E89D\">name\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">Verify production env safety\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">  if\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">github.ref == 'refs/heads/main'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">  run\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#F97583\">|\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    if grep -q 'NEXT_PUBLIC_SANITY_PREVIEW_ENABLED=true' .env.production 2>/dev/null; then\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">      echo \"ERROR: Preview mode enabled in production env file\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">      exit 1\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    fi\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    echo \"✅ Production env is clean — no preview mode\"\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Ch3>La récupération\u003C/h3>\n\u003Cp>Les 47 pages fantômes sont désindexées en 4 à 6 jours. Les demandes de suppression accélèrent le processus pour les plus visibles (24-48 heures).\u003C/p>\n\u003Cp>Pour les 293 fiches existantes qui servaient du contenu draft, la récupération est plus lente. Google doit re-crawler les pages, constater que le contenu a changé, et réévaluer le ranking. Le CTR des pages affectées remonte progressivement :\u003C/p>\n\u003Cul>\n\u003Cli>J+3 : 3,1 % (vs 2,8 % au creux).\u003C/li>\n\u003Cli>J+10 : 3,8 %.\u003C/li>\n\u003Cli>J+21 : retour à 4,1 %, proche du niveau pré-incident.\u003C/li>\n\u003C/ul>\n\u003Cp>Le trafic organique global du site accuse une baisse de 6 % sur la période (environ −108K clics sur 21 jours), en partie due à la dilution de crawl budget causée par les 340 pages parasites. Ce phénomène de dilution est similaire à ce qu'on observe quand \u003Ca href=\"/blog/a-b-test-header-la-variante-b-sert-un-noindex-a-50-du-trafic-pendant-9-jours\">un A/B test sert un noindex à 50 % du trafic\u003C/a> — le crawler gaspille des ressources sur des pages qui ne devraient pas exister.\u003C/p>\n\u003Ch2>Ce qu'on en retient\u003C/h2>\n\u003Cp>Les architectures headless créent un espace entre le CMS et le rendu. Cet espace est fertile pour les régressions SEO silencieuses. Un flag de preview, une variable d'environnement mal scopée, un token qui fuit — et Googlebot voit un site que personne dans l'équipe ne voit.\u003C/p>\n\u003Cp>La règle : en production, le client CMS ne doit jamais avoir accès aux drafts. Pas par convention. Par contrainte technique. Un token absent. Un \u003Ccode>NODE_ENV\u003C/code> check. Un test CI qui casse le build.\u003C/p>\n\u003Cp>Et si le bug passe quand même ? Un monitoring continu type Seogard détecte la divergence entre contenu CMS publié et contenu servi au crawler en quelques minutes — pas en douze jours.\u003C/p>\n\u003Cp>La preview, c'est pour les humains. Pas pour les bots. Quand la frontière saute, l'index se remplit de brouillons. Et les brouillons, Google les traite comme des pages. Avec tout ce que ça implique.\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,11,[18,19,20,21,22],"sanity","preview","drafts","indexation","next.js","Sanity preview mode en prod : drafts indexés par Google","Tue Jun 16 2026 06:01:45 GMT+0000 (Coordinated Universal Time)",[26,40,55,70,84,100],{"_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":38,"updatedAt":39},"6a2f9542aa6b273b0c30f3ec","contentful-le-champ-seo-title-non-sync-vers-next-js-fallback-first-h1-genere","https://seogard.io/blog/contentful-le-champ-seo-title-non-sync-vers-next-js-fallback-first-h1-genere","2026-06-15T06:01:38.446Z","2026-06-15","Un champ SEO title Contentful non mappé dans Next.js génère un fallback H1 identique sur 1 200 variantes produit. Récit, diagnostic, fix.",12,[35,36,22,37],"contentful","headless","mapping","Contentful + Next.js : title manquant, fallback H1 sur 1 200 pages","Mon Jun 15 2026 06:01:38 GMT+0000 (Coordinated Universal Time)",{"_id":41,"slug":42,"__v":6,"author":7,"canonical":43,"category":44,"createdAt":45,"date":31,"description":46,"image":15,"imageAlt":15,"readingTime":33,"tags":47,"title":53,"updatedAt":54},"6a30222eaa6b273b0ca1e7dc","what-ai-overview-click-data-reveals-about-consumer-search-behavior-5-strategic-insights-for-cmos-via-sejournal-gregjarboe","https://seogard.io/blog/what-ai-overview-click-data-reveals-about-consumer-search-behavior-5-strategic-insights-for-cmos-via-sejournal-gregjarboe","Actualités SEO","2026-06-15T16:02:54.519Z","Les utilisateurs quotidiens d'AI Overview cliquent 3.5x plus sur les sources. Analyse technique des données et stratégies d'optimisation concrètes.",[48,49,50,51,52],"AI Overview","click data","search behavior","SGE","structured data","AI Overview Click Data : ce que les clics révèlent vraiment","Mon Jun 15 2026 16:02:54 GMT+0000 (Coordinated Universal Time)",{"_id":56,"slug":57,"__v":6,"author":7,"canonical":58,"category":44,"createdAt":59,"date":60,"description":61,"image":15,"imageAlt":15,"readingTime":33,"tags":62,"title":68,"updatedAt":69},"6a2e441caa6b273b0c22bc85","what-apple-s-gemini-powered-siri-means-for-search-visibility-via-sejournal-mattgsouthern","https://seogard.io/blog/what-apple-s-gemini-powered-siri-means-for-search-visibility-via-sejournal-mattgsouthern","2026-06-14T06:03:08.037Z","2026-06-14","Apple intègre Gemini dans Siri. Analyse technique des conséquences pour le crawl, le rendering, le structured data et la visibilité organique de vos pages.",[63,64,65,66,67],"siri","gemini","apple-intelligence","llm-seo","search-visibility","Siri + Gemini : impact concret sur la visibilité SEO","Sun Jun 14 2026 06:03:08 GMT+0000 (Coordinated Universal Time)",{"_id":71,"slug":72,"__v":6,"author":7,"canonical":73,"category":74,"createdAt":75,"date":60,"description":76,"image":15,"imageAlt":15,"readingTime":33,"tags":77,"title":82,"updatedAt":83},"6a2ed065aa6b273b0c9328d9","rank-math-update-nouveau-format-de-sitemaps-declenche-une-reindexation-complete","https://seogard.io/blog/rank-math-update-nouveau-format-de-sitemaps-declenche-une-reindexation-complete","CMS","2026-06-14T16:01:41.413Z","Un update Rank Math change le format des sitemaps. Google traite chaque URL comme nouvelle. Récit du pic de crawl, de la chute, et du fix.",[78,79,80,81],"rank math","sitemap","wordpress","reindexation","Rank Math sitemap : mise à jour qui force une réindexation","Sun Jun 14 2026 16:01:41 GMT+0000 (Coordinated Universal Time)",{"_id":85,"slug":86,"__v":6,"author":7,"canonical":87,"category":88,"createdAt":89,"date":90,"description":91,"image":15,"imageAlt":15,"readingTime":33,"tags":92,"title":98,"updatedAt":99},"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","Framework","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.",[93,94,95,96,97],"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":101,"slug":102,"__v":6,"author":7,"canonical":103,"category":74,"createdAt":104,"date":90,"description":105,"image":15,"imageAlt":15,"readingTime":33,"tags":106,"title":110,"updatedAt":111},"6a2d7ef3aa6b273b0c80d69d","yoast-seo-desactive-par-un-update-plugin-fallback-meta-vides-sur-80-du-blog","https://seogard.io/blog/yoast-seo-desactive-par-un-update-plugin-fallback-meta-vides-sur-80-du-blog","2026-06-13T16:01:55.662Z","Un update WordPress désactive Yoast SEO sans alerte. 1 200 articles perdent leurs meta en silence. Récit, diagnostic technique et fix complet.",[80,107,108,109],"yoast","plugin","meta","Yoast SEO désactivé par un update : meta vides sur 80% du blog","Sat Jun 13 2026 16:01:55 GMT+0000 (Coordinated Universal Time)",{"categories":113},[114,117,121,125,129,131,135,138,141,145,149,152,155,159,162,165,168,171,175],{"category":44,"slug":115,"count":116},"actualites-seo",168,{"category":118,"slug":119,"count":120},"Migration","migration",18,{"category":122,"slug":123,"count":124},"Rendering","rendering",9,{"category":126,"slug":127,"count":128},"Performance","performance",8,{"category":88,"slug":130,"count":128},"framework",{"category":132,"slug":133,"count":134},"Crawl","crawl",7,{"category":136,"slug":137,"count":134},"SEO Technique","seo-technique",{"category":139,"slug":140,"count":134},"Meta Tags","meta-tags",{"category":142,"slug":143,"count":144},"Architecture","architecture",6,{"category":146,"slug":147,"count":148},"Structured Data","structured-data",5,{"category":150,"slug":151,"count":148},"JavaScript SEO","javascript-seo",{"category":153,"slug":154,"count":148},"Monitoring","monitoring",{"category":156,"slug":157,"count":158},"E-commerce","e-commerce",4,{"category":160,"slug":161,"count":158},"Avancé","avance",{"category":163,"slug":164,"count":158},"Refonte","refonte",{"category":166,"slug":167,"count":158},"Redirections","redirections",{"category":169,"slug":170,"count":158},"Outils","outils",{"category":172,"slug":173,"count":174},"IA & SEO","ia-seo",3,{"category":176,"slug":177,"count":174},"Contenu","contenu"]