[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fGbd-Zr0IMwkRQCdaI6Y7lEDuWUj6gHpU45ZNv7U0re4":3,"$f3Ulha76RpSvsG-sHlxKPf8j6c3TP6Bn5F6cViodT120":24,"$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":22,"updatedAt":23},"6a32c4e8aa6b273b0cbed17b","storyblok-redirections-en-custom-field-oubliees-au-passage-nouveau-plan",0,"Equipe Seogard","# Storyblok : 1 200 redirections custom disparaissent lors d'un upgrade de plan\n\nMardi 14h. L'équipe ops d'un site e-commerce français — 8 400 pages produit, 2,1 millions de sessions mensuelles — finalise la migration de son espace Storyblok du plan Startup vers le plan Business. Le backoffice affiche le nouveau plan. Les composants sont là. Les stories aussi. Personne ne vérifie la table de redirections stockée dans un content type custom. 72 heures plus tard, Search Console remonte 1 247 erreurs 404 sur des URLs qui généraient encore 38 000 clics par mois.\n\n## Mercredi 9h12 — Les premières alertes\n\nL'alerte vient de Slack, pas de Search Console. Un développeur front remarque un pic d'erreurs 404 dans les logs Vercel du middleware de redirection. Il ouvre le dashboard : 847 requêtes 404 en une heure sur des anciennes URLs produit — des slugs de la V2 du site, migrés il y a onze mois.\n\nPremier réflexe : vérifier le middleware Next.js. Le code n'a pas bougé depuis trois semaines. Le fichier `middleware.ts` fetch bien l'API Storyblok au démarrage pour charger la map de redirections. Le problème n'est pas dans le code.\n\nDeuxième réflexe : ouvrir Storyblok. Le content type `redirect-rule` existe toujours dans le schéma. Mais quand l'équipe filtre les stories de type `redirect-rule`, le résultat est vide. Zéro entrée. 1 247 stories de redirection ont disparu.\n\nL'hypothèse initiale est un bug d'affichage. Quelqu'un rafraîchit. Quelqu'un vide le cache du navigateur. Quelqu'un essaie l'API directement :\n\n```bash\ncurl -s \"https://api.storyblok.com/v2/cdn/stories?starts_with=redirects/&token=$STORYBLOK_TOKEN&per_page=25\" | jq '.stories | length'\n```\n\nRéponse : `0`.\n\nLa panique s'installe à 9h34. Le lead SEO ouvre Search Console. La couverture d'indexation montre déjà 312 URLs passées de \"Valide\" à \"Non trouvée (404)\". Google a crawlé une partie des anciennes URLs pendant la nuit. Le reste va suivre.\n\nL'équipe contacte le support Storyblok. En parallèle, quelqu'un vérifie le changelog de l'espace : la migration de plan date de mardi 14h07. Aucune suppression manuelle de story n'apparaît dans les logs d'activité après cette date. Les stories n'ont pas été supprimées — elles n'ont pas été portées.\n\nLe lead dev ouvre l'ancien espace (encore accessible en lecture pendant 48h après downgrade) et confirme : les 1 247 stories `redirect-rule` sont bien là. Dans le nouvel espace Business, elles n'existent pas.\n\nÀ 10h15, le constat est posé : l'export-import de plan n'a pas inclus les stories dont le content type était marqué comme \"nestable\" plutôt que \"content type\". Et `redirect-rule` avait été créé comme nestable component il y a onze mois, avant d'être promu en content type — sans que la conversion soit complète côté Storyblok.\n\n## Le bug : un content type fantôme entre deux mondes\n\nPour comprendre ce qui s'est passé, il faut revenir à l'architecture de redirections mise en place un an plus tôt.\n\n### L'architecture initiale\n\nL'équipe avait créé un composant Storyblok nommé `redirect-rule` avec trois champs :\n\n```json\n{\n  \"name\": \"redirect-rule\",\n  \"schema\": {\n    \"source_path\": {\n      \"type\": \"text\",\n      \"required\": true,\n      \"description\": \"Ancien chemin (ex: /produit/ancien-slug)\"\n    },\n    \"target_path\": {\n      \"type\": \"text\",\n      \"required\": true,\n      \"description\": \"Nouveau chemin (ex: /p/nouveau-slug)\"\n    },\n    \"status_code\": {\n      \"type\": \"option\",\n      \"options\": [\n        { \"name\": \"301\", \"value\": \"301\" },\n        { \"name\": \"302\", \"value\": \"302\" }\n      ],\n      \"default_value\": \"301\"\n    }\n  },\n  \"is_nestable\": false,\n  \"is_root\": true\n}\n```\n\nCe composant avait été créé initialement comme `nestable` (intégrable dans d'autres blocs), puis converti en `root` (utilisable comme content type autonome) via l'interface Storyblok. L'équipe avait ensuite créé un dossier `redirects/` dans l'arborescence et y avait stocké 1 247 stories, une par règle de redirection.\n\nCôté Next.js, le middleware chargeait ces stories au cold start :\n\n```typescript\n// middleware.ts\nimport { NextResponse } from 'next/server'\nimport type { NextRequest } from 'next/server'\n\ninterface RedirectRule {\n  source_path: string\n  target_path: string\n  status_code: '301' | '302'\n}\n\nlet redirectMap: Map\u003Cstring, RedirectRule> | null = null\n\nasync function loadRedirects(): Promise\u003CMap\u003Cstring, RedirectRule>> {\n  const map = new Map\u003Cstring, RedirectRule>()\n  let page = 1\n  let total = 0\n\n  do {\n    const res = await fetch(\n      `https://api.storyblok.com/v2/cdn/stories?starts_with=redirects/&token=${process.env.STORYBLOK_TOKEN}&per_page=100&page=${page}&version=published`\n    )\n    const data = await res.json()\n    total = data.total\n\n    for (const story of data.stories) {\n      const content = story.content as RedirectRule\n      map.set(content.source_path, {\n        source_path: content.source_path,\n        target_path: content.target_path,\n        status_code: content.status_code || '301',\n      })\n    }\n    page++\n  } while ((page - 1) * 100 \u003C total)\n\n  return map\n}\n\nexport async function middleware(request: NextRequest) {\n  if (!redirectMap) {\n    redirectMap = await loadRedirects()\n  }\n\n  const rule = redirectMap.get(request.nextUrl.pathname)\n  if (rule) {\n    const statusCode = rule.status_code === '302' ? 302 : 301\n    return NextResponse.redirect(\n      new URL(rule.target_path, request.url),\n      statusCode\n    )\n  }\n\n  return NextResponse.next()\n}\n\nexport const config = {\n  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],\n}\n```\n\nCe système fonctionnait depuis onze mois sans incident.\n\n### Ce qui se passe lors du changement de plan\n\nStoryblok propose un mécanisme d'export/import d'espace lors d'un changement de plan. Le processus exporte les composants (schémas), les stories (contenus), les assets et les datasources. Mais l'export repose sur la classification interne des composants.\n\nLe problème : `redirect-rule` avait été créé comme `nestable`, puis sa propriété `is_root` avait été passée à `true` via l'interface. Dans la base Storyblok, le composant conservait un flag interne `component_group_id` hérité de sa création en tant que nestable. Lors de l'export d'espace, le processus de sérialisation traitait les stories associées à des composants ayant ce flag comme des blocs imbriqués — pas comme des stories autonomes.\n\nRésultat : les schémas de composants étaient bien exportés (y compris `redirect-rule`), mais les 1 247 stories du dossier `redirects/` n'étaient pas incluses dans le fichier d'export. Elles appartenaient à un composant que le système considérait encore partiellement comme nestable.\n\n### Ce que voit le développeur vs ce que voit le middleware\n\nDans le navigateur, l'interface Storyblok du nouvel espace Business affiche le content type `redirect-rule` dans la liste des composants. Un éditeur peut même créer une nouvelle story de ce type. Tout semble normal.\n\nMais le dossier `redirects/` est vide. Et le middleware Next.js, au prochain cold start, charge une map vide. Chaque requête vers une ancienne URL traverse le middleware sans match et atteint le router Next.js — qui renvoie un 404.\n\nPour confirmer le diagnostic, l'équipe exécute un diff entre l'export JSON de l'ancien espace et celui du nouveau :\n\n```bash\n# Extraction des slugs de stories dans l'ancien export\ncat old-space-export.json | jq '[.stories[] | select(.full_slug | startswith(\"redirects/\")) | .full_slug] | length'\n# Résultat : 1247\n\n# Même extraction dans le nouveau\ncat new-space-export.json | jq '[.stories[] | select(.full_slug | startswith(\"redirects/\")) | .full_slug] | length'\n# Résultat : 0\n```\n\n1 247 à 0. Pas de suppression. Pas d'erreur loguée. Juste un export qui ne les a jamais incluses.\n\n### Pourquoi personne ne l'a vu\n\nTrois raisons.\n\nPremièrement, l'équipe QA testait les pages du site — pas les redirections. Le plan de test post-migration couvrait les composants visuels, les stories produit, les pages catégorie. Personne n'avait de test automatisé vérifiant que l'API retournait des stories dans `redirects/`.\n\nDeuxièmement, le middleware Next.js ne logue pas d'erreur quand la map est vide. Il initialise silencieusement une `Map` de taille 0 et traite chaque requête normalement. Pas de warning, pas d'alerte.\n\nTroisièmement, les 404 ne sont pas immédiatement visibles côté utilisateur. Les anciennes URLs ne sont pas dans la navigation. Elles sont atteintes via des backlinks externes, des résultats de recherche, des bookmarks. Le trafic arrive — et repart avec un 404. Silencieusement. Pendant 72 heures, ce problème est invisible de l'intérieur. Seuls les logs serveur et Googlebot le voient. Un scénario comparable à ce qu'on observe quand [un CMS headless sert du contenu vide à Googlebot sans que l'équipe s'en aperçoive](/blog/strapi-public-role-bloque-l-api-lecture-ssr-void-googlebot-voit-page-blanche).\n\n## Le fix : réimport, fallback et alerting\n\n### Étape 1 — Réimport d'urgence des stories de redirection\n\nL'ancien espace étant encore accessible, l'équipe exporte les stories du dossier `redirects/` via l'API Management :\n\n```bash\n#!/bin/bash\n# Export des stories de redirection depuis l'ancien espace\nOLD_TOKEN=\"old-space-management-token\"\nSPACE_ID=\"old-space-id\"\nPAGE=1\nALL_STORIES=\"[]\"\n\nwhile true; do\n  RESPONSE=$(curl -s -H \"Authorization: $OLD_TOKEN\" \\\n    \"https://mapi.storyblok.com/v1/spaces/$SPACE_ID/stories?starts_with=redirects/&per_page=100&page=$PAGE\")\n\n  STORIES=$(echo \"$RESPONSE\" | jq '.stories')\n  COUNT=$(echo \"$STORIES\" | jq 'length')\n\n  if [ \"$COUNT\" -eq 0 ]; then\n    break\n  fi\n\n  ALL_STORIES=$(echo \"$ALL_STORIES $STORIES\" | jq -s 'add')\n  PAGE=$((PAGE + 1))\ndone\n\necho \"$ALL_STORIES\" > redirect-stories-backup.json\necho \"Exported $(echo \"$ALL_STORIES\" | jq 'length') stories\"\n```\n\nPuis réimport dans le nouvel espace, story par story, via l'API Management du nouveau space :\n\n```bash\n#!/bin/bash\nNEW_TOKEN=\"new-space-management-token\"\nNEW_SPACE_ID=\"new-space-id\"\n\n# Créer le dossier redirects/ d'abord\ncurl -s -X POST -H \"Authorization: $NEW_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"story\":{\"name\":\"redirects\",\"slug\":\"redirects\",\"is_folder\":true}}' \\\n  \"https://mapi.storyblok.com/v1/spaces/$NEW_SPACE_ID/stories\"\n\n# Importer chaque story\ncat redirect-stories-backup.json | jq -c '.[]' | while read -r story; do\n  NAME=$(echo \"$story\" | jq -r '.name')\n  SLUG=$(echo \"$story\" | jq -r '.slug')\n  CONTENT=$(echo \"$story\" | jq '.content')\n\n  curl -s -X POST -H \"Authorization: $NEW_TOKEN\" \\\n    -H \"Content-Type: application/json\" \\\n    -d \"{\\\"story\\\":{\\\"name\\\":\\\"$NAME\\\",\\\"slug\\\":\\\"$SLUG\\\",\\\"content\\\":$CONTENT,\\\"parent_id\\\":null},\\\"publish\\\":1}\" \\\n    \"https://mapi.storyblok.com/v1/spaces/$NEW_SPACE_ID/stories\" > /dev/null\n\n  echo \"Imported: $SLUG\"\ndone\n```\n\nL'import prend 23 minutes pour les 1 247 stories. Chaque story est publiée immédiatement.\n\n### Étape 2 — Invalidation du cache middleware\n\nLe middleware Next.js cache la map de redirections en mémoire au cold start. Deux actions nécessaires :\n\n1. Redéploiement sur Vercel pour forcer un nouveau cold start.\n2. Ajout d'une variable d'environnement `REDIRECT_CACHE_TTL=3600` pour forcer un rechargement horaire au lieu de garder la map indéfiniment.\n\n```typescript\n// middleware.ts — version corrigée avec TTL\nlet redirectMap: Map\u003Cstring, RedirectRule> | null = null\nlet lastLoadTime = 0\nconst CACHE_TTL = parseInt(process.env.REDIRECT_CACHE_TTL || '3600', 10) * 1000\n\nexport async function middleware(request: NextRequest) {\n  const now = Date.now()\n  if (!redirectMap || now - lastLoadTime > CACHE_TTL) {\n    redirectMap = await loadRedirects()\n    lastLoadTime = now\n\n    // Alerte si la map est vide ou anormalement petite\n    if (redirectMap.size \u003C 100) {\n      console.error(\n        `[REDIRECT WARNING] Only ${redirectMap.size} redirect rules loaded. Expected 1200+.`\n      )\n    }\n  }\n\n  const rule = redirectMap.get(request.nextUrl.pathname)\n  if (rule) {\n    const statusCode = rule.status_code === '302' ? 302 : 301\n    return NextResponse.redirect(\n      new URL(rule.target_path, request.url),\n      statusCode\n    )\n  }\n\n  return NextResponse.next()\n}\n```\n\nLe `console.error` avec seuil est rudimentaire mais aurait suffi à déclencher une alerte Vercel dès le premier cold start post-migration.\n\n### Étape 3 — Vérification Screaming Frog\n\nL'équipe lance un crawl Screaming Frog en mode liste, en injectant un échantillon de 200 anciennes URLs. Résultat attendu : 200 réponses 301. Résultat obtenu après réimport : 198 réponses 301, 2 réponses 404. Les deux 404 correspondent à des stories dont le slug contenait des caractères spéciaux mal encodés lors de l'export. Fix manuel en 5 minutes.\n\n### Étape 4 — Demande de re-crawl\n\nPour les 312 URLs déjà passées en 404 dans l'index Google, l'équipe utilise l'outil d'inspection d'URL dans Search Console pour demander une réindexation sur les 50 pages les plus critiques. Pour le reste, le crawl naturel de Googlebot — environ 800 pages/jour sur ce site — doit suffire.\n\n### Temps de récupération\n\n- **J+1** après le fix : les logs Vercel montrent 0 erreur 404 sur les URLs redirigées.\n- **J+3** : Search Console commence à reclasser les URLs de \"Non trouvée\" à \"Valide\".\n- **J+9** : 95 % des URLs sont de retour dans l'index.\n- **J+14** : le trafic organique sur les pages cibles revient à son niveau pré-incident. Perte estimée sur la période : 12 400 clics, soit environ 6 800 € de revenu attribuable au SEO pour ce site.\n\nL'impact aurait pu être bien pire. Les redirections servaient principalement des backlinks anciens et du trafic longue traîne. Si le content type perdu avait été les [métadonnées SEO elles-mêmes](/blog/contentful-le-champ-seo-title-non-sync-vers-next-js-fallback-first-h1-genere), la récupération aurait pris des semaines, pas des jours.\n\n## Ce qu'on en retient\n\nLes CMS headless ne sont pas des bases de données relationnelles. Leur couche d'export/import a des angles morts, surtout quand un composant a changé de nature au fil du temps. Storyblok n'est pas le seul concerné — tout CMS qui distingue \"nestable\" et \"root\" expose ce risque.\n\nTrois garde-fous auraient évité cet incident. Un test automatisé post-migration qui compte les stories par content type et compare à l'espace source. Un seuil d'alerte dans le middleware quand la map de redirections passe sous un minimum attendu. Et un crawl de validation sur un échantillon d'anciennes URLs avant de couper l'ancien espace.\n\nUn monitoring continu type Seogard détecte ce genre de régression — 1 200 URLs passant de 301 à 404 en une nuit — en quelques minutes. Pas en 72 heures.\n\nLes redirections ne sont pas un détail d'ops. Ce sont des [contenus critiques au même titre que les métadonnées](/blog/next-js-metadata-async-qui-throw-la-page-sert-le-fallback-default-next-js), et elles méritent le même niveau de test, de backup et de surveillance.\n```","https://seogard.io/blog/storyblok-redirections-en-custom-field-oubliees-au-passage-nouveau-plan","Headless","2026-06-17T16:01:44.888Z","2026-06-17","Un site e-commerce perd 1 200 redirections stockées en custom field Storyblok lors d'un upgrade de plan. Récit, diagnostic et fix complet.","\u003Ch1>Storyblok : 1 200 redirections custom disparaissent lors d'un upgrade de plan\u003C/h1>\n\u003Cp>Mardi 14h. L'équipe ops d'un site e-commerce français — 8 400 pages produit, 2,1 millions de sessions mensuelles — finalise la migration de son espace Storyblok du plan Startup vers le plan Business. Le backoffice affiche le nouveau plan. Les composants sont là. Les stories aussi. Personne ne vérifie la table de redirections stockée dans un content type custom. 72 heures plus tard, Search Console remonte 1 247 erreurs 404 sur des URLs qui généraient encore 38 000 clics par mois.\u003C/p>\n\u003Ch2>Mercredi 9h12 — Les premières alertes\u003C/h2>\n\u003Cp>L'alerte vient de Slack, pas de Search Console. Un développeur front remarque un pic d'erreurs 404 dans les logs Vercel du middleware de redirection. Il ouvre le dashboard : 847 requêtes 404 en une heure sur des anciennes URLs produit — des slugs de la V2 du site, migrés il y a onze mois.\u003C/p>\n\u003Cp>Premier réflexe : vérifier le middleware Next.js. Le code n'a pas bougé depuis trois semaines. Le fichier \u003Ccode>middleware.ts\u003C/code> fetch bien l'API Storyblok au démarrage pour charger la map de redirections. Le problème n'est pas dans le code.\u003C/p>\n\u003Cp>Deuxième réflexe : ouvrir Storyblok. Le content type \u003Ccode>redirect-rule\u003C/code> existe toujours dans le schéma. Mais quand l'équipe filtre les stories de type \u003Ccode>redirect-rule\u003C/code>, le résultat est vide. Zéro entrée. 1 247 stories de redirection ont disparu.\u003C/p>\n\u003Cp>L'hypothèse initiale est un bug d'affichage. Quelqu'un rafraîchit. Quelqu'un vide le cache du navigateur. Quelqu'un essaie l'API directement :\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://api.storyblok.com/v2/cdn/stories?starts_with=redirects/&#x26;token=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$STORYBLOK_TOKEN\u003C/span>\u003Cspan style=\"color:#9ECBFF\">&#x26;per_page=25\"\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> jq\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '.stories | length'\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Réponse : \u003Ccode>0\u003C/code>.\u003C/p>\n\u003Cp>La panique s'installe à 9h34. Le lead SEO ouvre Search Console. La couverture d'indexation montre déjà 312 URLs passées de \"Valide\" à \"Non trouvée (404)\". Google a crawlé une partie des anciennes URLs pendant la nuit. Le reste va suivre.\u003C/p>\n\u003Cp>L'équipe contacte le support Storyblok. En parallèle, quelqu'un vérifie le changelog de l'espace : la migration de plan date de mardi 14h07. Aucune suppression manuelle de story n'apparaît dans les logs d'activité après cette date. Les stories n'ont pas été supprimées — elles n'ont pas été portées.\u003C/p>\n\u003Cp>Le lead dev ouvre l'ancien espace (encore accessible en lecture pendant 48h après downgrade) et confirme : les 1 247 stories \u003Ccode>redirect-rule\u003C/code> sont bien là. Dans le nouvel espace Business, elles n'existent pas.\u003C/p>\n\u003Cp>À 10h15, le constat est posé : l'export-import de plan n'a pas inclus les stories dont le content type était marqué comme \"nestable\" plutôt que \"content type\". Et \u003Ccode>redirect-rule\u003C/code> avait été créé comme nestable component il y a onze mois, avant d'être promu en content type — sans que la conversion soit complète côté Storyblok.\u003C/p>\n\u003Ch2>Le bug : un content type fantôme entre deux mondes\u003C/h2>\n\u003Cp>Pour comprendre ce qui s'est passé, il faut revenir à l'architecture de redirections mise en place un an plus tôt.\u003C/p>\n\u003Ch3>L'architecture initiale\u003C/h3>\n\u003Cp>L'équipe avait créé un composant Storyblok nommé \u003Ccode>redirect-rule\u003C/code> avec trois champs :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">{\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  \"name\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"redirect-rule\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  \"schema\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    \"source_path\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">      \"type\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"text\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">      \"required\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">      \"description\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"Ancien chemin (ex: /produit/ancien-slug)\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    \"target_path\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">      \"type\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"text\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">      \"required\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">      \"description\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"Nouveau chemin (ex: /p/nouveau-slug)\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    \"status_code\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">      \"type\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"option\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">      \"options\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        { \u003C/span>\u003Cspan style=\"color:#79B8FF\">\"name\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"301\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">\"value\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"301\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        { \u003C/span>\u003Cspan style=\"color:#79B8FF\">\"name\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"302\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">\"value\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"302\"\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:#79B8FF\">      \"default_value\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"301\"\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:#79B8FF\">  \"is_nestable\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">false\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  \"is_root\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Ce composant avait été créé initialement comme \u003Ccode>nestable\u003C/code> (intégrable dans d'autres blocs), puis converti en \u003Ccode>root\u003C/code> (utilisable comme content type autonome) via l'interface Storyblok. L'équipe avait ensuite créé un dossier \u003Ccode>redirects/\u003C/code> dans l'arborescence et y avait stocké 1 247 stories, une par règle de redirection.\u003C/p>\n\u003Cp>Côté Next.js, le middleware chargeait ces stories au cold start :\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\">interface\u003C/span>\u003Cspan style=\"color:#B392F0\"> RedirectRule\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  source_path\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  target_path\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  status_code\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '301'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '302'\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\">let\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> redirectMap\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> Map\u003C/span>\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#B392F0\">RedirectRule\u003C/span>\u003Cspan style=\"color:#E1E4E8\">> \u003C/span>\u003Cspan style=\"color:#F97583\">|\u003C/span>\u003Cspan style=\"color:#79B8FF\"> null\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#79B8FF\"> null\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\"> loadRedirects\u003C/span>\u003Cspan style=\"color:#E1E4E8\">()\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> Promise\u003C/span>\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#B392F0\">Map\u003C/span>\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#B392F0\">RedirectRule\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\"> map\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> new\u003C/span>\u003Cspan style=\"color:#B392F0\"> Map\u003C/span>\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#B392F0\">RedirectRule\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  let\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> page \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 1\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  let\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> total \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  do\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\">      `https://api.storyblok.com/v2/cdn/stories?starts_with=redirects/&#x26;token=${\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\">STORYBLOK_TOKEN\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}&#x26;per_page=100&#x26;page=${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">page\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}&#x26;version=published`\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\"> data\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:#E1E4E8\">    total \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> data.total\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\"> story\u003C/span>\u003Cspan style=\"color:#F97583\"> of\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> data.stories) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> content\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> story.content \u003C/span>\u003Cspan style=\"color:#F97583\">as\u003C/span>\u003Cspan style=\"color:#B392F0\"> RedirectRule\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      map.\u003C/span>\u003Cspan style=\"color:#B392F0\">set\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(content.source_path, {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        source_path: content.source_path,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        target_path: content.target_path,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        status_code: content.status_code \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '301'\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\">    page\u003C/span>\u003Cspan style=\"color:#F97583\">++\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  } \u003C/span>\u003Cspan style=\"color:#F97583\">while\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ((page \u003C/span>\u003Cspan style=\"color:#F97583\">-\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">*\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 100\u003C/span>\u003Cspan style=\"color:#F97583\"> &#x3C;\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> total)\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\"> map\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\"> async\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\">  if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">redirectMap) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    redirectMap \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> loadRedirects\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\"> rule\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> redirectMap.\u003C/span>\u003Cspan style=\"color:#B392F0\">get\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(request.nextUrl.pathname)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (rule) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> statusCode\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> rule.status_code \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '302'\u003C/span>\u003Cspan style=\"color:#F97583\"> ?\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 302\u003C/span>\u003Cspan style=\"color:#F97583\"> :\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 301\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> NextResponse.\u003C/span>\u003Cspan style=\"color:#B392F0\">redirect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      new\u003C/span>\u003Cspan style=\"color:#B392F0\"> URL\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(rule.target_path, request.url),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      statusCode\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    )\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> NextResponse.\u003C/span>\u003Cspan style=\"color:#B392F0\">next\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\">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\">'/((?!api|_next/static|_next/image|favicon.ico).*)'\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 système fonctionnait depuis onze mois sans incident.\u003C/p>\n\u003Ch3>Ce qui se passe lors du changement de plan\u003C/h3>\n\u003Cp>Storyblok propose un mécanisme d'export/import d'espace lors d'un changement de plan. Le processus exporte les composants (schémas), les stories (contenus), les assets et les datasources. Mais l'export repose sur la classification interne des composants.\u003C/p>\n\u003Cp>Le problème : \u003Ccode>redirect-rule\u003C/code> avait été créé comme \u003Ccode>nestable\u003C/code>, puis sa propriété \u003Ccode>is_root\u003C/code> avait été passée à \u003Ccode>true\u003C/code> via l'interface. Dans la base Storyblok, le composant conservait un flag interne \u003Ccode>component_group_id\u003C/code> hérité de sa création en tant que nestable. Lors de l'export d'espace, le processus de sérialisation traitait les stories associées à des composants ayant ce flag comme des blocs imbriqués — pas comme des stories autonomes.\u003C/p>\n\u003Cp>Résultat : les schémas de composants étaient bien exportés (y compris \u003Ccode>redirect-rule\u003C/code>), mais les 1 247 stories du dossier \u003Ccode>redirects/\u003C/code> n'étaient pas incluses dans le fichier d'export. Elles appartenaient à un composant que le système considérait encore partiellement comme nestable.\u003C/p>\n\u003Ch3>Ce que voit le développeur vs ce que voit le middleware\u003C/h3>\n\u003Cp>Dans le navigateur, l'interface Storyblok du nouvel espace Business affiche le content type \u003Ccode>redirect-rule\u003C/code> dans la liste des composants. Un éditeur peut même créer une nouvelle story de ce type. Tout semble normal.\u003C/p>\n\u003Cp>Mais le dossier \u003Ccode>redirects/\u003C/code> est vide. Et le middleware Next.js, au prochain cold start, charge une map vide. Chaque requête vers une ancienne URL traverse le middleware sans match et atteint le router Next.js — qui renvoie un 404.\u003C/p>\n\u003Cp>Pour confirmer le diagnostic, l'équipe exécute un diff entre l'export JSON de l'ancien espace et celui du nouveau :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Extraction des slugs de stories dans l'ancien export\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">cat\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> old-space-export.json\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> jq\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '[.stories[] | select(.full_slug | startswith(\"redirects/\")) | .full_slug] | length'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Résultat : 1247\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Même extraction dans le nouveau\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">cat\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> new-space-export.json\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> jq\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '[.stories[] | select(.full_slug | startswith(\"redirects/\")) | .full_slug] | length'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Résultat : 0\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>1 247 à 0. Pas de suppression. Pas d'erreur loguée. Juste un export qui ne les a jamais incluses.\u003C/p>\n\u003Ch3>Pourquoi personne ne l'a vu\u003C/h3>\n\u003Cp>Trois raisons.\u003C/p>\n\u003Cp>Premièrement, l'équipe QA testait les pages du site — pas les redirections. Le plan de test post-migration couvrait les composants visuels, les stories produit, les pages catégorie. Personne n'avait de test automatisé vérifiant que l'API retournait des stories dans \u003Ccode>redirects/\u003C/code>.\u003C/p>\n\u003Cp>Deuxièmement, le middleware Next.js ne logue pas d'erreur quand la map est vide. Il initialise silencieusement une \u003Ccode>Map\u003C/code> de taille 0 et traite chaque requête normalement. Pas de warning, pas d'alerte.\u003C/p>\n\u003Cp>Troisièmement, les 404 ne sont pas immédiatement visibles côté utilisateur. Les anciennes URLs ne sont pas dans la navigation. Elles sont atteintes via des backlinks externes, des résultats de recherche, des bookmarks. Le trafic arrive — et repart avec un 404. Silencieusement. Pendant 72 heures, ce problème est invisible de l'intérieur. Seuls les logs serveur et Googlebot le voient. Un scénario comparable à ce qu'on observe quand \u003Ca href=\"/blog/strapi-public-role-bloque-l-api-lecture-ssr-void-googlebot-voit-page-blanche\">un CMS headless sert du contenu vide à Googlebot sans que l'équipe s'en aperçoive\u003C/a>.\u003C/p>\n\u003Ch2>Le fix : réimport, fallback et alerting\u003C/h2>\n\u003Ch3>Étape 1 — Réimport d'urgence des stories de redirection\u003C/h3>\n\u003Cp>L'ancien espace étant encore accessible, l'équipe exporte les stories du dossier \u003Ccode>redirects/\u003C/code> via l'API Management :\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\"># Export des stories de redirection depuis l'ancien espace\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">OLD_TOKEN\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"old-space-management-token\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">SPACE_ID\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"old-space-id\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">PAGE\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">1\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">ALL_STORIES\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"[]\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">while\u003C/span>\u003Cspan style=\"color:#79B8FF\"> true\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\">  RESPONSE\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\"> -H\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"Authorization: \u003C/span>\u003Cspan style=\"color:#E1E4E8\">$OLD_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://mapi.storyblok.com/v1/spaces/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$SPACE_ID\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/stories?starts_with=redirects/&#x26;per_page=100&#x26;page=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$PAGE\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:#E1E4E8\">  STORIES\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$(\u003C/span>\u003Cspan style=\"color:#79B8FF\">echo\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$RESPONSE\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> jq\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '.stories'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  COUNT\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$(\u003C/span>\u003Cspan style=\"color:#79B8FF\">echo\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$STORIES\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> jq\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'length'\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:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$COUNT\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#F97583\"> -eq\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ]; \u003C/span>\u003Cspan style=\"color:#F97583\">then\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    break\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  fi\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  ALL_STORIES\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$(\u003C/span>\u003Cspan style=\"color:#79B8FF\">echo\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$ALL_STORIES\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> $STORIES\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> jq\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -s\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'add'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  PAGE\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$((\u003C/span>\u003Cspan style=\"color:#B392F0\">PAGE\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> +\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">))\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\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$ALL_STORIES\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#F97583\"> >\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> redirect-stories-backup.json\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">echo\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"Exported $(\u003C/span>\u003Cspan style=\"color:#79B8FF\">echo\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$ALL_STORIES\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\" \u003C/span>\u003Cspan style=\"color:#F97583\">|\u003C/span>\u003Cspan style=\"color:#B392F0\"> jq\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'length') stories\"\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Puis réimport dans le nouvel espace, story par story, via l'API Management du nouveau space :\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:#E1E4E8\">NEW_TOKEN\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"new-space-management-token\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">NEW_SPACE_ID\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"new-space-id\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Créer le dossier redirects/ d'abord\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\"> -H\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"Authorization: \u003C/span>\u003Cspan style=\"color:#E1E4E8\">$NEW_TOKEN\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\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\"> '{\"story\":{\"name\":\"redirects\",\"slug\":\"redirects\",\"is_folder\":true}}'\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  \"https://mapi.storyblok.com/v1/spaces/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$NEW_SPACE_ID\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/stories\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Importer chaque story\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">cat\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> redirect-stories-backup.json\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> jq\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -c\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '.[]'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#F97583\"> while\u003C/span>\u003Cspan style=\"color:#79B8FF\"> read\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -r\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> story\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\">  NAME\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$(\u003C/span>\u003Cspan style=\"color:#79B8FF\">echo\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$story\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> jq\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -r\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '.name'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  SLUG\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$(\u003C/span>\u003Cspan style=\"color:#79B8FF\">echo\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$story\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> jq\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -r\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '.slug'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  CONTENT\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$(\u003C/span>\u003Cspan style=\"color:#79B8FF\">echo\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$story\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> jq\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '.content'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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\"> -H\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"Authorization: \u003C/span>\u003Cspan style=\"color:#E1E4E8\">$NEW_TOKEN\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\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\">story\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\">:{\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\">name\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$NAME\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\">,\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\">slug\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$SLUG\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\">,\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\">content\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\">:\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$CONTENT\u003C/span>\u003Cspan style=\"color:#9ECBFF\">,\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\">parent_id\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\">:null},\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\">publish\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\">:1}\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"https://mapi.storyblok.com/v1/spaces/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$NEW_SPACE_ID\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/stories\"\u003C/span>\u003Cspan style=\"color:#F97583\"> >\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> /dev/null\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\"> \"Imported: \u003C/span>\u003Cspan style=\"color:#E1E4E8\">$SLUG\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">done\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>L'import prend 23 minutes pour les 1 247 stories. Chaque story est publiée immédiatement.\u003C/p>\n\u003Ch3>Étape 2 — Invalidation du cache middleware\u003C/h3>\n\u003Cp>Le middleware Next.js cache la map de redirections en mémoire au cold start. Deux actions nécessaires :\u003C/p>\n\u003Col>\n\u003Cli>Redéploiement sur Vercel pour forcer un nouveau cold start.\u003C/li>\n\u003Cli>Ajout d'une variable d'environnement \u003Ccode>REDIRECT_CACHE_TTL=3600\u003C/code> pour forcer un rechargement horaire au lieu de garder la map indéfiniment.\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\">// middleware.ts — version corrigée avec TTL\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">let\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> redirectMap\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> Map\u003C/span>\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\">string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#B392F0\">RedirectRule\u003C/span>\u003Cspan style=\"color:#E1E4E8\">> \u003C/span>\u003Cspan style=\"color:#F97583\">|\u003C/span>\u003Cspan style=\"color:#79B8FF\"> null\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#79B8FF\"> null\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">let\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> lastLoadTime \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> CACHE_TTL\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> parseInt\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(process.env.\u003C/span>\u003Cspan style=\"color:#79B8FF\">REDIRECT_CACHE_TTL\u003C/span>\u003Cspan style=\"color:#F97583\"> ||\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '3600'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">10\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">*\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 1000\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\"> async\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\"> now\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> Date.\u003C/span>\u003Cspan style=\"color:#B392F0\">now\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:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">redirectMap \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> now \u003C/span>\u003Cspan style=\"color:#F97583\">-\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> lastLoadTime \u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003Cspan style=\"color:#79B8FF\"> CACHE_TTL\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    redirectMap \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> loadRedirects\u003C/span>\u003Cspan style=\"color:#E1E4E8\">()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    lastLoadTime \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> now\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Alerte si la map est vide ou anormalement petite\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (redirectMap.size \u003C/span>\u003Cspan style=\"color:#F97583\">&#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 100\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\">error\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        `[REDIRECT WARNING] Only ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">redirectMap\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">size\u003C/span>\u003Cspan style=\"color:#9ECBFF\">} redirect rules loaded. Expected 1200+.`\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:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> rule\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> redirectMap.\u003C/span>\u003Cspan style=\"color:#B392F0\">get\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(request.nextUrl.pathname)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (rule) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> statusCode\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> rule.status_code \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '302'\u003C/span>\u003Cspan style=\"color:#F97583\"> ?\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 302\u003C/span>\u003Cspan style=\"color:#F97583\"> :\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 301\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> NextResponse.\u003C/span>\u003Cspan style=\"color:#B392F0\">redirect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      new\u003C/span>\u003Cspan style=\"color:#B392F0\"> URL\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(rule.target_path, request.url),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      statusCode\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    )\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> NextResponse.\u003C/span>\u003Cspan style=\"color:#B392F0\">next\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 \u003Ccode>console.error\u003C/code> avec seuil est rudimentaire mais aurait suffi à déclencher une alerte Vercel dès le premier cold start post-migration.\u003C/p>\n\u003Ch3>Étape 3 — Vérification Screaming Frog\u003C/h3>\n\u003Cp>L'équipe lance un crawl Screaming Frog en mode liste, en injectant un échantillon de 200 anciennes URLs. Résultat attendu : 200 réponses 301. Résultat obtenu après réimport : 198 réponses 301, 2 réponses 404. Les deux 404 correspondent à des stories dont le slug contenait des caractères spéciaux mal encodés lors de l'export. Fix manuel en 5 minutes.\u003C/p>\n\u003Ch3>Étape 4 — Demande de re-crawl\u003C/h3>\n\u003Cp>Pour les 312 URLs déjà passées en 404 dans l'index Google, l'équipe utilise l'outil d'inspection d'URL dans Search Console pour demander une réindexation sur les 50 pages les plus critiques. Pour le reste, le crawl naturel de Googlebot — environ 800 pages/jour sur ce site — doit suffire.\u003C/p>\n\u003Ch3>Temps de récupération\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Cstrong>J+1\u003C/strong> après le fix : les logs Vercel montrent 0 erreur 404 sur les URLs redirigées.\u003C/li>\n\u003Cli>\u003Cstrong>J+3\u003C/strong> : Search Console commence à reclasser les URLs de \"Non trouvée\" à \"Valide\".\u003C/li>\n\u003Cli>\u003Cstrong>J+9\u003C/strong> : 95 % des URLs sont de retour dans l'index.\u003C/li>\n\u003Cli>\u003Cstrong>J+14\u003C/strong> : le trafic organique sur les pages cibles revient à son niveau pré-incident. Perte estimée sur la période : 12 400 clics, soit environ 6 800 € de revenu attribuable au SEO pour ce site.\u003C/li>\n\u003C/ul>\n\u003Cp>L'impact aurait pu être bien pire. Les redirections servaient principalement des backlinks anciens et du trafic longue traîne. Si le content type perdu avait été les \u003Ca href=\"/blog/contentful-le-champ-seo-title-non-sync-vers-next-js-fallback-first-h1-genere\">métadonnées SEO elles-mêmes\u003C/a>, la récupération aurait pris des semaines, pas des jours.\u003C/p>\n\u003Ch2>Ce qu'on en retient\u003C/h2>\n\u003Cp>Les CMS headless ne sont pas des bases de données relationnelles. Leur couche d'export/import a des angles morts, surtout quand un composant a changé de nature au fil du temps. Storyblok n'est pas le seul concerné — tout CMS qui distingue \"nestable\" et \"root\" expose ce risque.\u003C/p>\n\u003Cp>Trois garde-fous auraient évité cet incident. Un test automatisé post-migration qui compte les stories par content type et compare à l'espace source. Un seuil d'alerte dans le middleware quand la map de redirections passe sous un minimum attendu. Et un crawl de validation sur un échantillon d'anciennes URLs avant de couper l'ancien espace.\u003C/p>\n\u003Cp>Un monitoring continu type Seogard détecte ce genre de régression — 1 200 URLs passant de 301 à 404 en une nuit — en quelques minutes. Pas en 72 heures.\u003C/p>\n\u003Cp>Les redirections ne sont pas un détail d'ops. Ce sont des \u003Ca href=\"/blog/next-js-metadata-async-qui-throw-la-page-sert-le-fallback-default-next-js\">contenus critiques au même titre que les métadonnées\u003C/a>, et elles méritent le même niveau de test, de backup et de surveillance.\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,11,[18,19,20,21],"storyblok","redirects","migration","cms","Storyblok : redirections custom perdues après changement de plan","Wed Jun 17 2026 16:01:44 GMT+0000 (Coordinated Universal Time)",[25,40,55,68,83,97],{"_id":26,"slug":27,"__v":6,"author":7,"canonical":28,"category":10,"createdAt":29,"date":30,"description":31,"image":15,"imageAlt":15,"readingTime":16,"tags":32,"title":38,"updatedAt":39},"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","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.",[33,34,35,36,37],"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":41,"slug":42,"__v":6,"author":7,"canonical":43,"category":10,"createdAt":44,"date":30,"description":45,"image":15,"imageAlt":15,"readingTime":46,"tags":47,"title":53,"updatedAt":54},"6a317369aa6b273b0cb03aa2","strapi-public-role-bloque-l-api-lecture-ssr-void-googlebot-voit-page-blanche","https://seogard.io/blog/strapi-public-role-bloque-l-api-lecture-ssr-void-googlebot-voit-page-blanche","2026-06-16T16:01:45.497Z","Un seed Strapi écrase le rôle Public. L'API renvoie 403, le SSR sert du vide. Récit complet : diagnostic, fix, récupération SEO en 19 jours.",12,[48,49,50,51,52],"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)",{"_id":56,"slug":57,"__v":6,"author":7,"canonical":58,"category":10,"createdAt":59,"date":60,"description":61,"image":15,"imageAlt":15,"readingTime":46,"tags":62,"title":66,"updatedAt":67},"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.",[63,64,37,65],"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":69,"slug":70,"__v":6,"author":7,"canonical":71,"category":72,"createdAt":73,"date":12,"description":74,"image":15,"imageAlt":15,"readingTime":46,"tags":75,"title":81,"updatedAt":82},"6a3238a5aa6b273b0c4e69eb","bing-rolls-out-ai-citation-share-in-webmaster-tools-via-sejournal-mattgsouthern","https://seogard.io/blog/bing-rolls-out-ai-citation-share-in-webmaster-tools-via-sejournal-mattgsouthern","Actualités SEO","2026-06-17T06:03:17.202Z","Analyse technique du nouveau Citation Share dans Bing Webmaster Tools. Métriques AI, impact sur le trafic, et stratégies d'optimisation concrètes.",[76,77,78,79,80],"bing","citation share","webmaster tools","AI search","SEO technique","Bing AI Citation Share : ce que ça change pour le SEO technique","Wed Jun 17 2026 06:03:17 GMT+0000 (Coordinated Universal Time)",{"_id":84,"slug":85,"__v":6,"author":7,"canonical":86,"category":72,"createdAt":87,"date":60,"description":88,"image":15,"imageAlt":15,"readingTime":46,"tags":89,"title":95,"updatedAt":96},"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","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.",[90,91,92,93,94],"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":98,"slug":99,"__v":6,"author":7,"canonical":100,"category":72,"createdAt":101,"date":102,"description":103,"image":15,"imageAlt":15,"readingTime":46,"tags":104,"title":110,"updatedAt":111},"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.",[105,106,107,108,109],"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)",{"categories":113},[114,117,120,124,128,131,135,138,141,145,149,152,155,159,162,165,168,171,172,176],{"category":72,"slug":115,"count":116},"actualites-seo",169,{"category":118,"slug":20,"count":119},"Migration",18,{"category":121,"slug":122,"count":123},"Rendering","rendering",9,{"category":125,"slug":126,"count":127},"Performance","performance",8,{"category":129,"slug":130,"count":127},"Framework","framework",{"category":132,"slug":133,"count":134},"SEO Technique","seo-technique",7,{"category":136,"slug":137,"count":134},"Crawl","crawl",{"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},"JavaScript SEO","javascript-seo",5,{"category":150,"slug":151,"count":148},"Monitoring","monitoring",{"category":153,"slug":154,"count":148},"Structured Data","structured-data",{"category":156,"slug":157,"count":158},"Refonte","refonte",4,{"category":160,"slug":161,"count":158},"Redirections","redirections",{"category":163,"slug":164,"count":158},"Outils","outils",{"category":166,"slug":167,"count":158},"E-commerce","e-commerce",{"category":169,"slug":170,"count":158},"Avancé","avance",{"category":10,"slug":64,"count":158},{"category":173,"slug":174,"count":175},"Contenu","contenu",3,{"category":177,"slug":178,"count":175},"IA & SEO","ia-seo"]