[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fpxWK5Tw0eLD8POjZOgAVVqnS9noApw7086VLVSTufgU":3,"$fk4fLhTvc2ov4_h0TNMtpcIOyIfpugwwaKXHVemyfFrc":25,"$fcTFjTaPPT1sVcnh1wWEGoTWMSaOD4bwmwntkMae6w5s":113},{"_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},"6a317369aa6b273b0cb03aa2","strapi-public-role-bloque-l-api-lecture-ssr-void-googlebot-voit-page-blanche",0,"Equipe Seogard","# Un seed Strapi efface le rôle Public : 2 400 pages blanches indexées pendant 19 jours\n\nJeudi 16h42. L'équipe backend d'une marketplace française de mobilier — 2 400 fiches produit, 180 000 visites organiques mensuelles — merge un script de seed censé initialiser les données de staging. Le déploiement passe. Les tests end-to-end passent. Le navigateur affiche les pages normalement. Personne ne remarque que le SSR Next.js, lui, sert désormais du vide. Googlebot non plus ne dit rien. Pas tout de suite.\n\n## Lundi 9h12 — \"Le trafic a planté ce week-end\"\n\nL'alerte vient du channel Slack `#seo-monitoring`. Le lead SEO poste une capture d'écran de Google Search Console : les clics du week-end sont en chute libre. Samedi : −38 %. Dimanche : −51 %. Le lundi matin en temps réel, la tendance continue.\n\nPremière hypothèse : un problème de serveur. L'équipe devops vérifie les uptime monitors. Tout est vert. TTFB stable à 290 ms. Aucun 5xx dans les logs Nginx.\n\nDeuxième hypothèse : une mise à jour d'algorithme Google. Le lead SEO vérifie les trackers habituels — Semrush Sensor, MozCast. Rien de significatif sur la période.\n\nTroisième hypothèse : un problème d'indexation. Le lead ouvre l'outil d'inspection d'URL dans Search Console sur une fiche produit populaire. Le rendu HTML renvoyé par Google montre… une page quasi vide. Le shell de l'application est là — header, footer, layout — mais le contenu produit est absent. Pas de titre produit. Pas de description. Pas de prix. Pas de structured data.\n\nÀ 9h47, le lead reproduit avec `curl` depuis son terminal :\n\n```bash\ncurl -s https://www.example.com/produit/chaise-scandinave-noyer | grep '\u003Ch1'\n```\n\nRésultat : `\u003Ch1 class=\"product-title\">\u003C/h1>`. Le H1 est vide.\n\nIl ouvre la même URL dans Chrome. Le H1 affiche \"Chaise scandinave — Noyer massif\". Le contenu est là, rendu côté client par React après hydratation. Mais le HTML servi par le serveur — celui que Googlebot consomme en priorité — est vide.\n\nLe lead sait ce que ça signifie. Le fetch SSR de Next.js vers l'API Strapi échoue silencieusement. Il ouvre le channel `#backend` : \"Qui a touché à Strapi jeudi ?\"\n\nLe silence dure quatre minutes. Puis le dev backend répond : \"J'ai mergé le seed script pour staging. Mais ça ne devrait pas affecter la prod.\"\n\nÀ 10h03, l'équipe commence à comprendre que le \"ça ne devrait pas\" est le problème.\n\n## Le bug : un seed qui réinitialise les permissions du rôle Public\n\n### Ce qui s'est passé dans le seed\n\nL'équipe utilise Strapi v4 (4.15.x) avec le plugin Users & Permissions. Le script de seed, prévu pour staging, réinitialise les rôles pour garantir un état propre avant les tests. Le problème : le script tourne aussi en production.\n\nVoici le fichier `src/index.ts` de Strapi, dans la fonction `bootstrap` :\n\n```typescript\n// src/index.ts — fonction bootstrap de Strapi\nexport default {\n  async bootstrap({ strapi }) {\n    // Seed initial \"pour staging\" — mais exécuté à chaque démarrage\n    const publicRole = await strapi\n      .query('plugin::users-permissions.role')\n      .findOne({ where: { type: 'public' } });\n\n    if (publicRole) {\n      // Réinitialise TOUTES les permissions du rôle Public\n      await strapi\n        .query('plugin::users-permissions.permission')\n        .deleteMany({ where: { role: publicRole.id } });\n\n      // Recrée uniquement les permissions auth\n      const authActions = [\n        'plugin::users-permissions.auth.callback',\n        'plugin::users-permissions.auth.connect',\n        'plugin::users-permissions.auth.register',\n      ];\n\n      for (const action of authActions) {\n        await strapi.query('plugin::users-permissions.permission').create({\n          data: { action, role: publicRole.id },\n        });\n      }\n    }\n\n    strapi.log.info('Bootstrap seed completed');\n  },\n};\n```\n\nLe script supprime toutes les permissions du rôle Public, puis ne recrée que les permissions d'authentification. Les permissions `find` et `findOne` des content-types — `api::product.product`, `api::category.category`, `api::page.page` — disparaissent.\n\nRésultat : chaque appel API public vers `/api/products`, `/api/categories`, `/api/pages` renvoie un **403 Forbidden**.\n\n### Ce que voit le développeur vs ce que voit Googlebot\n\nLe développeur ouvre `https://www.example.com/produit/chaise-scandinave-noyer` dans Chrome. Voici ce qui se passe :\n\n1. Le serveur Next.js exécute `getServerSideProps`.\n2. `getServerSideProps` fetch `https://strapi.internal/api/products?filters[slug]=chaise-scandinave-noyer`.\n3. Strapi renvoie 403.\n4. Le code Next.js **catch l'erreur et retourne un objet props vide** au lieu de faire un throw :\n\n```typescript\n// pages/produit/[slug].tsx\nexport const getServerSideProps: GetServerSideProps = async ({ params }) => {\n  try {\n    const res = await fetch(\n      `${process.env.STRAPI_URL}/api/products?filters[slug][$eq]=${params.slug}&populate=*`,\n      { headers: { Authorization: `Bearer ${process.env.STRAPI_PUBLIC_TOKEN}` } }\n    );\n    const json = await res.json();\n    return { props: { product: json.data?.[0] ?? null } };\n  } catch (error) {\n    // L'erreur est avalée. Pas de log. Pas de throw. Pas de notFound.\n    return { props: { product: null } };\n  }\n};\n```\n\nProblème supplémentaire : le code utilise un `STRAPI_PUBLIC_TOKEN` — un token API Strapi — mais le rôle associé à ce token est aussi \"Public\" dans la configuration Strapi de cette équipe. Le 403 frappe donc aussi les requêtes authentifiées par token.\n\n5. Le serveur renvoie un HTML 200 avec le layout mais sans données produit.\n6. Le composant React côté client tente un second fetch, cette fois depuis le navigateur du développeur. Ce fetch passe par un BFF (Backend for Frontend) interne qui, lui, utilise un token avec le rôle \"Authenticated\". Ce token a encore ses permissions `find`.\n7. Le contenu s'affiche côté client après hydratation.\n\nLe développeur voit le contenu. Googlebot, lui, reçoit le HTML SSR vide et n'exécute pas le fetch client de la même manière. Même si Googlebot exécute du JavaScript, le BFF interne nécessite un cookie de session que le bot n'a pas.\n\nVoici le HTML que Googlebot reçoit :\n\n```html\n\u003C!DOCTYPE html>\n\u003Chtml lang=\"fr\">\n\u003Chead>\n  \u003Ctitle>Produit | MaisonDeco\u003C/title>\n  \u003Cmeta name=\"description\" content=\"\" />\n  \u003Cmeta name=\"robots\" content=\"index, follow\" />\n\u003C/head>\n\u003Cbody>\n  \u003Cheader>\u003C!-- navigation -->\u003C/header>\n  \u003Cmain>\n    \u003Carticle class=\"product-page\">\n      \u003Ch1 class=\"product-title\">\u003C/h1>\n      \u003Cdiv class=\"product-description\">\u003C/div>\n      \u003Cspan class=\"product-price\">\u003C/span>\n    \u003C/article>\n  \u003C/main>\n  \u003Cfooter>\u003C!-- footer -->\u003C/footer>\n\u003C/body>\n\u003C/html>\n```\n\nUn H1 vide. Une meta description vide. Aucun structured data Product. Le title est le fallback générique du layout. Sur 2 400 fiches produit, c'est la même chose. Et les 87 pages catégories sont dans le même état — elles aussi dépendent de l'API Strapi pour leur contenu.\n\n### Pourquoi les tests n'ont rien détecté\n\nTrois raisons.\n\n**1. Le pipeline CI/CD teste en mode \"Authenticated\".** Les tests Cypress s'exécutent après un login utilisateur. Le rôle Authenticated a toujours ses permissions. Les pages rendent correctement dans le contexte de test.\n\n**2. Le seed n'a pas de garde d'environnement.** Le script `bootstrap` s'exécute à chaque démarrage Strapi, tous environnements confondus. L'équipe n'a jamais ajouté de condition `if (process.env.NODE_ENV === 'staging')`.\n\n**3. Le code SSR avale les erreurs.** Le `try/catch` dans `getServerSideProps` transforme un 403 en `props: { product: null }`. Le serveur renvoie un 200. Les monitors de santé ne voient rien. Aucune alerte. Le problème est similaire à ce qu'on observe quand [un fallback meta vide se substitue au contenu réel](/blog/contentful-le-champ-seo-title-non-sync-vers-next-js-fallback-first-h1-genere) — sauf qu'ici c'est l'intégralité du contenu qui disparaît.\n\n### Vérification : confirmer le 403\n\nL'équipe confirme le diagnostic en interrogeant l'API Strapi directement, sans token :\n\n```bash\n# Requête publique sans authentification\ncurl -s -o /dev/null -w \"%{http_code}\" \\\n  https://strapi.example.com/api/products?filters[slug][$eq]=chaise-scandinave-noyer\n\n# Résultat : 403\n```\n\nPuis avec le token Public :\n\n```bash\ncurl -s -o /dev/null -w \"%{http_code}\" \\\n  -H \"Authorization: Bearer $STRAPI_PUBLIC_TOKEN\" \\\n  https://strapi.example.com/api/products?filters[slug][$eq]=chaise-scandinave-noyer\n\n# Résultat : 403\n```\n\nEt en vérifiant les permissions via l'admin Strapi :\n\n```bash\n# Lister les permissions du rôle Public via l'API admin\ncurl -s -H \"Authorization: Bearer $STRAPI_ADMIN_TOKEN\" \\\n  https://strapi.example.com/api/users-permissions/roles | \\\n  jq '.roles[] | select(.type == \"public\") | .permissions'\n\n# Résultat : seules les 3 permissions auth sont présentes.\n# Aucune permission api::product, api::category, api::page.\n```\n\nLe diagnostic est confirmé. Le seed a nettoyé les permissions `find` et `findOne` de tous les content-types publics.\n\n## Le fix : restaurer, sécuriser, alerter\n\n### Étape 1 — Restaurer les permissions immédiatement\n\nPas question de passer par l'admin UI pour cocher manuellement les cases sur 12 content-types. L'équipe écrit un script de restauration ciblé :\n\n```typescript\n// scripts/restore-public-permissions.ts\nimport Strapi from '@strapi/strapi';\n\nasync function restorePublicPermissions() {\n  const appContext = await Strapi().load();\n  const strapi = appContext;\n\n  const publicRole = await strapi\n    .query('plugin::users-permissions.role')\n    .findOne({ where: { type: 'public' } });\n\n  if (!publicRole) {\n    console.error('Rôle Public introuvable');\n    process.exit(1);\n  }\n\n  // Content-types qui doivent être lisibles publiquement\n  const publicContentTypes = [\n    'api::product.product',\n    'api::category.category',\n    'api::page.page',\n    'api::brand.brand',\n    'api::collection.collection',\n  ];\n\n  const actions = ['find', 'findOne'];\n\n  for (const contentType of publicContentTypes) {\n    for (const action of actions) {\n      const fullAction = `${contentType}.${action}`;\n      const existing = await strapi\n        .query('plugin::users-permissions.permission')\n        .findOne({ where: { action: fullAction, role: publicRole.id } });\n\n      if (!existing) {\n        await strapi\n          .query('plugin::users-permissions.permission')\n          .create({ data: { action: fullAction, role: publicRole.id } });\n        console.log(`✓ Créé : ${fullAction}`);\n      } else {\n        console.log(`— Existe déjà : ${fullAction}`);\n      }\n    }\n  }\n\n  console.log('Restauration terminée');\n  process.exit(0);\n}\n\nrestorePublicPermissions();\n```\n\nLe script est exécuté à 10h31, lundi. Les permissions sont restaurées en 4 secondes.\n\n### Étape 2 — Supprimer le seed destructeur\n\nLe code `bootstrap` est nettoyé. Le `deleteMany` sur les permissions est supprimé. L'équipe ajoute une garde d'environnement stricte pour tout futur seed :\n\n```typescript\n// src/index.ts — corrigé\nexport default {\n  async bootstrap({ strapi }) {\n    if (process.env.NODE_ENV !== 'development') {\n      strapi.log.info('Bootstrap seed skipped (non-dev environment)');\n      return;\n    }\n    // Seed uniquement en développement local\n    // ...\n  },\n};\n```\n\n### Étape 3 — Rendre le SSR résistant aux 403\n\nLe `getServerSideProps` est refactorisé pour ne plus avaler les erreurs :\n\n```typescript\n// pages/produit/[slug].tsx — corrigé\nexport const getServerSideProps: GetServerSideProps = async ({ params, res }) => {\n  const apiRes = await fetch(\n    `${process.env.STRAPI_URL}/api/products?filters[slug][$eq]=${params.slug}&populate=*`,\n    { headers: { Authorization: `Bearer ${process.env.STRAPI_PUBLIC_TOKEN}` } }\n  );\n\n  if (!apiRes.ok) {\n    // Log explicite pour le monitoring\n    console.error(`Strapi API error: ${apiRes.status} for slug ${params.slug}`);\n\n    if (apiRes.status === 403) {\n      // Renvoyer un 503 pour que Googlebot revienne plus tard\n      res.statusCode = 503;\n      res.setHeader('Retry-After', '3600');\n      return { props: { product: null, error: 'PERMISSION_DENIED' } };\n    }\n\n    return { notFound: true };\n  }\n\n  const json = await apiRes.json();\n  const product = json.data?.[0] ?? null;\n\n  if (!product) {\n    return { notFound: true };\n  }\n\n  return { props: { product } };\n};\n```\n\nLe 503 avec `Retry-After` est un choix délibéré. Comme le documente [Google Search Central](https://developers.google.com/search/docs/crawling-indexing/http-network-errors#http-status-codes), un 503 signale à Googlebot que l'indisponibilité est temporaire. Le bot reviendra. Un 200 avec du contenu vide, lui, entraîne une désindexation progressive.\n\nCe pattern d'erreur silencieuse SSR est un classique des architectures headless. L'incident rappelle le cas d'un [preview mode Sanity resté actif en production](/blog/sanity-preview-mode-actif-en-prod-indexation-de-drafts-non-publies) — un autre scénario où le CMS envoie les mauvaises données au SSR sans que personne ne s'en aperçoive.\n\n### Étape 4 — Ajouter un health check API permissions\n\nL'équipe ajoute un test automatisé dans le pipeline de déploiement :\n\n```bash\n#!/bin/bash\n# ci/check-strapi-public-permissions.sh\n\nSTRAPI_URL=\"${STRAPI_URL:-https://strapi.example.com}\"\nENDPOINTS=(\n  \"/api/products?pagination[limit]=1\"\n  \"/api/categories?pagination[limit]=1\"\n  \"/api/pages?pagination[limit]=1\"\n)\n\nfor endpoint in \"${ENDPOINTS[@]}\"; do\n  status=$(curl -s -o /dev/null -w \"%{http_code}\" \"${STRAPI_URL}${endpoint}\")\n  if [ \"$status\" -ne 200 ]; then\n    echo \"ERREUR: ${endpoint} retourne ${status} (attendu 200)\"\n    exit 1\n  fi\n  echo \"OK: ${endpoint} → ${status}\"\ndone\n\necho \"Toutes les permissions publiques sont fonctionnelles\"\n```\n\nCe script est ajouté comme étape post-déploiement dans le pipeline GitHub Actions. Si un endpoint public renvoie autre chose qu'un 200, le déploiement est marqué en échec et une alerte Slack est envoyée.\n\n### Étape 5 — Invalider le cache et demander la réindexation\n\nLe site utilise un CDN Cloudflare. L'équipe purge le cache de toutes les pages produit et catégorie. Puis elle soumet le sitemap dans Search Console pour accélérer le recrawl.\n\nL'équipe lance aussi Screaming Frog sur l'intégralité du site en mode \"JavaScript rendering\" pour vérifier que toutes les pages servent désormais le contenu SSR correct.\n\nRésultat du crawl Screaming Frog post-fix :\n- 2 400 fiches produit : H1 présent, meta description renseignée, structured data Product détecté.\n- 87 pages catégorie : H1 et contenu restaurés.\n- 0 page avec H1 vide.\n\n### La récupération\n\nLa timeline de récupération observée dans Search Console :\n\n- **J+0 (lundi, 10h31)** : fix déployé.\n- **J+1 à J+3** : Googlebot recrawle environ 400 pages/jour. Les résultats mis en cache avec le contenu vide persistent dans l'index.\n- **J+5** : les premières fiches produit retrouvent leur snippet enrichi dans les SERPs.\n- **J+10** : le trafic organique remonte à 72 % du niveau pré-incident.\n- **J+14** : 91 % du trafic récupéré.\n- **J+19** : retour complet au niveau antérieur. Le nombre de pages indexées dans Search Console correspond à nouveau au sitemap.\n\nImpact total estimé : −112 000 clics organiques sur 19 jours. Pour une marketplace dont le trafic organique génère environ 35 % du chiffre d'affaires, le coût est significatif.\n\n## Ce qu'on en retient\n\nUn script de seed sans garde d'environnement est une bombe à retardement. Un `try/catch` qui avale un 403 est un complice silencieux. Les deux combinés transforment un CMS headless en distributeur de pages blanches — sans qu'aucun monitor classique ne bronche.\n\nTrois règles à graver :\n\n1. **Tout seed destructeur doit être conditionné à l'environnement.** `process.env.NODE_ENV` ou une variable dédiée. Sans exception.\n2. **Le SSR ne doit jamais servir un 200 quand la source de données échoue.** Un 503 avec `Retry-After` protège l'index. Un 200 vide le détruit. Le pattern rappelle d'autres cas où [le contenu est invisible au fetch HTTP brut](/blog/lazy-load-du-hero-h1-dans-une-section-v-if-invisible-au-fetch-http-brut) malgré une apparence normale dans le navigateur.\n3. **Les permissions CMS doivent être testées automatiquement après chaque déploiement.** Pas manuellement. Pas \"quand on y pense\".\n\nUn monitoring continu type Seogard détecte ce type de divergence entre rendu navigateur et rendu SSR/bot en quelques minutes — pas trois semaines après dans un graphique Search Console.\n```","https://seogard.io/blog/strapi-public-role-bloque-l-api-lecture-ssr-void-googlebot-voit-page-blanche","Headless","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.","\u003Ch1>Un seed Strapi efface le rôle Public : 2 400 pages blanches indexées pendant 19 jours\u003C/h1>\n\u003Cp>Jeudi 16h42. L'équipe backend d'une marketplace française de mobilier — 2 400 fiches produit, 180 000 visites organiques mensuelles — merge un script de seed censé initialiser les données de staging. Le déploiement passe. Les tests end-to-end passent. Le navigateur affiche les pages normalement. Personne ne remarque que le SSR Next.js, lui, sert désormais du vide. Googlebot non plus ne dit rien. Pas tout de suite.\u003C/p>\n\u003Ch2>Lundi 9h12 — \"Le trafic a planté ce week-end\"\u003C/h2>\n\u003Cp>L'alerte vient du channel Slack \u003Ccode>#seo-monitoring\u003C/code>. Le lead SEO poste une capture d'écran de Google Search Console : les clics du week-end sont en chute libre. Samedi : −38 %. Dimanche : −51 %. Le lundi matin en temps réel, la tendance continue.\u003C/p>\n\u003Cp>Première hypothèse : un problème de serveur. L'équipe devops vérifie les uptime monitors. Tout est vert. TTFB stable à 290 ms. Aucun 5xx dans les logs Nginx.\u003C/p>\n\u003Cp>Deuxième hypothèse : une mise à jour d'algorithme Google. Le lead SEO vérifie les trackers habituels — Semrush Sensor, MozCast. Rien de significatif sur la période.\u003C/p>\n\u003Cp>Troisième hypothèse : un problème d'indexation. Le lead ouvre l'outil d'inspection d'URL dans Search Console sur une fiche produit populaire. Le rendu HTML renvoyé par Google montre… une page quasi vide. Le shell de l'application est là — header, footer, layout — mais le contenu produit est absent. Pas de titre produit. Pas de description. Pas de prix. Pas de structured data.\u003C/p>\n\u003Cp>À 9h47, le lead reproduit avec \u003Ccode>curl\u003C/code> depuis son terminal :\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/produit/chaise-scandinave-noyer\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> grep\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '&#x3C;h1'\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Résultat : \u003Ccode>&#x3C;h1 class=\"product-title\">&#x3C;/h1>\u003C/code>. Le H1 est vide.\u003C/p>\n\u003Cp>Il ouvre la même URL dans Chrome. Le H1 affiche \"Chaise scandinave — Noyer massif\". Le contenu est là, rendu côté client par React après hydratation. Mais le HTML servi par le serveur — celui que Googlebot consomme en priorité — est vide.\u003C/p>\n\u003Cp>Le lead sait ce que ça signifie. Le fetch SSR de Next.js vers l'API Strapi échoue silencieusement. Il ouvre le channel \u003Ccode>#backend\u003C/code> : \"Qui a touché à Strapi jeudi ?\"\u003C/p>\n\u003Cp>Le silence dure quatre minutes. Puis le dev backend répond : \"J'ai mergé le seed script pour staging. Mais ça ne devrait pas affecter la prod.\"\u003C/p>\n\u003Cp>À 10h03, l'équipe commence à comprendre que le \"ça ne devrait pas\" est le problème.\u003C/p>\n\u003Ch2>Le bug : un seed qui réinitialise les permissions du rôle Public\u003C/h2>\n\u003Ch3>Ce qui s'est passé dans le seed\u003C/h3>\n\u003Cp>L'équipe utilise Strapi v4 (4.15.x) avec le plugin Users &#x26; Permissions. Le script de seed, prévu pour staging, réinitialise les rôles pour garantir un état propre avant les tests. Le problème : le script tourne aussi en production.\u003C/p>\n\u003Cp>Voici le fichier \u003Ccode>src/index.ts\u003C/code> de Strapi, dans la fonction \u003Ccode>bootstrap\u003C/code> :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// src/index.ts — fonction bootstrap de Strapi\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> default\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  async\u003C/span>\u003Cspan style=\"color:#B392F0\"> bootstrap\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({ \u003C/span>\u003Cspan style=\"color:#FFAB70\">strapi\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> }) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Seed initial \"pour staging\" — mais exécuté à chaque démarrage\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> publicRole\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> strapi\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      .\u003C/span>\u003Cspan style=\"color:#B392F0\">query\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'plugin::users-permissions.role'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      .\u003C/span>\u003Cspan style=\"color:#B392F0\">findOne\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({ where: { type: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'public'\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (publicRole) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">      // Réinitialise TOUTES les permissions du rôle Public\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> strapi\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        .\u003C/span>\u003Cspan style=\"color:#B392F0\">query\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'plugin::users-permissions.permission'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        .\u003C/span>\u003Cspan style=\"color:#B392F0\">deleteMany\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({ where: { role: publicRole.id } });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">      // Recrée uniquement les permissions auth\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> authActions\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        'plugin::users-permissions.auth.callback'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        'plugin::users-permissions.auth.connect'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        'plugin::users-permissions.auth.register'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      ];\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> action\u003C/span>\u003Cspan style=\"color:#F97583\"> of\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> authActions) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> strapi.\u003C/span>\u003Cspan style=\"color:#B392F0\">query\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'plugin::users-permissions.permission'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">).\u003C/span>\u003Cspan style=\"color:#B392F0\">create\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          data: { action, role: publicRole.id },\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\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    strapi.log.\u003C/span>\u003Cspan style=\"color:#B392F0\">info\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Bootstrap seed completed'\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 script supprime toutes les permissions du rôle Public, puis ne recrée que les permissions d'authentification. Les permissions \u003Ccode>find\u003C/code> et \u003Ccode>findOne\u003C/code> des content-types — \u003Ccode>api::product.product\u003C/code>, \u003Ccode>api::category.category\u003C/code>, \u003Ccode>api::page.page\u003C/code> — disparaissent.\u003C/p>\n\u003Cp>Résultat : chaque appel API public vers \u003Ccode>/api/products\u003C/code>, \u003Ccode>/api/categories\u003C/code>, \u003Ccode>/api/pages\u003C/code> renvoie un \u003Cstrong>403 Forbidden\u003C/strong>.\u003C/p>\n\u003Ch3>Ce que voit le développeur vs ce que voit Googlebot\u003C/h3>\n\u003Cp>Le développeur ouvre \u003Ccode>https://www.example.com/produit/chaise-scandinave-noyer\u003C/code> dans Chrome. Voici ce qui se passe :\u003C/p>\n\u003Col>\n\u003Cli>Le serveur Next.js exécute \u003Ccode>getServerSideProps\u003C/code>.\u003C/li>\n\u003Cli>\u003Ccode>getServerSideProps\u003C/code> fetch \u003Ccode>https://strapi.internal/api/products?filters[slug]=chaise-scandinave-noyer\u003C/code>.\u003C/li>\n\u003Cli>Strapi renvoie 403.\u003C/li>\n\u003Cli>Le code Next.js \u003Cstrong>catch l'erreur et retourne un objet props vide\u003C/strong> au lieu de faire un throw :\u003C/li>\n\u003C/ol>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// pages/produit/[slug].tsx\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> const\u003C/span>\u003Cspan style=\"color:#B392F0\"> getServerSideProps\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> GetServerSideProps\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> async\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ({ \u003C/span>\u003Cspan style=\"color:#FFAB70\">params\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\">  try\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\"> res\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>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">      `${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">process\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">env\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#79B8FF\">STRAPI_URL\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}/api/products?filters[slug][$eq]=${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">params\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}&#x26;populate=*`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      { headers: { Authorization: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">`Bearer ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">process\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">env\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#79B8FF\">STRAPI_PUBLIC_TOKEN\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:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> json\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> res.\u003C/span>\u003Cspan style=\"color:#B392F0\">json\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { props: { product: json.data?.[\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">??\u003C/span>\u003Cspan style=\"color:#79B8FF\"> null\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } };\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  } \u003C/span>\u003Cspan style=\"color:#F97583\">catch\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (error) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // L'erreur est avalée. Pas de log. Pas de throw. Pas de notFound.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { props: { product: \u003C/span>\u003Cspan style=\"color:#79B8FF\">null\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>Problème supplémentaire : le code utilise un \u003Ccode>STRAPI_PUBLIC_TOKEN\u003C/code> — un token API Strapi — mais le rôle associé à ce token est aussi \"Public\" dans la configuration Strapi de cette équipe. Le 403 frappe donc aussi les requêtes authentifiées par token.\u003C/p>\n\u003Col start=\"5\">\n\u003Cli>Le serveur renvoie un HTML 200 avec le layout mais sans données produit.\u003C/li>\n\u003Cli>Le composant React côté client tente un second fetch, cette fois depuis le navigateur du développeur. Ce fetch passe par un BFF (Backend for Frontend) interne qui, lui, utilise un token avec le rôle \"Authenticated\". Ce token a encore ses permissions \u003Ccode>find\u003C/code>.\u003C/li>\n\u003Cli>Le contenu s'affiche côté client après hydratation.\u003C/li>\n\u003C/ol>\n\u003Cp>Le développeur voit le contenu. Googlebot, lui, reçoit le HTML SSR vide et n'exécute pas le fetch client de la même manière. Même si Googlebot exécute du JavaScript, le BFF interne nécessite un cookie de session que le bot n'a pas.\u003C/p>\n\u003Cp>Voici le HTML que Googlebot reçoit :\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\">>Produit | MaisonDeco&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">title\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">meta\u003C/span>\u003Cspan style=\"color:#B392F0\"> name\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"description\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> content\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#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\">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\">header\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003Cspan style=\"color:#6A737D\">&#x3C;!-- navigation -->\u003C/span>\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">header\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\">main\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">article\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"product-page\"\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:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"product-title\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">h1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">div\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"product-description\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">div\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\">\"product-price\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>&#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\">article\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">main\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\">footer\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003Cspan style=\"color:#6A737D\">&#x3C;!-- footer -->\u003C/span>\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">footer\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>Un H1 vide. Une meta description vide. Aucun structured data Product. Le title est le fallback générique du layout. Sur 2 400 fiches produit, c'est la même chose. Et les 87 pages catégories sont dans le même état — elles aussi dépendent de l'API Strapi pour leur contenu.\u003C/p>\n\u003Ch3>Pourquoi les tests n'ont rien détecté\u003C/h3>\n\u003Cp>Trois raisons.\u003C/p>\n\u003Cp>\u003Cstrong>1. Le pipeline CI/CD teste en mode \"Authenticated\".\u003C/strong> Les tests Cypress s'exécutent après un login utilisateur. Le rôle Authenticated a toujours ses permissions. Les pages rendent correctement dans le contexte de test.\u003C/p>\n\u003Cp>\u003Cstrong>2. Le seed n'a pas de garde d'environnement.\u003C/strong> Le script \u003Ccode>bootstrap\u003C/code> s'exécute à chaque démarrage Strapi, tous environnements confondus. L'équipe n'a jamais ajouté de condition \u003Ccode>if (process.env.NODE_ENV === 'staging')\u003C/code>.\u003C/p>\n\u003Cp>\u003Cstrong>3. Le code SSR avale les erreurs.\u003C/strong> Le \u003Ccode>try/catch\u003C/code> dans \u003Ccode>getServerSideProps\u003C/code> transforme un 403 en \u003Ccode>props: { product: null }\u003C/code>. Le serveur renvoie un 200. Les monitors de santé ne voient rien. Aucune alerte. Le problème est similaire à ce qu'on observe quand \u003Ca href=\"/blog/contentful-le-champ-seo-title-non-sync-vers-next-js-fallback-first-h1-genere\">un fallback meta vide se substitue au contenu réel\u003C/a> — sauf qu'ici c'est l'intégralité du contenu qui disparaît.\u003C/p>\n\u003Ch3>Vérification : confirmer le 403\u003C/h3>\n\u003Cp>L'équipe confirme le diagnostic en interrogeant l'API Strapi directement, sans token :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Requête publique sans authentification\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\"> -o\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> /dev/null\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -w\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"%{http_code}\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  https://strapi.example.com/api/products?filters[slug][\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$eq\u003C/span>\u003Cspan style=\"color:#9ECBFF\">]=chaise-scandinave-noyer\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Résultat : 403\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Puis avec le token Public :\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:#79B8FF\"> -o\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> /dev/null\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -w\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"%{http_code}\"\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:#E1E4E8\">$STRAPI_PUBLIC_TOKEN\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  https://strapi.example.com/api/products?filters[slug][\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$eq\u003C/span>\u003Cspan style=\"color:#9ECBFF\">]=chaise-scandinave-noyer\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Résultat : 403\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Et en vérifiant les permissions via l'admin Strapi :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Lister les permissions du rôle Public via l'API admin\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\"> -H\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"Authorization: Bearer \u003C/span>\u003Cspan style=\"color:#E1E4E8\">$STRAPI_ADMIN_TOKEN\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  https://strapi.example.com/api/users-permissions/roles\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  jq\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '.roles[] | select(.type == \"public\") | .permissions'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Résultat : seules les 3 permissions auth sont présentes.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Aucune permission api::product, api::category, api::page.\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le diagnostic est confirmé. Le seed a nettoyé les permissions \u003Ccode>find\u003C/code> et \u003Ccode>findOne\u003C/code> de tous les content-types publics.\u003C/p>\n\u003Ch2>Le fix : restaurer, sécuriser, alerter\u003C/h2>\n\u003Ch3>Étape 1 — Restaurer les permissions immédiatement\u003C/h3>\n\u003Cp>Pas question de passer par l'admin UI pour cocher manuellement les cases sur 12 content-types. L'équipe écrit un script de restauration ciblé :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// scripts/restore-public-permissions.ts\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> Strapi \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '@strapi/strapi'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">async\u003C/span>\u003Cspan style=\"color:#F97583\"> function\u003C/span>\u003Cspan style=\"color:#B392F0\"> restorePublicPermissions\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\"> appContext\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> Strapi\u003C/span>\u003Cspan style=\"color:#E1E4E8\">().\u003C/span>\u003Cspan style=\"color:#B392F0\">load\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\"> strapi\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> appContext;\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\"> publicRole\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> strapi\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    .\u003C/span>\u003Cspan style=\"color:#B392F0\">query\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'plugin::users-permissions.role'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    .\u003C/span>\u003Cspan style=\"color:#B392F0\">findOne\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({ where: { type: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'public'\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">publicRole) {\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>\u003Cspan style=\"color:#9ECBFF\">'Rôle Public introuvable'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    process.\u003C/span>\u003Cspan style=\"color:#B392F0\">exit\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\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:#6A737D\">  // Content-types qui doivent être lisibles publiquement\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> publicContentTypes\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'api::product.product'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'api::category.category'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'api::page.page'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'api::brand.brand'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'api::collection.collection'\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\"> actions\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'find'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'findOne'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">];\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> contentType\u003C/span>\u003Cspan style=\"color:#F97583\"> of\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> publicContentTypes) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> action\u003C/span>\u003Cspan style=\"color:#F97583\"> of\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> actions) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> fullAction\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> `${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">contentType\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}.${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">action\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\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\"> existing\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> strapi\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        .\u003C/span>\u003Cspan style=\"color:#B392F0\">query\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'plugin::users-permissions.permission'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        .\u003C/span>\u003Cspan style=\"color:#B392F0\">findOne\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({ where: { action: fullAction, role: publicRole.id } });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">existing) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> strapi\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          .\u003C/span>\u003Cspan style=\"color:#B392F0\">query\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'plugin::users-permissions.permission'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          .\u003C/span>\u003Cspan style=\"color:#B392F0\">create\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({ data: { action: fullAction, role: publicRole.id } });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        console.\u003C/span>\u003Cspan style=\"color:#B392F0\">log\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`✓ Créé : ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">fullAction\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>\u003Cspan style=\"color:#F97583\">else\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        console.\u003C/span>\u003Cspan style=\"color:#B392F0\">log\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`— Existe déjà : ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">fullAction\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\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  console.\u003C/span>\u003Cspan style=\"color:#B392F0\">log\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Restauration terminée'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  process.\u003C/span>\u003Cspan style=\"color:#B392F0\">exit\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\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\">restorePublicPermissions\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le script est exécuté à 10h31, lundi. Les permissions sont restaurées en 4 secondes.\u003C/p>\n\u003Ch3>Étape 2 — Supprimer le seed destructeur\u003C/h3>\n\u003Cp>Le code \u003Ccode>bootstrap\u003C/code> est nettoyé. Le \u003Ccode>deleteMany\u003C/code> sur les permissions est supprimé. L'équipe ajoute une garde d'environnement stricte pour tout futur seed :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// src/index.ts — corrigé\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> default\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  async\u003C/span>\u003Cspan style=\"color:#B392F0\"> bootstrap\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({ \u003C/span>\u003Cspan style=\"color:#FFAB70\">strapi\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\"> (process.env.\u003C/span>\u003Cspan style=\"color:#79B8FF\">NODE_ENV\u003C/span>\u003Cspan style=\"color:#F97583\"> !==\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'development'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      strapi.log.\u003C/span>\u003Cspan style=\"color:#B392F0\">info\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Bootstrap seed skipped (non-dev environment)'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      return\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Seed uniquement en développement local\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // ...\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\u003Ch3>Étape 3 — Rendre le SSR résistant aux 403\u003C/h3>\n\u003Cp>Le \u003Ccode>getServerSideProps\u003C/code> est refactorisé pour ne plus avaler les erreurs :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// pages/produit/[slug].tsx — corrigé\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> const\u003C/span>\u003Cspan style=\"color:#B392F0\"> getServerSideProps\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> GetServerSideProps\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> async\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ({ \u003C/span>\u003Cspan style=\"color:#FFAB70\">params\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">res\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\"> apiRes\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>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    `${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">process\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">env\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#79B8FF\">STRAPI_URL\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}/api/products?filters[slug][$eq]=${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">params\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}&#x26;populate=*`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    { headers: { Authorization: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">`Bearer ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">process\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">env\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#79B8FF\">STRAPI_PUBLIC_TOKEN\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:#F97583\">  if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">apiRes.ok) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Log explicite pour le monitoring\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>\u003Cspan style=\"color:#9ECBFF\">`Strapi API error: ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">apiRes\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">status\u003C/span>\u003Cspan style=\"color:#9ECBFF\">} for slug ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">params\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (apiRes.status \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 403\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">      // Renvoyer un 503 pour que Googlebot revienne plus tard\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      res.statusCode \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 503\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      res.\u003C/span>\u003Cspan style=\"color:#B392F0\">setHeader\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Retry-After'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'3600'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { props: { product: \u003C/span>\u003Cspan style=\"color:#79B8FF\">null\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, error: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'PERMISSION_DENIED'\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\"> { notFound: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\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\"> json\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> apiRes.\u003C/span>\u003Cspan style=\"color:#B392F0\">json\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:#E1E4E8\"> json.data?.[\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">??\u003C/span>\u003Cspan style=\"color:#79B8FF\"> null\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">product) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { notFound: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\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\"> { props: { product } };\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">};\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le 503 avec \u003Ccode>Retry-After\u003C/code> est un choix délibéré. Comme le documente \u003Ca href=\"https://developers.google.com/search/docs/crawling-indexing/http-network-errors#http-status-codes\">Google Search Central\u003C/a>, un 503 signale à Googlebot que l'indisponibilité est temporaire. Le bot reviendra. Un 200 avec du contenu vide, lui, entraîne une désindexation progressive.\u003C/p>\n\u003Cp>Ce pattern d'erreur silencieuse SSR est un classique des architectures headless. L'incident rappelle le cas d'un \u003Ca href=\"/blog/sanity-preview-mode-actif-en-prod-indexation-de-drafts-non-publies\">preview mode Sanity resté actif en production\u003C/a> — un autre scénario où le CMS envoie les mauvaises données au SSR sans que personne ne s'en aperçoive.\u003C/p>\n\u003Ch3>Étape 4 — Ajouter un health check API permissions\u003C/h3>\n\u003Cp>L'équipe ajoute un test automatisé dans le pipeline de déploiement :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">#!/bin/bash\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># ci/check-strapi-public-permissions.sh\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">STRAPI_URL\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">STRAPI_URL\u003C/span>\u003Cspan style=\"color:#F97583\">:-\u003C/span>\u003Cspan style=\"color:#E1E4E8\">https\u003C/span>\u003Cspan style=\"color:#F97583\">://\u003C/span>\u003Cspan style=\"color:#E1E4E8\">strapi\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">example\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">com\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">ENDPOINTS\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  \"/api/products?pagination[limit]=1\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  \"/api/categories?pagination[limit]=1\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  \"/api/pages?pagination[limit]=1\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> endpoint \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">ENDPOINTS\u003C/span>\u003Cspan style=\"color:#9ECBFF\">[\u003C/span>\u003Cspan style=\"color:#F97583\">@\u003C/span>\u003Cspan style=\"color:#9ECBFF\">]}\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; \u003C/span>\u003Cspan style=\"color:#F97583\">do\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  status\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$(\u003C/span>\u003Cspan style=\"color:#B392F0\">curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -s\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -o\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> /dev/null\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -w\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"%{http_code}\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">STRAPI_URL\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">endpoint\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [ \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$status\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#F97583\"> -ne\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 200\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ]; \u003C/span>\u003Cspan style=\"color:#F97583\">then\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    echo\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"ERREUR: ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">endpoint\u003C/span>\u003Cspan style=\"color:#9ECBFF\">} retourne ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">status\u003C/span>\u003Cspan style=\"color:#9ECBFF\">} (attendu 200)\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    exit\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 1\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  fi\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  echo\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"OK: ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">endpoint\u003C/span>\u003Cspan style=\"color:#9ECBFF\">} → ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">status\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">done\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">echo\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"Toutes les permissions publiques sont fonctionnelles\"\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Ce script est ajouté comme étape post-déploiement dans le pipeline GitHub Actions. Si un endpoint public renvoie autre chose qu'un 200, le déploiement est marqué en échec et une alerte Slack est envoyée.\u003C/p>\n\u003Ch3>Étape 5 — Invalider le cache et demander la réindexation\u003C/h3>\n\u003Cp>Le site utilise un CDN Cloudflare. L'équipe purge le cache de toutes les pages produit et catégorie. Puis elle soumet le sitemap dans Search Console pour accélérer le recrawl.\u003C/p>\n\u003Cp>L'équipe lance aussi Screaming Frog sur l'intégralité du site en mode \"JavaScript rendering\" pour vérifier que toutes les pages servent désormais le contenu SSR correct.\u003C/p>\n\u003Cp>Résultat du crawl Screaming Frog post-fix :\u003C/p>\n\u003Cul>\n\u003Cli>2 400 fiches produit : H1 présent, meta description renseignée, structured data Product détecté.\u003C/li>\n\u003Cli>87 pages catégorie : H1 et contenu restaurés.\u003C/li>\n\u003Cli>0 page avec H1 vide.\u003C/li>\n\u003C/ul>\n\u003Ch3>La récupération\u003C/h3>\n\u003Cp>La timeline de récupération observée dans Search Console :\u003C/p>\n\u003Cul>\n\u003Cli>\u003Cstrong>J+0 (lundi, 10h31)\u003C/strong> : fix déployé.\u003C/li>\n\u003Cli>\u003Cstrong>J+1 à J+3\u003C/strong> : Googlebot recrawle environ 400 pages/jour. Les résultats mis en cache avec le contenu vide persistent dans l'index.\u003C/li>\n\u003Cli>\u003Cstrong>J+5\u003C/strong> : les premières fiches produit retrouvent leur snippet enrichi dans les SERPs.\u003C/li>\n\u003Cli>\u003Cstrong>J+10\u003C/strong> : le trafic organique remonte à 72 % du niveau pré-incident.\u003C/li>\n\u003Cli>\u003Cstrong>J+14\u003C/strong> : 91 % du trafic récupéré.\u003C/li>\n\u003Cli>\u003Cstrong>J+19\u003C/strong> : retour complet au niveau antérieur. Le nombre de pages indexées dans Search Console correspond à nouveau au sitemap.\u003C/li>\n\u003C/ul>\n\u003Cp>Impact total estimé : −112 000 clics organiques sur 19 jours. Pour une marketplace dont le trafic organique génère environ 35 % du chiffre d'affaires, le coût est significatif.\u003C/p>\n\u003Ch2>Ce qu'on en retient\u003C/h2>\n\u003Cp>Un script de seed sans garde d'environnement est une bombe à retardement. Un \u003Ccode>try/catch\u003C/code> qui avale un 403 est un complice silencieux. Les deux combinés transforment un CMS headless en distributeur de pages blanches — sans qu'aucun monitor classique ne bronche.\u003C/p>\n\u003Cp>Trois règles à graver :\u003C/p>\n\u003Col>\n\u003Cli>\u003Cstrong>Tout seed destructeur doit être conditionné à l'environnement.\u003C/strong> \u003Ccode>process.env.NODE_ENV\u003C/code> ou une variable dédiée. Sans exception.\u003C/li>\n\u003Cli>\u003Cstrong>Le SSR ne doit jamais servir un 200 quand la source de données échoue.\u003C/strong> Un 503 avec \u003Ccode>Retry-After\u003C/code> protège l'index. Un 200 vide le détruit. Le pattern rappelle d'autres cas où \u003Ca href=\"/blog/lazy-load-du-hero-h1-dans-une-section-v-if-invisible-au-fetch-http-brut\">le contenu est invisible au fetch HTTP brut\u003C/a> malgré une apparence normale dans le navigateur.\u003C/li>\n\u003Cli>\u003Cstrong>Les permissions CMS doivent être testées automatiquement après chaque déploiement.\u003C/strong> Pas manuellement. Pas \"quand on y pense\".\u003C/li>\n\u003C/ol>\n\u003Cp>Un monitoring continu type Seogard détecte ce type de divergence entre rendu navigateur et rendu SSR/bot en quelques minutes — pas trois semaines après dans un graphique Search Console.\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,12,[18,19,20,21,22],"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)",[26,41,54,69,84,98],{"_id":27,"slug":28,"__v":6,"author":7,"canonical":29,"category":10,"createdAt":30,"date":12,"description":31,"image":15,"imageAlt":15,"readingTime":32,"tags":33,"title":39,"updatedAt":40},"6a30e6c9aa6b273b0c3f8791","sanity-preview-mode-actif-en-prod-indexation-de-drafts-non-publies","https://seogard.io/blog/sanity-preview-mode-actif-en-prod-indexation-de-drafts-non-publies","2026-06-16T06:01:45.128Z","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.",11,[34,35,36,37,38],"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)",{"_id":42,"slug":43,"__v":6,"author":7,"canonical":44,"category":10,"createdAt":45,"date":46,"description":47,"image":15,"imageAlt":15,"readingTime":16,"tags":48,"title":52,"updatedAt":53},"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.",[49,50,38,51],"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":55,"slug":56,"__v":6,"author":7,"canonical":57,"category":58,"createdAt":59,"date":46,"description":60,"image":15,"imageAlt":15,"readingTime":16,"tags":61,"title":67,"updatedAt":68},"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.",[62,63,64,65,66],"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":70,"slug":71,"__v":6,"author":7,"canonical":72,"category":58,"createdAt":73,"date":74,"description":75,"image":15,"imageAlt":15,"readingTime":16,"tags":76,"title":82,"updatedAt":83},"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.",[77,78,79,80,81],"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":85,"slug":86,"__v":6,"author":7,"canonical":87,"category":88,"createdAt":89,"date":74,"description":90,"image":15,"imageAlt":15,"readingTime":16,"tags":91,"title":96,"updatedAt":97},"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.",[92,93,94,95],"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":99,"slug":100,"__v":6,"author":7,"canonical":101,"category":102,"createdAt":103,"date":104,"description":105,"image":15,"imageAlt":15,"readingTime":16,"tags":106,"title":111,"updatedAt":112},"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.",[107,108,20,109,110],"tanstack router","react","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)",{"categories":114},[115,118,122,126,129,132,136,139,142,146,150,153,156,160,163,166,169,172,176,177],{"category":58,"slug":116,"count":117},"actualites-seo",168,{"category":119,"slug":120,"count":121},"Migration","migration",18,{"category":123,"slug":124,"count":125},"Rendering","rendering",9,{"category":102,"slug":127,"count":128},"framework",8,{"category":130,"slug":131,"count":128},"Performance","performance",{"category":133,"slug":134,"count":135},"Crawl","crawl",7,{"category":137,"slug":138,"count":135},"Meta Tags","meta-tags",{"category":140,"slug":141,"count":135},"SEO Technique","seo-technique",{"category":143,"slug":144,"count":145},"Architecture","architecture",6,{"category":147,"slug":148,"count":149},"Monitoring","monitoring",5,{"category":151,"slug":152,"count":149},"JavaScript SEO","javascript-seo",{"category":154,"slug":155,"count":149},"Structured Data","structured-data",{"category":157,"slug":158,"count":159},"Outils","outils",4,{"category":161,"slug":162,"count":159},"Avancé","avance",{"category":164,"slug":165,"count":159},"Redirections","redirections",{"category":167,"slug":168,"count":159},"Refonte","refonte",{"category":170,"slug":171,"count":159},"E-commerce","e-commerce",{"category":173,"slug":174,"count":175},"Contenu","contenu",3,{"category":10,"slug":50,"count":175},{"category":178,"slug":179,"count":175},"IA & SEO","ia-seo"]