[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fzkMMD8QArPn62UQTCyGBs8ilYLcCtMXlULdcseLF3B8":3,"$fddVvxNdoyFZ_xDZMLzeTMF6LM9o4JB0IH2YzsGDIsns":25},{"_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},"6a034125aa6b273b0c92e66e","how-soft-404s-and-indexing-issues-caused-a-90-traffic-collapse",0,"Equipe Seogard","Un éditeur de contenu avec 28 000 pages indexées migre vers un nouveau CMS. Six semaines plus tard, le trafic organique a chuté de 90%. Aucune pénalité manuelle dans Search Console. Aucun core update en cours. Le coupable : des milliers de pages renvoyant un HTTP 200 avec un contenu vide ou générique — des soft 404s que Google a méthodiquement retirées de son index sans la moindre alerte explicite.\n\n## Ce qu'est réellement une soft 404 — au-delà de la définition basique\n\nUne soft 404, ce n'est pas simplement \"une page qui affiche un message d'erreur avec un code 200\". C'est une classification que Google attribue unilatéralement à toute page qu'il juge fonctionnellement vide, même si elle contient du HTML.\n\nLa documentation Google Search Central [définit la soft 404](https://developers.google.com/search/docs/crawling-indexing/http-network-errors#soft-404-errors) comme une page qui « retourne un code de réponse indiquant un succès mais dont le contenu signale un état d'erreur ». En pratique, le classificateur de Google va beaucoup plus loin que cette définition. Il détecte :\n\n- Les pages dont le contenu principal est substantiellement identique à la page 404 officielle du site\n- Les pages avec un body quasi vide après exécution du JavaScript\n- Les pages contenant uniquement un shell applicatif (header, footer, sidebar) sans contenu unique dans la zone principale\n- Les pages dont le contenu se résume à un message de type \"Aucun résultat trouvé\", \"Cette page n'existe plus\", ou leur équivalent dans n'importe quelle langue\n\nLe problème fondamental : Google ne vous envoie pas d'alerte en temps réel quand il commence à classifier vos pages comme soft 404. Le rapport \"Pages\" de Search Console agrège ces données avec un délai qui peut atteindre plusieurs jours, voire semaines. Pendant ce temps, l'hémorragie d'indexation continue.\n\n### Le mécanisme de désindexation en cascade\n\nQuand Google identifie une soft 404, la page est retirée de l'index. Mais l'effet ne s'arrête pas là. Si un volume significatif de pages d'un même répertoire ou d'un même pattern d'URL retourne des soft 404, Google réduit le crawl budget alloué à ce segment du site. Les pages légitimes de ce même répertoire sont crawlées moins fréquemment, ce qui retarde leur réindexation même après correction.\n\nSur un site de 28 000 pages, si 8 000 URLs d'un coup basculent en soft 404 suite à une migration, Google peut décider de ralentir drastiquement le crawl de l'ensemble du sous-répertoire concerné. C'est exactement le scénario décrit dans [l'analyse publiée par Search Engine Land](https://searchengineland.com/soft-404s-indexing-issues-traffic-collapse-477116) — et c'est un pattern que l'on observe régulièrement sur les migrations mal monitorées.\n\n## Anatomie d'une migration qui génère des soft 404s silencieuses\n\nPrenons un scénario réaliste et détaillé. Un média en ligne — appelons-le *TechDaily* — opère 28 000 articles sur un CMS legacy (Drupal 9). L'équipe migre vers un headless CMS (Strapi) avec un front Next.js en SSR. Les URLs sont préservées, les redirections semblent inutiles puisque la structure ne change pas.\n\n### Ce qui se passe côté serveur\n\nLe nouveau front Next.js utilise `getServerSideProps` pour récupérer le contenu depuis l'API Strapi. Voici un pattern typique qui génère des soft 404s en masse :\n\n```typescript\n// pages/article/[slug].tsx — le pattern problématique\nexport async function getServerSideProps(context: GetServerSidePropsContext) {\n  const { slug } = context.params;\n  \n  try {\n    const res = await fetch(`${STRAPI_URL}/api/articles?filters[slug][$eq]=${slug}`);\n    const data = await res.json();\n    \n    if (!data.data || data.data.length === 0) {\n      // Le problème : on retourne une page vide avec HTTP 200\n      return {\n        props: {\n          article: null,\n          notFound: true  // Flag côté client, invisible pour Googlebot\n        }\n      };\n    }\n    \n    return { props: { article: data.data[0] } };\n  } catch (error) {\n    // En cas d'erreur API, on retourne aussi un 200 avec un contenu vide\n    return {\n      props: {\n        article: null,\n        notFound: true\n      }\n    };\n  }\n}\n```\n\nLe composant React vérifie ensuite le flag `notFound` et affiche un message \"Article non disponible\". Le problème : le serveur renvoie un HTTP 200. Googlebot reçoit une page HTML complète (shell Next.js + header + footer + message d'erreur), la rend, constate que le contenu principal est un message d'erreur, et la classifie comme soft 404.\n\n### La correction : renvoyer un vrai 404 ou utiliser `notFound`\n\nNext.js fournit un mécanisme natif pour ce cas exact :\n\n```typescript\n// pages/article/[slug].tsx — le pattern correct\nexport async function getServerSideProps(context: GetServerSidePropsContext) {\n  const { slug } = context.params;\n  \n  try {\n    const res = await fetch(`${STRAPI_URL}/api/articles?filters[slug][$eq]=${slug}`);\n    const data = await res.json();\n    \n    if (!data.data || data.data.length === 0) {\n      // Next.js renvoie un vrai HTTP 404 et affiche pages/404.tsx\n      return { notFound: true };\n    }\n    \n    return { props: { article: data.data[0] } };\n  } catch (error) {\n    // Timeout API ou erreur serveur : \n    // Choix 1 — renvoyer une 503 avec Retry-After pour signaler un problème temporaire\n    // Choix 2 — renvoyer un 404 si l'article est réellement absent\n    context.res.statusCode = 503;\n    context.res.setHeader('Retry-After', '3600');\n    return { props: { article: null, serverError: true } };\n  }\n}\n```\n\nLa différence est critique : `return { notFound: true }` dans `getServerSideProps` provoque un HTTP 404 réel au niveau de la réponse. Googlebot comprend immédiatement que la page n'existe pas et ne la classifie pas comme soft 404 — il la traite comme un vrai 404, ce qui est le comportement attendu.\n\n### Le facteur aggravant : la migration partielle des données\n\nDans le cas de *TechDaily*, le problème venait du fait que la migration des 28 000 articles vers Strapi n'était pas complète au moment du basculement DNS. Environ 8 200 articles n'avaient pas encore été importés dans le nouveau CMS. Chaque requête vers ces slugs retournait un JSON vide depuis l'API Strapi, et le front Next.js renvoyait un HTTP 200 avec le shell de page.\n\nRésultat en 6 semaines :\n- 8 200 pages classifiées comme soft 404 par Google\n- Crawl rate sur `/article/` divisé par 4 (visible dans les stats de crawl de Search Console)\n- 4 300 pages légitimes (correctement migrées) passées en \"Discovered — currently not indexed\" faute de crawl suffisant\n- Trafic organique : de 185 000 sessions/mois à 19 000 sessions/mois\n\n## Détecter les soft 404s avant que Google ne les détecte pour vous\n\nLe rapport \"Pages\" de Search Console est l'outil de référence, mais il a un défaut structurel : le délai. Google [a d'ailleurs reconnu des problèmes de logging dans Search Console]((/blog/google-fixes-search-console-s-year-long-data-logging-issue-well-kind-of)) qui ont masqué des données pendant une période prolongée. Vous ne pouvez pas vous reposer uniquement sur cet outil pour une détection rapide.\n\n### Audit proactif avec Screaming Frog\n\nScreaming Frog permet de simuler la détection de soft 404 en combinant le crawl avec une recherche de contenu. La méthode :\n\n1. Crawler le site complet en mode \"JavaScript rendering\" (Configuration > Spider > Rendering > JavaScript)\n2. Configurer un Custom Search pour détecter les marqueurs de pages vides\n\nDans Screaming Frog, allez dans Configuration > Custom > Search et ajoutez ces patterns :\n\n```\n# Regex à rechercher dans le HTML rendu (onglet \"Contains\")\nAucun résultat\nArticle non disponible\nPage not found\nThis page doesn't exist\nclass=\"empty-state\"\nclass=\"error-content\"\ndata-page-type=\"404\"\n```\n\nExportez ensuite toutes les URLs qui matchent ces patterns ET qui retournent un HTTP 200. Ce sont vos soft 404s candidates.\n\nPour les sites à gros volume (15 000+ pages), une approche par échantillonnage scripted est plus efficace :\n\n```bash\n#!/bin/bash\n# soft404-detector.sh — Détection rapide de soft 404s sur une liste d'URLs\n# Prérequis : curl, jq\n\nINPUT_FILE=\"urls_to_check.txt\"\nOUTPUT_FILE=\"soft_404_candidates.csv\"\nEMPTY_MARKERS=(\"Aucun résultat\" \"Article non disponible\" \"Page not found\" \"empty-state\")\n\necho \"url,status_code,content_length,soft_404_marker\" > \"$OUTPUT_FILE\"\n\nwhile IFS= read -r url; do\n  # Récupérer la page avec le User-Agent de Googlebot\n  response=$(curl -s -o /tmp/page_body.html -w \"%{http_code}|%{size_download}\" \\\n    -H \"User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\" \\\n    --max-time 10 \"$url\")\n  \n  http_code=$(echo \"$response\" | cut -d'|' -f1)\n  content_length=$(echo \"$response\" | cut -d'|' -f2)\n  \n  marker_found=\"none\"\n  for marker in \"${EMPTY_MARKERS[@]}\"; do\n    if grep -qi \"$marker\" /tmp/page_body.html; then\n      marker_found=\"$marker\"\n      break\n    fi\n  done\n  \n  # Flaguer les pages HTTP 200 avec un marqueur de contenu vide\n  # ou un content_length anormalement bas (\u003C 5KB pour un article éditorial)\n  if [[ \"$http_code\" == \"200\" ]] && ([[ \"$marker_found\" != \"none\" ]] || [[ \"$content_length\" -lt 5000 ]]); then\n    echo \"$url,$http_code,$content_length,$marker_found\" >> \"$OUTPUT_FILE\"\n  fi\n  \n  sleep 0.5  # Rate limiting\ndone \u003C \"$INPUT_FILE\"\n\necho \"Analyse terminée. Résultats dans $OUTPUT_FILE\"\n```\n\nCe script a une limite importante : il ne rend pas le JavaScript. Pour les sites en SSR, ça suffit généralement puisque le HTML initial contient déjà le contenu. Pour les SPA ou les sites avec du client-side rendering, il faut passer par un headless browser (Puppeteer, Playwright) — ce qui est exactement le type de vérification que les [leçons de JavaScript SEO sur les sites e-commerce](/blog/5-javascript-seo-lessons-from-top-ecommerce-sites) détaillent.\n\n### Le piège du contenu \"thin\" vs soft 404\n\nAttention à ne pas confondre soft 404 et contenu thin. Une page de catégorie e-commerce avec 2 produits n'est pas une soft 404 — elle a du contenu légitime, même peu. Une page de catégorie avec 0 produit et le message \"Aucun produit ne correspond à votre recherche\" est une soft 404.\n\nLa distinction est importante parce que la remédiation est différente :\n- Soft 404 → renvoyer un vrai 404 ou 410, ou rediriger vers la page parente\n- Contenu thin → enrichir le contenu ou consolider avec une page canonique\n\nGoogle est de plus en plus agressif sur la classification soft 404 depuis les mises à jour qualité récentes. Les [seuils de qualité que Google applique au contenu scalé](/blog/google-s-quality-threshold-is-quietly-killing-scaled-ai-content-via-sejournal-taylordanrw) s'étendent logiquement aux pages fonctionnellement vides : si le contenu principal ne fournit aucune valeur, la page sort de l'index.\n\n## Le plan de remédiation : de la détection à la réindexation\n\nLa correction des soft 404s suit un ordre précis. Agir dans le désordre peut aggraver la situation.\n\n### Étape 1 : Trier les URLs en trois catégories\n\nExportez la liste complète des URLs en \"Soft 404\" depuis Search Console (Pages > \"Soft 404\"). Classez chaque URL :\n\n**Catégorie A — Contenu existant, problème technique** : L'article existe dans le CMS mais n'est pas servi correctement (erreur API, problème de routing, données non migrées). Correction : résoudre le problème technique pour que la page serve le contenu avec un HTTP 200.\n\n**Catégorie B — Contenu supprimé intentionnellement** : L'article a été retiré (obsolète, doublon, contenu de faible qualité). Correction : renvoyer un HTTP 410 (Gone) plutôt qu'un 200 avec un message d'erreur. Le 410 est plus fort qu'un 404 pour signaler à Google que la page ne reviendra pas.\n\n**Catégorie C — URL qui n'a jamais existé** : Variantes d'URL avec des paramètres mal formés, URLs de crawl fantôme. Correction : HTTP 404 standard + vérifier que ces URLs ne sont pas liées dans le sitemap ou le maillage interne.\n\n### Étape 2 : Corriger les codes HTTP au niveau serveur\n\nPour les URLs de catégorie B, une configuration Nginx permet de gérer proprement les URLs supprimées en masse :\n\n```nginx\n# /etc/nginx/conf.d/gone-urls.conf\n# URLs d'articles supprimés — retournent un 410 Gone\n\n# Pattern matching pour les slugs connus\nlocation ~ ^/article/(ancien-slug-1|ancien-slug-2|ancien-slug-3)$ {\n    return 410;\n}\n\n# Pour un volume important, utiliser une map\nmap $uri $is_gone {\n    default 0;\n    include /etc/nginx/gone-urls.map;  # Fichier avec une entrée par ligne : /article/slug 1;\n}\n\nserver {\n    # ...\n    \n    if ($is_gone) {\n        return 410;\n    }\n    \n    # Pour les articles manquants non explicitement listés,\n    # Next.js gère le 404 via getServerSideProps\n}\n```\n\nLe fichier `/etc/nginx/gone-urls.map` contient une ligne par URL supprimée :\n\n```\n/article/guide-windows-xp-2024 1;\n/article/review-nokia-3310-reconditionne 1;\n# ... 3000+ entrées générées par script\n```\n\n### Étape 3 : Nettoyer le sitemap\n\nUn sitemap qui contient des URLs renvoyant un 404 ou 410 envoie un signal contradictoire à Google. Après correction des codes HTTP, régénérez le sitemap en excluant :\n- Toutes les URLs de catégorie B et C\n- Les URLs retournant un code autre que 200\n- Les URLs avec un `noindex`\n\nValidez le sitemap dans Search Console et vérifiez que le nombre d'URLs \"soumises\" correspond approximativement au nombre d'URLs que vous souhaitez réellement indexer.\n\n### Étape 4 : Forcer le re-crawl — avec mesure\n\nL'API d'indexation Google n'est pas prévue pour les contenus éditoriaux (elle est réservée aux `JobPosting` et `BroadcastEvent` selon la documentation officielle). La méthode recommandée reste l'outil \"Inspection de l'URL\" dans Search Console, avec la fonction \"Demander une indexation\".\n\nLa limite est de quelques centaines de requêtes par jour. Pour un site de 28 000 pages dont 8 000 sont à réindexer, priorisez :\n1. Les 500 pages qui généraient le plus de trafic avant la migration (données dans Google Analytics ou Search Console > Performances, période antérieure)\n2. Les pages cibles de backlinks externes (exportez les backlinks via Ahrefs, Majestic ou Search Console > Liens)\n3. Les pages au sommet de l'arborescence (hubs de catégorie) — leur re-crawl accélérera la découverte des pages enfants\n\nPour le reste, la mise à jour du sitemap et le maillage interne corrigé suffiront — Google les re-crawlera naturellement, mais le délai peut atteindre 4 à 8 semaines sur un site dont le crawl budget a été réduit.\n\n## Le monitoring post-correction : mesurer la récupération\n\nLa récupération ne suit pas une courbe linéaire. Voici les métriques à surveiller et les délais réalistes.\n\n### Métriques Search Console\n\n- **Pages indexées** (rapport \"Pages\") : devrait remonter progressivement. Attendez-vous à une semaine de latence avant que les premières corrections soient reflétées.\n- **Statistiques de crawl** (Paramètres > Statistiques de crawl) : le nombre de requêtes de crawl par jour est l'indicateur le plus précoce. Si Google recommence à crawler votre répertoire `/article/` à un rythme normal, la récupération est en cours.\n- **Impressions et clics** (rapport \"Performances\") : indicateur retardé. La récupération des impressions suit la réindexation avec un décalage de 1 à 3 semaines.\n\n### Le risque de récidive\n\nLe cas de *TechDaily* illustre un problème plus large : la détection ponctuelle ne suffit pas. Une régression peut survenir à tout moment — un déploiement qui casse une route API, une purge de cache qui expose des pages vides, un changement de structure de données côté CMS qui fait échouer silencieusement le rendu.\n\nC'est exactement le scénario dans lequel un monitoring continu apporte de la valeur. Un outil comme Seogard, qui crawle en continu et compare les réponses HTTP et le contenu rendu entre deux passages, détecte immédiatement quand une page qui servait du contenu légitime bascule vers une réponse vide — avant que Google ne la classifie comme soft 404 et ne la désindexe.\n\n### Timeline réaliste de récupération\n\nPour le cas *TechDaily* (28 000 pages, 8 200 soft 404s corrigées) :\n\n| Semaine | Pages réindexées | Trafic organique (sessions/mois) |\n|---------|------------------|----------------------------------|\n| S0 (correction) | 19 800 → baseline | 19 000 |\n| S2 | +1 200 | 32 000 |\n| S4 | +3 400 | 68 000 |\n| S8 | +6 800 | 112 000 |\n| S12 | +8 000 (quasi-total) | 155 000 |\n\nLa récupération à 100% du trafic d'origine (185 000) n'est pas garantie. Pendant la période de désindexation, des concurrents ont pu capter des positions. Certaines queries long-tail perdues ne reviennent jamais à l'identique. Dans le cas de *TechDaily*, la récupération a plafonné à ~84% du trafic pré-migration après 16 semaines.\n\n## Les soft 404s générées côté client : le cas SPA et hydration\n\nLes frameworks modernes ajoutent une couche de complexité. Sur un site en React SPA (sans SSR), Googlebot exécute le JavaScript et évalue le contenu rendu. Si le rendu côté client échoue silencieusement — API qui timeout, erreur de parsing JSON, composant qui crashe — le HTML rendu par Googlebot peut être un shell vide.\n\n### Le scénario d'hydration partielle\n\nAvec Next.js App Router ou Nuxt 3, un pattern vicieux se produit quand le SSR fonctionne mais l'hydration côté client échoue. Google voit le HTML initial servi par le SSR (contenu complet), mais le Web Rendering Service (WRS) de Google exécute aussi le JavaScript et peut aboutir à un état différent si le client-side diverge.\n\nEn pratique, Google se base principalement sur le HTML initial pour l'indexation (le SSR), mais le classificateur de soft 404 évalue le rendu complet. Si votre page fait un fetch côté client qui remplace le contenu SSR par un état d'erreur (parce que l'API est derrière un rate limiter qui bloque Googlebot, par exemple), vous pouvez vous retrouver avec une soft 404 alors que le HTML initial était correct.\n\nLa vérification passe par Chrome DevTools avec le test \"User-Agent Googlebot\" :\n\n1. Ouvrez DevTools > Network conditions\n2. Désactivez le cache et réglez le User-Agent sur `Googlebot/2.1`\n3. Chargez la page et vérifiez dans l'onglet Network que toutes les requêtes API retournent un 200\n4. Comparez le DOM final (Elements) avec le source HTML initial (View Source)\n\nSi le DOM final diffère significativement du HTML source — notamment si le contenu principal a disparu — vous avez un problème d'hydration qui peut déclencher une classification soft 404.\n\nCette problématique rejoint directement les enjeux d'[autorisation des bots que Google teste actuellement](/blog/google-is-testing-new-bot-authorization-standard-via-sejournal-martinibuster) : si votre API ou votre CDN traite Googlebot différemment des utilisateurs humains (rate limiting, challenge JavaScript, blocage géographique), le contenu rendu pour le bot peut être substantiellement différent de ce que voit un utilisateur réel.\n\n## Les leçons structurelles : pourquoi les migrations génèrent systématiquement des soft 404s\n\nLe problème n'est pas spécifique à un CMS ou un framework. Il est structurel. Lors d'une migration, trois conditions se combinent presque systématiquement :\n\n**1. Décalage temporel entre le basculement du front et la migration des données.** Même avec une planification rigoureuse, il y a toujours des articles qui ne passent pas la migration automatique (caractères spéciaux dans les slugs, champs obligatoires manquants dans le nouveau schéma, médias non transférés). Ces articles deviennent des soft 404s silencieuses.\n\n**2. Absence de validation pré-déploiement exhaustive.** Tester 50 URLs sur un site de 28 000 pages ne détecte rien. Il faut crawler l'intégralité du site en staging, comparer le contenu rendu page par page avec la production, et vérifier que chaque URL retourne un contenu substantiel.\n\n**3. Monitoring post-déploiement insuffisant.** L'équipe vérifie que le site \"fonctionne\" (pas de 500, la homepage s'affiche), mais personne ne crawle systématiquement les 28 000 URLs dans les 24h suivant le déploiement pour vérifier que chacune renvoie du contenu.\n\nLe pattern récurrent : l'éditeur détecte le problème 3 à 6 semaines après la migration, quand la chute de trafic devient visible dans les dashboards analytics. À ce stade, Google a déjà désindexé les pages et réduit le crawl budget. La récupération prend ensuite 2 à 4 mois — un trimestre de revenus publicitaires perdu.\n\nC'est le même mécanisme de détection tardive que celui observé sur les [régressions de visibilité après les core updates de Google](/blog/google-s-march-core-update-shifted-visibility-away-from-aggregators-via-sejournal-mattgsouthern) : quand l'impact devient visible, le mal est fait. La seule défense efficace est un monitoring automatisé qui compare l'état du site crawl après crawl et alerte dès qu'une anomalie apparaît — avant que Google n'ait le temps de réagir.\n\nLes soft 404s ne sont pas un bug obscur. C'est la première cause de perte de trafic silencieuse après une migration, devant les redirections manquantes et les canonicals cassées. La différence entre un site qui récupère en 4 semaines et un site qui met 4 mois, c'est la vitesse de détection. Automatisez cette détection — avec Seogard ou avec vos propres scripts — mais ne la laissez jamais dépendre d'un humain qui ouvre Search Console une fois par semaine.\n```","https://seogard.io/blog/how-soft-404s-and-indexing-issues-caused-a-90-traffic-collapse","Actualités SEO","2026-05-12T15:03:01.689Z","2026-05-12","Comment des soft 404s massives après une migration ont provoqué une chute de 90% du trafic organique, et les étapes techniques pour inverser la tendance.","\u003Cp>Un éditeur de contenu avec 28 000 pages indexées migre vers un nouveau CMS. Six semaines plus tard, le trafic organique a chuté de 90%. Aucune pénalité manuelle dans Search Console. Aucun core update en cours. Le coupable : des milliers de pages renvoyant un HTTP 200 avec un contenu vide ou générique — des soft 404s que Google a méthodiquement retirées de son index sans la moindre alerte explicite.\u003C/p>\n\u003Ch2>Ce qu'est réellement une soft 404 — au-delà de la définition basique\u003C/h2>\n\u003Cp>Une soft 404, ce n'est pas simplement \"une page qui affiche un message d'erreur avec un code 200\". C'est une classification que Google attribue unilatéralement à toute page qu'il juge fonctionnellement vide, même si elle contient du HTML.\u003C/p>\n\u003Cp>La documentation Google Search Central \u003Ca href=\"https://developers.google.com/search/docs/crawling-indexing/http-network-errors#soft-404-errors\">définit la soft 404\u003C/a> comme une page qui « retourne un code de réponse indiquant un succès mais dont le contenu signale un état d'erreur ». En pratique, le classificateur de Google va beaucoup plus loin que cette définition. Il détecte :\u003C/p>\n\u003Cul>\n\u003Cli>Les pages dont le contenu principal est substantiellement identique à la page 404 officielle du site\u003C/li>\n\u003Cli>Les pages avec un body quasi vide après exécution du JavaScript\u003C/li>\n\u003Cli>Les pages contenant uniquement un shell applicatif (header, footer, sidebar) sans contenu unique dans la zone principale\u003C/li>\n\u003Cli>Les pages dont le contenu se résume à un message de type \"Aucun résultat trouvé\", \"Cette page n'existe plus\", ou leur équivalent dans n'importe quelle langue\u003C/li>\n\u003C/ul>\n\u003Cp>Le problème fondamental : Google ne vous envoie pas d'alerte en temps réel quand il commence à classifier vos pages comme soft 404. Le rapport \"Pages\" de Search Console agrège ces données avec un délai qui peut atteindre plusieurs jours, voire semaines. Pendant ce temps, l'hémorragie d'indexation continue.\u003C/p>\n\u003Ch3>Le mécanisme de désindexation en cascade\u003C/h3>\n\u003Cp>Quand Google identifie une soft 404, la page est retirée de l'index. Mais l'effet ne s'arrête pas là. Si un volume significatif de pages d'un même répertoire ou d'un même pattern d'URL retourne des soft 404, Google réduit le crawl budget alloué à ce segment du site. Les pages légitimes de ce même répertoire sont crawlées moins fréquemment, ce qui retarde leur réindexation même après correction.\u003C/p>\n\u003Cp>Sur un site de 28 000 pages, si 8 000 URLs d'un coup basculent en soft 404 suite à une migration, Google peut décider de ralentir drastiquement le crawl de l'ensemble du sous-répertoire concerné. C'est exactement le scénario décrit dans \u003Ca href=\"https://searchengineland.com/soft-404s-indexing-issues-traffic-collapse-477116\">l'analyse publiée par Search Engine Land\u003C/a> — et c'est un pattern que l'on observe régulièrement sur les migrations mal monitorées.\u003C/p>\n\u003Ch2>Anatomie d'une migration qui génère des soft 404s silencieuses\u003C/h2>\n\u003Cp>Prenons un scénario réaliste et détaillé. Un média en ligne — appelons-le \u003Cem>TechDaily\u003C/em> — opère 28 000 articles sur un CMS legacy (Drupal 9). L'équipe migre vers un headless CMS (Strapi) avec un front Next.js en SSR. Les URLs sont préservées, les redirections semblent inutiles puisque la structure ne change pas.\u003C/p>\n\u003Ch3>Ce qui se passe côté serveur\u003C/h3>\n\u003Cp>Le nouveau front Next.js utilise \u003Ccode>getServerSideProps\u003C/code> pour récupérer le contenu depuis l'API Strapi. Voici un pattern typique qui génère des soft 404s en masse :\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/article/[slug].tsx — le pattern problématique\u003C/span>\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\"> getServerSideProps\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">context\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> GetServerSidePropsContext\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#79B8FF\">slug\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> context.params;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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>\u003Cspan style=\"color:#9ECBFF\">`${\u003C/span>\u003Cspan style=\"color:#79B8FF\">STRAPI_URL\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}/api/articles?filters[slug][$eq]=${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#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\">    \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\">data.data \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> data.data.\u003C/span>\u003Cspan style=\"color:#79B8FF\">length\u003C/span>\u003Cspan style=\"color:#F97583\"> ===\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">      // Le problème : on retourne une page vide avec HTTP 200\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\">        props: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          article: \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\">          notFound: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\u003C/span>\u003Cspan style=\"color:#6A737D\">  // Flag côté client, invisible pour Googlebot\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      };\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { props: { article: data.data[\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>\u003Cspan style=\"color:#F97583\">catch\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (error) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // En cas d'erreur API, on retourne aussi un 200 avec un contenu vide\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\">      props: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        article: \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\">        notFound: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    };\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le composant React vérifie ensuite le flag \u003Ccode>notFound\u003C/code> et affiche un message \"Article non disponible\". Le problème : le serveur renvoie un HTTP 200. Googlebot reçoit une page HTML complète (shell Next.js + header + footer + message d'erreur), la rend, constate que le contenu principal est un message d'erreur, et la classifie comme soft 404.\u003C/p>\n\u003Ch3>La correction : renvoyer un vrai 404 ou utiliser \u003Ccode>notFound\u003C/code>\u003C/h3>\n\u003Cp>Next.js fournit un mécanisme natif pour ce cas exact :\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/article/[slug].tsx — le pattern correct\u003C/span>\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\"> getServerSideProps\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">context\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> GetServerSidePropsContext\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#79B8FF\">slug\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> context.params;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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>\u003Cspan style=\"color:#9ECBFF\">`${\u003C/span>\u003Cspan style=\"color:#79B8FF\">STRAPI_URL\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}/api/articles?filters[slug][$eq]=${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#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\">    \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\">data.data \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> data.data.\u003C/span>\u003Cspan style=\"color:#79B8FF\">length\u003C/span>\u003Cspan style=\"color:#F97583\"> ===\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">      // Next.js renvoie un vrai HTTP 404 et affiche pages/404.tsx\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\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { props: { article: data.data[\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>\u003Cspan style=\"color:#F97583\">catch\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (error) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Timeout API ou erreur serveur : \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Choix 1 — renvoyer une 503 avec Retry-After pour signaler un problème temporaire\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Choix 2 — renvoyer un 404 si l'article est réellement absent\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    context.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\">    context.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: { article: \u003C/span>\u003Cspan style=\"color:#79B8FF\">null\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, serverError: \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\">\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>La différence est critique : \u003Ccode>return { notFound: true }\u003C/code> dans \u003Ccode>getServerSideProps\u003C/code> provoque un HTTP 404 réel au niveau de la réponse. Googlebot comprend immédiatement que la page n'existe pas et ne la classifie pas comme soft 404 — il la traite comme un vrai 404, ce qui est le comportement attendu.\u003C/p>\n\u003Ch3>Le facteur aggravant : la migration partielle des données\u003C/h3>\n\u003Cp>Dans le cas de \u003Cem>TechDaily\u003C/em>, le problème venait du fait que la migration des 28 000 articles vers Strapi n'était pas complète au moment du basculement DNS. Environ 8 200 articles n'avaient pas encore été importés dans le nouveau CMS. Chaque requête vers ces slugs retournait un JSON vide depuis l'API Strapi, et le front Next.js renvoyait un HTTP 200 avec le shell de page.\u003C/p>\n\u003Cp>Résultat en 6 semaines :\u003C/p>\n\u003Cul>\n\u003Cli>8 200 pages classifiées comme soft 404 par Google\u003C/li>\n\u003Cli>Crawl rate sur \u003Ccode>/article/\u003C/code> divisé par 4 (visible dans les stats de crawl de Search Console)\u003C/li>\n\u003Cli>4 300 pages légitimes (correctement migrées) passées en \"Discovered — currently not indexed\" faute de crawl suffisant\u003C/li>\n\u003Cli>Trafic organique : de 185 000 sessions/mois à 19 000 sessions/mois\u003C/li>\n\u003C/ul>\n\u003Ch2>Détecter les soft 404s avant que Google ne les détecte pour vous\u003C/h2>\n\u003Cp>Le rapport \"Pages\" de Search Console est l'outil de référence, mais il a un défaut structurel : le délai. Google \u003Ca href=\"(/blog/google-fixes-search-console-s-year-long-data-logging-issue-well-kind-of)\">a d'ailleurs reconnu des problèmes de logging dans Search Console\u003C/a> qui ont masqué des données pendant une période prolongée. Vous ne pouvez pas vous reposer uniquement sur cet outil pour une détection rapide.\u003C/p>\n\u003Ch3>Audit proactif avec Screaming Frog\u003C/h3>\n\u003Cp>Screaming Frog permet de simuler la détection de soft 404 en combinant le crawl avec une recherche de contenu. La méthode :\u003C/p>\n\u003Col>\n\u003Cli>Crawler le site complet en mode \"JavaScript rendering\" (Configuration > Spider > Rendering > JavaScript)\u003C/li>\n\u003Cli>Configurer un Custom Search pour détecter les marqueurs de pages vides\u003C/li>\n\u003C/ol>\n\u003Cp>Dans Screaming Frog, allez dans Configuration > Custom > Search et ajoutez ces patterns :\u003C/p>\n\u003Cpre>\u003Ccode># Regex à rechercher dans le HTML rendu (onglet \"Contains\")\nAucun résultat\nArticle non disponible\nPage not found\nThis page doesn't exist\nclass=\"empty-state\"\nclass=\"error-content\"\ndata-page-type=\"404\"\n\u003C/code>\u003C/pre>\n\u003Cp>Exportez ensuite toutes les URLs qui matchent ces patterns ET qui retournent un HTTP 200. Ce sont vos soft 404s candidates.\u003C/p>\n\u003Cp>Pour les sites à gros volume (15 000+ pages), une approche par échantillonnage scripted est plus efficace :\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\"># soft404-detector.sh — Détection rapide de soft 404s sur une liste d'URLs\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Prérequis : curl, jq\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">INPUT_FILE\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"urls_to_check.txt\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">OUTPUT_FILE\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"soft_404_candidates.csv\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">EMPTY_MARKERS\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"Aucun résultat\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"Article non disponible\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"Page not found\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"empty-state\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\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\"> \"url,status_code,content_length,soft_404_marker\"\u003C/span>\u003Cspan style=\"color:#F97583\"> >\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$OUTPUT_FILE\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:#E1E4E8\"> IFS\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> read\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -r\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> url\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; \u003C/span>\u003Cspan style=\"color:#F97583\">do\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  # Récupérer la page avec le User-Agent de Googlebot\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\"> -o\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> /tmp/page_body.html\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -w\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"%{http_code}|%{size_download}\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    -H\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    --max-time\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 10\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$url\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\">  http_code\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\"> cut\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -d\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'|'\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -f1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  content_length\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\"> cut\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -d\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'|'\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -f2\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\">  marker_found\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"none\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> marker \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">EMPTY_MARKERS\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:#F97583\">    if\u003C/span>\u003Cspan style=\"color:#B392F0\"> grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -qi\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$marker\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> /tmp/page_body.html\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; \u003C/span>\u003Cspan style=\"color:#F97583\">then\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      marker_found\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$marker\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\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\">\u003Cspan style=\"color:#F97583\">  done\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  # Flaguer les pages HTTP 200 avec un marqueur de contenu vide\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  # ou un content_length anormalement bas (&#x3C; 5KB pour un article éditorial)\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\">$http_code\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#F97583\"> ==\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"200\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ]] &#x26;&#x26; ([[ \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$marker_found\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#F97583\"> !=\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"none\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ]] \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [[ \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$content_length\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#F97583\"> -lt\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 5000\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\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$url\u003C/span>\u003Cspan style=\"color:#9ECBFF\">,\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$http_code\u003C/span>\u003Cspan style=\"color:#9ECBFF\">,\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$content_length\u003C/span>\u003Cspan style=\"color:#9ECBFF\">,\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$marker_found\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#F97583\"> >>\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$OUTPUT_FILE\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  fi\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  sleep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0.5\u003C/span>\u003Cspan style=\"color:#6A737D\">  # Rate limiting\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">done\u003C/span>\u003Cspan style=\"color:#F97583\"> &#x3C;\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$INPUT_FILE\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\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\"> \"Analyse terminée. Résultats dans \u003C/span>\u003Cspan style=\"color:#E1E4E8\">$OUTPUT_FILE\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Ce script a une limite importante : il ne rend pas le JavaScript. Pour les sites en SSR, ça suffit généralement puisque le HTML initial contient déjà le contenu. Pour les SPA ou les sites avec du client-side rendering, il faut passer par un headless browser (Puppeteer, Playwright) — ce qui est exactement le type de vérification que les \u003Ca href=\"/blog/5-javascript-seo-lessons-from-top-ecommerce-sites\">leçons de JavaScript SEO sur les sites e-commerce\u003C/a> détaillent.\u003C/p>\n\u003Ch3>Le piège du contenu \"thin\" vs soft 404\u003C/h3>\n\u003Cp>Attention à ne pas confondre soft 404 et contenu thin. Une page de catégorie e-commerce avec 2 produits n'est pas une soft 404 — elle a du contenu légitime, même peu. Une page de catégorie avec 0 produit et le message \"Aucun produit ne correspond à votre recherche\" est une soft 404.\u003C/p>\n\u003Cp>La distinction est importante parce que la remédiation est différente :\u003C/p>\n\u003Cul>\n\u003Cli>Soft 404 → renvoyer un vrai 404 ou 410, ou rediriger vers la page parente\u003C/li>\n\u003Cli>Contenu thin → enrichir le contenu ou consolider avec une page canonique\u003C/li>\n\u003C/ul>\n\u003Cp>Google est de plus en plus agressif sur la classification soft 404 depuis les mises à jour qualité récentes. Les \u003Ca href=\"/blog/google-s-quality-threshold-is-quietly-killing-scaled-ai-content-via-sejournal-taylordanrw\">seuils de qualité que Google applique au contenu scalé\u003C/a> s'étendent logiquement aux pages fonctionnellement vides : si le contenu principal ne fournit aucune valeur, la page sort de l'index.\u003C/p>\n\u003Ch2>Le plan de remédiation : de la détection à la réindexation\u003C/h2>\n\u003Cp>La correction des soft 404s suit un ordre précis. Agir dans le désordre peut aggraver la situation.\u003C/p>\n\u003Ch3>Étape 1 : Trier les URLs en trois catégories\u003C/h3>\n\u003Cp>Exportez la liste complète des URLs en \"Soft 404\" depuis Search Console (Pages > \"Soft 404\"). Classez chaque URL :\u003C/p>\n\u003Cp>\u003Cstrong>Catégorie A — Contenu existant, problème technique\u003C/strong> : L'article existe dans le CMS mais n'est pas servi correctement (erreur API, problème de routing, données non migrées). Correction : résoudre le problème technique pour que la page serve le contenu avec un HTTP 200.\u003C/p>\n\u003Cp>\u003Cstrong>Catégorie B — Contenu supprimé intentionnellement\u003C/strong> : L'article a été retiré (obsolète, doublon, contenu de faible qualité). Correction : renvoyer un HTTP 410 (Gone) plutôt qu'un 200 avec un message d'erreur. Le 410 est plus fort qu'un 404 pour signaler à Google que la page ne reviendra pas.\u003C/p>\n\u003Cp>\u003Cstrong>Catégorie C — URL qui n'a jamais existé\u003C/strong> : Variantes d'URL avec des paramètres mal formés, URLs de crawl fantôme. Correction : HTTP 404 standard + vérifier que ces URLs ne sont pas liées dans le sitemap ou le maillage interne.\u003C/p>\n\u003Ch3>Étape 2 : Corriger les codes HTTP au niveau serveur\u003C/h3>\n\u003Cp>Pour les URLs de catégorie B, une configuration Nginx permet de gérer proprement les URLs supprimées en masse :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># /etc/nginx/conf.d/gone-urls.conf\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># URLs d'articles supprimés — retournent un 410 Gone\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Pattern matching pour les slugs connus\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">location\u003C/span>\u003Cspan style=\"color:#F97583\"> ~\u003C/span>\u003Cspan style=\"color:#DBEDFF\"> ^/article/(ancien-slug-1|ancien-slug-2|ancien-slug-3)$ \u003C/span>\u003Cspan style=\"color:#E1E4E8\">{\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 410\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\"># Pour un volume important, utiliser une map\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">map\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> $\u003C/span>\u003Cspan style=\"color:#FFAB70\">uri\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> $is_gone {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    default\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\">    include /etc/nginx/gone-urls.map;  \u003C/span>\u003Cspan style=\"color:#6A737D\"># Fichier avec une entrée par ligne : /article/slug 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\">server\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # ...\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ($is_gone) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        return\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 410\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:#6A737D\">    # Pour les articles manquants non explicitement listés,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # Next.js gère le 404 via getServerSideProps\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le fichier \u003Ccode>/etc/nginx/gone-urls.map\u003C/code> contient une ligne par URL supprimée :\u003C/p>\n\u003Cpre>\u003Ccode>/article/guide-windows-xp-2024 1;\n/article/review-nokia-3310-reconditionne 1;\n# ... 3000+ entrées générées par script\n\u003C/code>\u003C/pre>\n\u003Ch3>Étape 3 : Nettoyer le sitemap\u003C/h3>\n\u003Cp>Un sitemap qui contient des URLs renvoyant un 404 ou 410 envoie un signal contradictoire à Google. Après correction des codes HTTP, régénérez le sitemap en excluant :\u003C/p>\n\u003Cul>\n\u003Cli>Toutes les URLs de catégorie B et C\u003C/li>\n\u003Cli>Les URLs retournant un code autre que 200\u003C/li>\n\u003Cli>Les URLs avec un \u003Ccode>noindex\u003C/code>\u003C/li>\n\u003C/ul>\n\u003Cp>Validez le sitemap dans Search Console et vérifiez que le nombre d'URLs \"soumises\" correspond approximativement au nombre d'URLs que vous souhaitez réellement indexer.\u003C/p>\n\u003Ch3>Étape 4 : Forcer le re-crawl — avec mesure\u003C/h3>\n\u003Cp>L'API d'indexation Google n'est pas prévue pour les contenus éditoriaux (elle est réservée aux \u003Ccode>JobPosting\u003C/code> et \u003Ccode>BroadcastEvent\u003C/code> selon la documentation officielle). La méthode recommandée reste l'outil \"Inspection de l'URL\" dans Search Console, avec la fonction \"Demander une indexation\".\u003C/p>\n\u003Cp>La limite est de quelques centaines de requêtes par jour. Pour un site de 28 000 pages dont 8 000 sont à réindexer, priorisez :\u003C/p>\n\u003Col>\n\u003Cli>Les 500 pages qui généraient le plus de trafic avant la migration (données dans Google Analytics ou Search Console > Performances, période antérieure)\u003C/li>\n\u003Cli>Les pages cibles de backlinks externes (exportez les backlinks via Ahrefs, Majestic ou Search Console > Liens)\u003C/li>\n\u003Cli>Les pages au sommet de l'arborescence (hubs de catégorie) — leur re-crawl accélérera la découverte des pages enfants\u003C/li>\n\u003C/ol>\n\u003Cp>Pour le reste, la mise à jour du sitemap et le maillage interne corrigé suffiront — Google les re-crawlera naturellement, mais le délai peut atteindre 4 à 8 semaines sur un site dont le crawl budget a été réduit.\u003C/p>\n\u003Ch2>Le monitoring post-correction : mesurer la récupération\u003C/h2>\n\u003Cp>La récupération ne suit pas une courbe linéaire. Voici les métriques à surveiller et les délais réalistes.\u003C/p>\n\u003Ch3>Métriques Search Console\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Cstrong>Pages indexées\u003C/strong> (rapport \"Pages\") : devrait remonter progressivement. Attendez-vous à une semaine de latence avant que les premières corrections soient reflétées.\u003C/li>\n\u003Cli>\u003Cstrong>Statistiques de crawl\u003C/strong> (Paramètres > Statistiques de crawl) : le nombre de requêtes de crawl par jour est l'indicateur le plus précoce. Si Google recommence à crawler votre répertoire \u003Ccode>/article/\u003C/code> à un rythme normal, la récupération est en cours.\u003C/li>\n\u003Cli>\u003Cstrong>Impressions et clics\u003C/strong> (rapport \"Performances\") : indicateur retardé. La récupération des impressions suit la réindexation avec un décalage de 1 à 3 semaines.\u003C/li>\n\u003C/ul>\n\u003Ch3>Le risque de récidive\u003C/h3>\n\u003Cp>Le cas de \u003Cem>TechDaily\u003C/em> illustre un problème plus large : la détection ponctuelle ne suffit pas. Une régression peut survenir à tout moment — un déploiement qui casse une route API, une purge de cache qui expose des pages vides, un changement de structure de données côté CMS qui fait échouer silencieusement le rendu.\u003C/p>\n\u003Cp>C'est exactement le scénario dans lequel un monitoring continu apporte de la valeur. Un outil comme Seogard, qui crawle en continu et compare les réponses HTTP et le contenu rendu entre deux passages, détecte immédiatement quand une page qui servait du contenu légitime bascule vers une réponse vide — avant que Google ne la classifie comme soft 404 et ne la désindexe.\u003C/p>\n\u003Ch3>Timeline réaliste de récupération\u003C/h3>\n\u003Cp>Pour le cas \u003Cem>TechDaily\u003C/em> (28 000 pages, 8 200 soft 404s corrigées) :\u003C/p>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Semaine\u003C/th>\n\u003Cth>Pages réindexées\u003C/th>\n\u003Cth>Trafic organique (sessions/mois)\u003C/th>\n\u003C/tr>\n\u003C/thead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>S0 (correction)\u003C/td>\n\u003Ctd>19 800 → baseline\u003C/td>\n\u003Ctd>19 000\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>S2\u003C/td>\n\u003Ctd>+1 200\u003C/td>\n\u003Ctd>32 000\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>S4\u003C/td>\n\u003Ctd>+3 400\u003C/td>\n\u003Ctd>68 000\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>S8\u003C/td>\n\u003Ctd>+6 800\u003C/td>\n\u003Ctd>112 000\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>S12\u003C/td>\n\u003Ctd>+8 000 (quasi-total)\u003C/td>\n\u003Ctd>155 000\u003C/td>\n\u003C/tr>\n\u003C/tbody>\n\u003C/table>\n\u003Cp>La récupération à 100% du trafic d'origine (185 000) n'est pas garantie. Pendant la période de désindexation, des concurrents ont pu capter des positions. Certaines queries long-tail perdues ne reviennent jamais à l'identique. Dans le cas de \u003Cem>TechDaily\u003C/em>, la récupération a plafonné à ~84% du trafic pré-migration après 16 semaines.\u003C/p>\n\u003Ch2>Les soft 404s générées côté client : le cas SPA et hydration\u003C/h2>\n\u003Cp>Les frameworks modernes ajoutent une couche de complexité. Sur un site en React SPA (sans SSR), Googlebot exécute le JavaScript et évalue le contenu rendu. Si le rendu côté client échoue silencieusement — API qui timeout, erreur de parsing JSON, composant qui crashe — le HTML rendu par Googlebot peut être un shell vide.\u003C/p>\n\u003Ch3>Le scénario d'hydration partielle\u003C/h3>\n\u003Cp>Avec Next.js App Router ou Nuxt 3, un pattern vicieux se produit quand le SSR fonctionne mais l'hydration côté client échoue. Google voit le HTML initial servi par le SSR (contenu complet), mais le Web Rendering Service (WRS) de Google exécute aussi le JavaScript et peut aboutir à un état différent si le client-side diverge.\u003C/p>\n\u003Cp>En pratique, Google se base principalement sur le HTML initial pour l'indexation (le SSR), mais le classificateur de soft 404 évalue le rendu complet. Si votre page fait un fetch côté client qui remplace le contenu SSR par un état d'erreur (parce que l'API est derrière un rate limiter qui bloque Googlebot, par exemple), vous pouvez vous retrouver avec une soft 404 alors que le HTML initial était correct.\u003C/p>\n\u003Cp>La vérification passe par Chrome DevTools avec le test \"User-Agent Googlebot\" :\u003C/p>\n\u003Col>\n\u003Cli>Ouvrez DevTools > Network conditions\u003C/li>\n\u003Cli>Désactivez le cache et réglez le User-Agent sur \u003Ccode>Googlebot/2.1\u003C/code>\u003C/li>\n\u003Cli>Chargez la page et vérifiez dans l'onglet Network que toutes les requêtes API retournent un 200\u003C/li>\n\u003Cli>Comparez le DOM final (Elements) avec le source HTML initial (View Source)\u003C/li>\n\u003C/ol>\n\u003Cp>Si le DOM final diffère significativement du HTML source — notamment si le contenu principal a disparu — vous avez un problème d'hydration qui peut déclencher une classification soft 404.\u003C/p>\n\u003Cp>Cette problématique rejoint directement les enjeux d'\u003Ca href=\"/blog/google-is-testing-new-bot-authorization-standard-via-sejournal-martinibuster\">autorisation des bots que Google teste actuellement\u003C/a> : si votre API ou votre CDN traite Googlebot différemment des utilisateurs humains (rate limiting, challenge JavaScript, blocage géographique), le contenu rendu pour le bot peut être substantiellement différent de ce que voit un utilisateur réel.\u003C/p>\n\u003Ch2>Les leçons structurelles : pourquoi les migrations génèrent systématiquement des soft 404s\u003C/h2>\n\u003Cp>Le problème n'est pas spécifique à un CMS ou un framework. Il est structurel. Lors d'une migration, trois conditions se combinent presque systématiquement :\u003C/p>\n\u003Cp>\u003Cstrong>1. Décalage temporel entre le basculement du front et la migration des données.\u003C/strong> Même avec une planification rigoureuse, il y a toujours des articles qui ne passent pas la migration automatique (caractères spéciaux dans les slugs, champs obligatoires manquants dans le nouveau schéma, médias non transférés). Ces articles deviennent des soft 404s silencieuses.\u003C/p>\n\u003Cp>\u003Cstrong>2. Absence de validation pré-déploiement exhaustive.\u003C/strong> Tester 50 URLs sur un site de 28 000 pages ne détecte rien. Il faut crawler l'intégralité du site en staging, comparer le contenu rendu page par page avec la production, et vérifier que chaque URL retourne un contenu substantiel.\u003C/p>\n\u003Cp>\u003Cstrong>3. Monitoring post-déploiement insuffisant.\u003C/strong> L'équipe vérifie que le site \"fonctionne\" (pas de 500, la homepage s'affiche), mais personne ne crawle systématiquement les 28 000 URLs dans les 24h suivant le déploiement pour vérifier que chacune renvoie du contenu.\u003C/p>\n\u003Cp>Le pattern récurrent : l'éditeur détecte le problème 3 à 6 semaines après la migration, quand la chute de trafic devient visible dans les dashboards analytics. À ce stade, Google a déjà désindexé les pages et réduit le crawl budget. La récupération prend ensuite 2 à 4 mois — un trimestre de revenus publicitaires perdu.\u003C/p>\n\u003Cp>C'est le même mécanisme de détection tardive que celui observé sur les \u003Ca href=\"/blog/google-s-march-core-update-shifted-visibility-away-from-aggregators-via-sejournal-mattgsouthern\">régressions de visibilité après les core updates de Google\u003C/a> : quand l'impact devient visible, le mal est fait. La seule défense efficace est un monitoring automatisé qui compare l'état du site crawl après crawl et alerte dès qu'une anomalie apparaît — avant que Google n'ait le temps de réagir.\u003C/p>\n\u003Cp>Les soft 404s ne sont pas un bug obscur. C'est la première cause de perte de trafic silencieuse après une migration, devant les redirections manquantes et les canonicals cassées. La différence entre un site qui récupère en 4 semaines et un site qui met 4 mois, c'est la vitesse de détection. Automatisez cette détection — avec Seogard ou avec vos propres scripts — mais ne la laissez jamais dépendre d'un humain qui ouvre Search Console une fois par semaine.\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,12,[18,19,20,21,22],"soft 404","indexation","migration","Search Console","SEO technique","Soft 404s et désindexation : autopsie d'un crash de trafic à -90%","Tue May 12 2026 15:03:01 GMT+0000 (Coordinated Universal Time)",[26,41,56],{"_id":27,"slug":28,"__v":6,"author":7,"canonical":29,"category":10,"createdAt":30,"date":31,"description":32,"image":15,"imageAlt":15,"readingTime":16,"tags":33,"title":39,"updatedAt":40},"6a041412aa6b273b0c40f181","how-to-build-local-pages-that-win-in-ai-powered-search-via-sejournal-lorenbaker","https://seogard.io/blog/how-to-build-local-pages-that-win-in-ai-powered-search-via-sejournal-lorenbaker","2026-05-13T06:02:58.743Z","2026-05-13","Guide technique pour construire des pages locales qui performent dans les AI Overviews et AI Mode. Schema, SSR, contenu structuré.",[34,35,36,37,38],"local SEO","AI search","pages locales","schema markup","SSR","Pages locales pour l'AI Search : architecture technique","Wed May 13 2026 06:02:58 GMT+0000 (Coordinated Universal Time)",{"_id":42,"slug":43,"__v":6,"author":7,"canonical":44,"category":10,"createdAt":45,"date":12,"description":46,"image":15,"imageAlt":15,"readingTime":47,"tags":48,"title":54,"updatedAt":55},"6a02c291aa6b273b0c2a74f9","the-tech-seo-audit-for-the-ai-search-era-how-to-maximize-your-ai-visibility-via-sejournal-jetoctopus","https://seogard.io/blog/the-tech-seo-audit-for-the-ai-search-era-how-to-maximize-your-ai-visibility-via-sejournal-jetoctopus","2026-05-12T06:02:57.339Z","Comment adapter votre audit technique SEO aux exigences des AI Overviews, du crawl par les LLMs et du grounding. Méthodes, code et scénarios concrets.",14,[49,50,51,52,53],"tech seo audit","ai search","ai visibility","crawl budget","structured data","Audit SEO technique pour l'ère AI Search : guide avancé","Tue May 12 2026 06:02:57 GMT+0000 (Coordinated Universal Time)",{"_id":57,"slug":58,"__v":6,"author":7,"canonical":59,"category":10,"createdAt":60,"date":12,"description":61,"image":15,"imageAlt":15,"readingTime":16,"tags":62,"title":67,"updatedAt":68},"6a02fac0aa6b273b0c58d096","the-consensus-gap-via-sejournal-kevin-indig","https://seogard.io/blog/the-consensus-gap-via-sejournal-kevin-indig","2026-05-12T10:02:40.519Z","Une marque peut dominer dans un dashboard AI agrégé et être absente de deux moteurs sur trois. Analyse technique du Consensus Gap et méthodes pour le détecter.",[63,35,64,65,66],"consensus gap","LLM visibility","GEO","multi-engine","The Consensus Gap : votre marque visible sur un LLM, invisible sur deux autres","Tue May 12 2026 10:02:40 GMT+0000 (Coordinated Universal Time)"]