[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fB1_1EMqg5i72lHji1-szzxvXn9p_d0fzFqUQ3hzbNqc":3,"$fbu04Jg_SubSk_6y8o8IO8iQFmXque9Z0y-pOoMKnMac":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},"69d7cdb2aa6b273b0c7f638b","api-first-et-seo-servir-du-contenu-crawlable-depuis-une-api",0,"Equipe Seogard","Un catalogue e-commerce de 22 000 fiches produits, alimenté par une API headless Commercetools, migré d'un monolithe Magento vers un front Next.js. Trois semaines après la mise en production, 40 % des pages produits disparaissent de l'index Google. La raison : le front appelle l'API côté client, Googlebot reçoit une coquille vide de `\u003Cdiv id=\"root\">\u003C/div>`, et le rendering JavaScript échoue silencieusement sur une partie du catalogue à cause d'un timeout API à 3 secondes que le WRS (Web Rendering Service) ne tolère pas. Ce scénario n'est pas hypothétique — c'est le pattern de défaillance le plus fréquent des architectures API-first mal préparées pour le crawl.\n\nL'architecture API-first offre une flexibilité inégalée pour découpler back-end et front-end. Mais cette flexibilité crée un fossé entre ce que voit un utilisateur (qui attend le chargement JavaScript) et ce que reçoit un crawler (qui a un budget temps et compute limité). Cet article détaille les patterns techniques concrets pour combler ce fossé.\n\n## Le problème fondamental : le crawler ne voit pas ce que l'utilisateur voit\n\n### Le modèle mental à corriger\n\nGooglebot utilise un pipeline en deux phases. La première phase (crawl) récupère le HTML brut via une requête HTTP classique. La deuxième phase (rendering) exécute le JavaScript dans une instance headless Chrome, mais cette exécution est **différée** et soumise à une file d'attente dont la latence varie de quelques secondes à plusieurs jours selon la priorité de la page. La [documentation officielle de Google sur le rendering JavaScript](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics) confirme explicitement ce modèle en deux phases.\n\nLe problème concret : si votre contenu dépend d'un appel API côté client (`fetch` dans un `useEffect`, `axios` dans un `mounted()`), ce contenu n'existe pas dans le HTML initial. Il ne sera visible qu'après la phase de rendering — si elle se passe bien.\n\n### Les trois modes de défaillance API-side\n\n**Timeout.** Le WRS de Google alloue un budget temps limité au rendering. Une API qui répond en 800 ms en conditions normales peut répondre en 4 secondes sous charge — et le WRS abandonne. Vous n'aurez aucune erreur dans la Search Console : la page sera simplement rendue avec un DOM incomplet.\n\n**Authentification implicite.** Les API headless utilisent souvent des tokens d'accès (API keys, JWT) injectés côté client. Si le token est conditionné à un cookie de session ou à un header custom, Googlebot ne l'aura pas. L'API retourne un 401, le front affiche un état vide, et la page est indexée sans contenu — un cas classique de [thin content](/blog/thin-content-quand-vos-pages-nuisent-au-seo-global).\n\n**Rate limiting.** Googlebot crawle de manière agressive. Un catalogue de 20 000 pages peut générer des centaines de requêtes API par minute si chaque page crawlée déclenche 2-3 appels API côté client via le WRS. L'API rate-limite, les pages sont rendues partiellement.\n\n## Pattern 1 : SSR systématique pour les pages indexables\n\nLa solution la plus fiable reste le Server-Side Rendering. Le principe : le serveur Node.js (ou edge function) appelle l'API, assemble le HTML complet, et le sert directement au crawler. Le JavaScript côté client prend le relais pour l'interactivité (hydratation), mais le contenu critique est déjà dans le HTML.\n\n### Implémentation Next.js avec appel API\n\nVoici un pattern concret pour une fiche produit alimentée par une API REST headless :\n\n```typescript\n// app/products/[slug]/page.tsx — Next.js App Router (SSR)\nimport { Metadata } from 'next';\nimport { notFound } from 'next/navigation';\n\ninterface Product {\n  slug: string;\n  name: string;\n  description: string;\n  price: number;\n  images: { url: string; alt: string }[];\n  category: string;\n  availability: 'in_stock' | 'out_of_stock';\n}\n\nasync function getProduct(slug: string): Promise\u003CProduct | null> {\n  const res = await fetch(\n    `${process.env.API_BASE_URL}/v2/products/${slug}`,\n    {\n      headers: {\n        'Authorization': `Bearer ${process.env.API_TOKEN}`,\n        'Accept': 'application/json',\n      },\n      // ISR : revalider toutes les 60 secondes\n      next: { revalidate: 60 },\n    }\n  );\n  \n  if (res.status === 404) return null;\n  if (!res.ok) throw new Error(`API error: ${res.status}`);\n  \n  return res.json();\n}\n\nexport async function generateMetadata(\n  { params }: { params: { slug: string } }\n): Promise\u003CMetadata> {\n  const product = await getProduct(params.slug);\n  if (!product) return {};\n  \n  return {\n    title: `${product.name} — Acheter en ligne | MonSite`,\n    description: product.description.slice(0, 155),\n    alternates: {\n      canonical: `https://monsite.fr/products/${product.slug}`,\n    },\n  };\n}\n\nexport default async function ProductPage(\n  { params }: { params: { slug: string } }\n) {\n  const product = await getProduct(params.slug);\n  if (!product) notFound();\n\n  const jsonLd = {\n    '@context': 'https://schema.org',\n    '@type': 'Product',\n    name: product.name,\n    description: product.description,\n    image: product.images.map(img => img.url),\n    offers: {\n      '@type': 'Offer',\n      price: product.price,\n      priceCurrency: 'EUR',\n      availability: product.availability === 'in_stock'\n        ? 'https://schema.org/InStock'\n        : 'https://schema.org/OutOfStock',\n    },\n  };\n\n  return (\n    \u003C>\n      \u003Cscript\n        type=\"application/ld+json\"\n        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}\n      />\n      \u003Carticle>\n        \u003Ch1>{product.name}\u003C/h1>\n        \u003Cp className=\"price\">{product.price} €\u003C/p>\n        \u003Cdiv \n          className=\"description\"\n          dangerouslySetInnerHTML={{ __html: product.description }}\n        />\n        {/* Le contenu est dans le HTML initial — crawlable immédiatement */}\n      \u003C/article>\n    \u003C/>\n  );\n}\n```\n\nTrois points critiques dans ce code :\n\n1. **L'appel API se fait côté serveur** avec un token stocké en variable d'environnement. Googlebot ne voit jamais le token, et l'API ne reçoit jamais de requête depuis le WRS.\n2. **`next: { revalidate: 60 }`** active l'ISR (Incremental Static Regeneration). La page est servie depuis le cache, puis revalidée en arrière-plan toutes les 60 secondes. Pour un catalogue de 22 000 pages, cela évite de regénérer l'intégralité du site à chaque déploiement.\n3. **Le JSON-LD est injecté dans le HTML initial**. Les données structurées doivent être présentes dans le HTML servi, pas générées côté client — même si Google affirme pouvoir les lire après rendering JavaScript, la fiabilité est significativement meilleure en SSR.\n\n### Quand le SSR systématique n'est pas viable\n\nLe SSR a un coût serveur. Pour un site média qui génère 500 articles par jour et reçoit 2 millions de pages vues quotidiennes, le compute nécessaire peut devenir significatif. C'est là que l'ISR ou le SSG (Static Site Generation) avec revalidation prennent tout leur sens. Le choix dépend de la fréquence de mise à jour du contenu et du volume de pages — un arbitrage détaillé dans notre article sur les [architectures headless CMS](/blog/headless-cms-et-seo-avantages-et-risques-techniques).\n\n## Pattern 2 : le cache HTTP comme couche de résilience SEO\n\nL'API est un point de défaillance unique (SPOF). Si elle tombe, toutes vos pages deviennent des coquilles vides. La couche de cache HTTP entre votre front et l'API n'est pas un luxe — c'est une assurance contre la désindexation.\n\n### Configuration Nginx comme reverse proxy cache devant l'API\n\n```nginx\n# /etc/nginx/conf.d/api-cache.conf\n\nproxy_cache_path /var/cache/nginx/api levels=1:2 \n  keys_zone=api_cache:64m max_size=2g inactive=24h use_temp_path=off;\n\nserver {\n    listen 8080;\n    server_name api-cache.internal;\n\n    location /v2/products/ {\n        proxy_pass https://api.headless-commerce.io;\n        \n        proxy_cache api_cache;\n        proxy_cache_valid 200 15m;        # Cache 200 OK pendant 15 min\n        proxy_cache_valid 404 1m;         # Cache 404 pendant 1 min (produit supprimé)\n        proxy_cache_valid any 0;          # Ne pas cacher les erreurs 5xx\n        \n        proxy_cache_use_stale error timeout updating http_500 http_502 http_503;\n        proxy_cache_lock on;              # Un seul request upstream en cas de cache miss simultané\n        proxy_cache_lock_timeout 5s;\n        \n        proxy_cache_key \"$scheme$request_method$host$request_uri\";\n        \n        add_header X-Cache-Status $upstream_cache_status always;\n        \n        proxy_set_header Authorization \"Bearer ${API_TOKEN}\";\n        proxy_set_header Accept \"application/json\";\n        proxy_connect_timeout 3s;\n        proxy_read_timeout 5s;\n    }\n}\n```\n\nLa directive `proxy_cache_use_stale` est la clé. Elle dit à Nginx : \"Si l'API retourne une erreur 500 ou ne répond pas, sers la version cachée précédente.\" Du point de vue de Googlebot, la page reste intacte même pendant une panne API de plusieurs heures.\n\nLe header `X-Cache-Status` (valeurs possibles : `HIT`, `MISS`, `STALE`, `BYPASS`) est précieux pour le debugging. En crawlant votre site avec Screaming Frog et en extrayant ce header custom, vous pouvez identifier les pages qui sont systématiquement en `MISS` — signe que le cache n'est pas correctement dimensionné ou que l'URL contient des paramètres qui fragmentent le cache.\n\nPour les architectures CDN, les mêmes principes s'appliquent via [les edge workers](/blog/edge-seo-modifier-les-reponses-http-au-niveau-cdn) — Cloudflare Workers ou Fastly VCL permettent d'implémenter une logique stale-while-revalidate directement au niveau du CDN.\n\n## Pattern 3 : le fallback HTML statique pour les contenus critiques\n\nIl existe un troisième pattern, souvent négligé, qui combine les avantages du SSG et du SSR : pré-générer un HTML statique minimal pour chaque page indexable, et l'enrichir côté client avec les données temps réel de l'API.\n\n### Le principe\n\nPour une fiche produit, les éléments critiques pour le SEO (titre, description, images, données structurées) changent rarement. Le prix et la disponibilité changent fréquemment, mais ne sont pas des facteurs de ranking directs.\n\nL'idée : générer un HTML statique contenant le contenu SEO-critique lors du build ou via ISR, et charger les données volatiles (prix en temps réel, stock, avis récents) via un appel API côté client. Les crawlers voient le contenu stable ; les utilisateurs voient les données fraîches après hydratation.\n\n```typescript\n// Composant hybride : contenu statique SSR + données live côté client\n'use client';\n\nimport { useEffect, useState } from 'react';\n\ninterface LiveData {\n  price: number;\n  stock: number;\n  reviewCount: number;\n  averageRating: number;\n}\n\ninterface Props {\n  // Ces props viennent du SSR — présentes dans le HTML initial\n  productName: string;\n  productDescription: string;\n  staticPrice: number; // Prix au moment du build/ISR\n  slug: string;\n}\n\nexport function ProductLiveData({ productName, productDescription, staticPrice, slug }: Props) {\n  const [liveData, setLiveData] = useState\u003CLiveData | null>(null);\n\n  useEffect(() => {\n    // Cet appel API ne s'exécute que côté client\n    // Googlebot verra staticPrice dans le HTML initial\n    fetch(`/api/products/${slug}/live`)\n      .then(res => res.json())\n      .then(setLiveData)\n      .catch(() => {\n        // En cas d'échec, le prix statique reste affiché\n        // Pas de contenu manquant, jamais\n      });\n  }, [slug]);\n\n  const displayPrice = liveData?.price ?? staticPrice;\n\n  return (\n    \u003Cdiv>\n      {/* Ce contenu est dans le HTML SSR */}\n      \u003Ch1>{productName}\u003C/h1>\n      \u003Cp>{productDescription}\u003C/p>\n      \n      {/* Le prix affiché est le prix statique (SSR) puis mis à jour (client) */}\n      \u003Cspan className=\"price\" data-live={!!liveData}>\n        {displayPrice.toFixed(2)} €\n      \u003C/span>\n      \n      {/* Les avis sont chargés côté client uniquement */}\n      {liveData && (\n        \u003Cdiv className=\"reviews\">\n          {liveData.averageRating}/5 ({liveData.reviewCount} avis)\n        \u003C/div>\n      )}\n    \u003C/div>\n  );\n}\n```\n\nLe trade-off est explicite : les avis ne seront pas dans le HTML initial. Si les avis sont un vecteur SEO important (rich snippets, contenu unique), il faut les inclure dans le SSR. Si ce sont des données purement UX, le chargement côté client est acceptable.\n\nC'est un arbitrage page par page. Un article sur [le contenu dupliqué](/blog/contenu-duplique-causes-techniques-et-solutions) ne se gère pas de la même manière qu'une page de listing avec [infinite scroll](/blog/infinite-scroll-et-seo-le-guide-technique).\n\n## Scénario concret : migration API-first d'un site média (8 500 pages)\n\n### Le contexte\n\nUn site média B2B avec 8 500 articles, hébergé sur un WordPress monolithique. L'équipe technique migre vers une architecture headless : WordPress reste le CMS (back-end éditorial), une API REST WordPress alimente un front Nuxt 3 déployé sur Vercel.\n\n**Avant migration** : WordPress sert du HTML complet. Toutes les pages sont indexées. Le site reçoit 180 000 sessions organiques par mois.\n\n### Les erreurs commises (et détectées après coup)\n\n**Semaine 1 post-migration.** Le rapport \"Pages\" de la Search Console affiche une augmentation de 3 200 pages en statut \"Discovered — currently not indexed.\" L'équipe panique.\n\nL'investigation révèle trois problèmes simultanés :\n\n1. **Sitemap non mis à jour.** L'ancien sitemap WordPress XML pointait vers les URLs de l'ancien domaine. Le nouveau front Nuxt n'avait pas de sitemap généré. Résultat : Googlebot découvrait les nouvelles pages uniquement via les liens internes, mais la structure de [mega menu](/blog/mega-menus-et-seo-attention-au-crawl-budget) ne liait pas les articles de plus de 6 mois.\n\n2. **Meta tags générés côté client.** Le composant `useHead()` de Nuxt 3 était utilisé dans un composant marqué `'use client'` (par erreur — un import d'un composant interactif contaminait l'arbre). Les balises `\u003Ctitle>` et `\u003Cmeta description>` n'étaient pas dans le HTML initial.\n\n3. **L'API WordPress rate-limitait Googlebot.** Le WordPress headless tournait sur un serveur 2 vCPU / 4 GB RAM. Quand Googlebot crawlait 15 pages par seconde (comportement normal pour un site de cette taille), chaque page déclenchait 3 appels API (article + catégorie + articles liés). Le serveur WordPress atteignait 45 requêtes API par seconde et commençait à retourner des 429.\n\n### La résolution\n\n**Pour le sitemap** : génération dynamique via une route API Nuxt qui interroge l'API WordPress et génère un sitemap XML avec `lastmod` basé sur `modified_date`. Soumission via Search Console.\n\n**Pour les meta tags** : audit systématique avec un crawl Screaming Frog en mode \"JavaScript rendering\" vs \"HTML brut\". Toute page où le `\u003Ctitle>` diffère entre les deux modes est flaggée. Correction : déplacer les `useHead()` dans les composants serveur de Nuxt (`definePageMeta` + `useAsyncData` dans le `setup` du composant page, pas dans un composant enfant client).\n\n**Pour le rate limiting** : mise en place du reverse proxy cache Nginx (pattern décrit plus haut) devant l'API WordPress avec `proxy_cache_valid 200 10m`. Ajout d'un `Cache-Control: public, s-maxage=600` sur les réponses API WordPress. Résultat : le cache absorbe 95 % des requêtes Googlebot, le serveur WordPress ne reçoit plus que 2-3 requêtes par seconde en pic de crawl.\n\n### Les métriques de récupération\n\n- **Semaine 2** : les pages \"Discovered — not indexed\" commencent à diminuer.\n- **Semaine 5** : retour au niveau d'indexation pré-migration (8 200 pages indexées sur 8 500).\n- **Semaine 8** : le trafic organique remonte à 165 000 sessions (vs 180 000 avant). Le delta de 15 000 sessions est attribué à la perte de PageRank temporaire liée aux redirections 301 et à la réindexation progressive.\n- **Semaine 14** : 192 000 sessions. Le gain net est attribué aux Core Web Vitals améliorés (LCP passé de 3.8s sous WordPress à 1.2s sous Nuxt/Vercel) et aux données structurées JSON-LD ajoutées systématiquement (absentes sous l'ancien WordPress).\n\nCe type de régression silencieuse — meta tags disparues, pages rendues partiellement — est exactement ce qu'un monitoring continu comme Seogard détecte en temps réel, avant que la Search Console ne vous le signale 2 semaines plus tard.\n\n## Validation et monitoring : fermer la boucle\n\n### Tester le rendering avant la mise en production\n\nLe test unitaire du rendering SSR devrait faire partie de votre pipeline CI/CD. Voici un test minimaliste avec Playwright qui vérifie que le HTML servi contient les éléments SEO critiques :\n\n```typescript\n// tests/seo-ssr.spec.ts — Playwright\nimport { test, expect } from '@playwright/test';\n\nconst CRITICAL_PAGES = [\n  '/products/chaussure-running-pro-x1',\n  '/products/montre-connectee-sport-v3',\n  '/category/equipement-running',\n];\n\nfor (const path of CRITICAL_PAGES) {\n  test(`SSR SEO check: ${path}`, async ({ request }) => {\n    // Requête HTTP brute — pas de rendering JS\n    // C'est ce que Googlebot voit en première phase\n    const response = await request.get(path);\n    const html = await response.text();\n    \n    // Le title doit être dans le HTML initial\n    expect(html).toMatch(/\u003Ctitle>[^\u003C]{10,60}\u003C\\/title>/);\n    \n    // La meta description doit être présente\n    expect(html).toMatch(\n      /\u003Cmeta name=\"description\" content=\"[^\"]{50,160}\">/\n    );\n    \n    // Le canonical doit pointer vers la bonne URL\n    expect(html).toContain(\n      `\u003Clink rel=\"canonical\" href=\"https://monsite.fr${path}\"`\n    );\n    \n    // Le contenu principal doit être dans le HTML (pas derrière un fetch client)\n    expect(html).toMatch(/\u003Ch1>[^\u003C]+\u003C\\/h1>/);\n    \n    // Le JSON-LD doit être présent\n    expect(html).toContain('\"@type\":\"Product\"');\n    \n    // Vérifier l'absence de signes de rendering client-only\n    expect(html).not.toContain('\u003Cdiv id=\"__next\">\u003C/div>'); // Coquille vide Next.js\n    expect(html).not.toContain('loading...'); // Placeholder non résolu\n  });\n}\n```\n\nCe test s'exécute sans navigateur headless — il fait une requête HTTP brute et analyse le HTML retourné. C'est exactement ce que Googlebot fait en première phase. Intégrez-le dans votre [pipeline CI/CD](/blog/automatiser-les-checks-seo-dans-le-ci-cd) : chaque déploiement qui casse le SSR est bloqué avant d'atteindre la production.\n\n### Les signaux à surveiller en production\n\n**Search Console — rapport \"Pages\".** Surveillez spécifiquement les statuts \"Crawled — currently not indexed\" et \"Discovered — currently not indexed.\" Une augmentation soudaine après un déploiement indique un problème de rendering. Les [rapports souvent ignorés](/blog/google-search-console-les-rapports-que-vous-ignorez) de la Search Console donnent des indices précieux, mais avec un délai de 2 à 5 jours.\n\n**Chrome DevTools — onglet Network.** Pour diagnostiquer ce que Googlebot voit, utilisez les [fonctionnalités avancées de Chrome DevTools](/blog/chrome-devtools-pour-le-seo-astuces-avancees) : désactivez JavaScript, rechargez la page. Si le contenu principal disparaît, votre page dépend du rendering côté client. C'est le test le plus rapide et le plus fiable en phase de développement.\n\n**Screaming Frog — crawl comparatif.** Configurez deux crawls du même périmètre : un en mode \"HTML brut\" et un en mode \"JavaScript rendering.\" Exportez les deux rapports, comparez les `\u003Ctitle>`, les `\u003Cmeta description>`, et les `\u003Ch1>`. Toute divergence est un bug SSR.\n\n**Logs serveur.** Analysez les logs d'accès de votre front-end. Filtrez sur le User-Agent Googlebot. Vérifiez les codes de réponse : des 500 ou 503 intermittents indiquent que l'API upstream défaille et que votre SSR ne gère pas correctement les erreurs (il devrait retourner un 503 avec un `Retry-After` header, pas un 200 avec un contenu vide).\n\n## Les edge cases que personne ne mentionne\n\n### Contenu API paginé et crawl budget\n\nUn catalogue de 22 000 produits avec une API qui retourne 50 produits par page = 440 pages de listing. Si chaque page de listing est un appel API distinct et que vous les rendez toutes en SSR, c'est 440 pages que Googlebot doit crawler. Ajoutez les pages produits individuelles, et votre [crawl budget](/blog/mega-menus-et-seo-attention-au-crawl-budget) explose.\n\nLa solution : ne rendez en SSR que les pages de listing qui ont une valeur SEO réelle (pages 1 à 3 de chaque catégorie, qui concentrent 95 % du trafic). Les pages de listing au-delà de la page 5 peuvent être des `noindex, follow` — elles servent de tremplin de découverte pour les crawlers, pas de pages de destination.\n\n### API versionnée et URLs cassées\n\nLes APIs headless évoluent. Un passage de `/v2/products/` à `/v3/products/` avec un changement de structure de réponse peut casser silencieusement votre SSR si le front-end attend un champ `description` qui a été renommé en `body` dans la v3. Le SSR ne plante pas — il rend une page avec une description vide. Googlebot indexe cette page comme du [contenu généré automatiquement](/blog/contenu-genere-automatiquement-et-seo-ce-que-google-accepte) de mauvaise qualité.\n\nProtégez-vous avec une validation de schéma côté serveur (Zod, Joi, ou un simple type guard TypeScript) qui lève une erreur explicite si la réponse API ne correspond pas au format attendu. Mieux vaut un 500 propre qu'un 200 avec du contenu tronqué.\n\n### Le piège des previews et des environnements de staging\n\nLes environnements de preview (Vercel Preview Deployments, Netlify Deploy Previews) génèrent des URLs uniques par branche ou par commit. Si ces URLs ne sont pas protégées par un `noindex` ou une authentification, elles peuvent être crawlées et indexées — créant du [contenu dupliqué](/blog/contenu-duplique-causes-techniques-et-solutions) massif. Vercel ajoute automatiquement un header `X-Robots-Tag: noindex` sur les preview deployments, mais ce n'est pas le cas de toutes les plateformes. Vérifiez.\n\n## Le contenu API-driven face aux crawlers IA\n\nAvec l'explosion du trafic des bots IA — [ChatGPT crawle désormais 3,6 fois plus que Googlebot](/blog/chatgpt-now-crawls-3-6x-more-than-googlebot-what-24m-requests-reveal) sur certains sites — votre architecture API-first doit aussi tenir compte de ces crawlers. GPTBot, ClaudeBot et consorts ne font pas de rendering JavaScript. Ils ne voient que le HTML brut. Si votre contenu n'est pas dans le HTML initial, il n'existe pas pour les LLMs qui alimentent les réponses conversationnelles.\n\nC'est un argument supplémentaire — et de plus en plus décisif — en faveur du SSR systématique pour tout contenu que vous souhaitez voir apparaître dans les [réponses des systèmes IA](/blog/how-to-design-content-that-ai-systems-prefer-and-promote).\n\n## La takeaway\n\nL'architecture API-first n'est pas incompatible avec le SEO — elle exige simplement que le rendering côté serveur soit traité comme une infrastructure critique, au même titre que la base de données ou le CDN. Le pattern gagnant : SSR pour le contenu indexable, cache HTTP résilient devant l'API, tests automatisés du HTML brut dans le CI/CD, et monitoring continu des régressions. Un outil comme Seogard permet de détecter en temps réel les pages dont le contenu SSR a disparu — avant que Google ne les désindexe silencieusement.","https://seogard.io/blog/api-first-et-seo-servir-du-contenu-crawlable-depuis-une-api","Architecture","2026-04-09T16:02:58.656Z","2026-04-09","Patterns techniques pour servir du contenu SEO-friendly depuis une architecture API-first : SSR, ISR, stale-while-revalidate et monitoring.","\u003Cp>Un catalogue e-commerce de 22 000 fiches produits, alimenté par une API headless Commercetools, migré d'un monolithe Magento vers un front Next.js. Trois semaines après la mise en production, 40 % des pages produits disparaissent de l'index Google. La raison : le front appelle l'API côté client, Googlebot reçoit une coquille vide de \u003Ccode>&#x3C;div id=\"root\">&#x3C;/div>\u003C/code>, et le rendering JavaScript échoue silencieusement sur une partie du catalogue à cause d'un timeout API à 3 secondes que le WRS (Web Rendering Service) ne tolère pas. Ce scénario n'est pas hypothétique — c'est le pattern de défaillance le plus fréquent des architectures API-first mal préparées pour le crawl.\u003C/p>\n\u003Cp>L'architecture API-first offre une flexibilité inégalée pour découpler back-end et front-end. Mais cette flexibilité crée un fossé entre ce que voit un utilisateur (qui attend le chargement JavaScript) et ce que reçoit un crawler (qui a un budget temps et compute limité). Cet article détaille les patterns techniques concrets pour combler ce fossé.\u003C/p>\n\u003Ch2>Le problème fondamental : le crawler ne voit pas ce que l'utilisateur voit\u003C/h2>\n\u003Ch3>Le modèle mental à corriger\u003C/h3>\n\u003Cp>Googlebot utilise un pipeline en deux phases. La première phase (crawl) récupère le HTML brut via une requête HTTP classique. La deuxième phase (rendering) exécute le JavaScript dans une instance headless Chrome, mais cette exécution est \u003Cstrong>différée\u003C/strong> et soumise à une file d'attente dont la latence varie de quelques secondes à plusieurs jours selon la priorité de la page. La \u003Ca href=\"https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics\">documentation officielle de Google sur le rendering JavaScript\u003C/a> confirme explicitement ce modèle en deux phases.\u003C/p>\n\u003Cp>Le problème concret : si votre contenu dépend d'un appel API côté client (\u003Ccode>fetch\u003C/code> dans un \u003Ccode>useEffect\u003C/code>, \u003Ccode>axios\u003C/code> dans un \u003Ccode>mounted()\u003C/code>), ce contenu n'existe pas dans le HTML initial. Il ne sera visible qu'après la phase de rendering — si elle se passe bien.\u003C/p>\n\u003Ch3>Les trois modes de défaillance API-side\u003C/h3>\n\u003Cp>\u003Cstrong>Timeout.\u003C/strong> Le WRS de Google alloue un budget temps limité au rendering. Une API qui répond en 800 ms en conditions normales peut répondre en 4 secondes sous charge — et le WRS abandonne. Vous n'aurez aucune erreur dans la Search Console : la page sera simplement rendue avec un DOM incomplet.\u003C/p>\n\u003Cp>\u003Cstrong>Authentification implicite.\u003C/strong> Les API headless utilisent souvent des tokens d'accès (API keys, JWT) injectés côté client. Si le token est conditionné à un cookie de session ou à un header custom, Googlebot ne l'aura pas. L'API retourne un 401, le front affiche un état vide, et la page est indexée sans contenu — un cas classique de \u003Ca href=\"/blog/thin-content-quand-vos-pages-nuisent-au-seo-global\">thin content\u003C/a>.\u003C/p>\n\u003Cp>\u003Cstrong>Rate limiting.\u003C/strong> Googlebot crawle de manière agressive. Un catalogue de 20 000 pages peut générer des centaines de requêtes API par minute si chaque page crawlée déclenche 2-3 appels API côté client via le WRS. L'API rate-limite, les pages sont rendues partiellement.\u003C/p>\n\u003Ch2>Pattern 1 : SSR systématique pour les pages indexables\u003C/h2>\n\u003Cp>La solution la plus fiable reste le Server-Side Rendering. Le principe : le serveur Node.js (ou edge function) appelle l'API, assemble le HTML complet, et le sert directement au crawler. Le JavaScript côté client prend le relais pour l'interactivité (hydratation), mais le contenu critique est déjà dans le HTML.\u003C/p>\n\u003Ch3>Implémentation Next.js avec appel API\u003C/h3>\n\u003Cp>Voici un pattern concret pour une fiche produit alimentée par une API REST headless :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// app/products/[slug]/page.tsx — Next.js App Router (SSR)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { Metadata } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'next'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { notFound } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'next/navigation'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\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\"> Product\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  slug\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  name\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  description\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  price\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> number\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  images\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#FFAB70\">url\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; \u003C/span>\u003Cspan style=\"color:#FFAB70\">alt\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> }[];\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  category\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  availability\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'in_stock'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'out_of_stock'\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\">async\u003C/span>\u003Cspan style=\"color:#F97583\"> function\u003C/span>\u003Cspan style=\"color:#B392F0\"> getProduct\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">slug\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\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\">Product\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#79B8FF\"> null\u003C/span>\u003Cspan style=\"color:#E1E4E8\">> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> res\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    `${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">process\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">env\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#79B8FF\">API_BASE_URL\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}/v2/products/${\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:#E1E4E8\">    {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      headers: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        'Authorization'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">`Bearer ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">process\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">env\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#79B8FF\">API_TOKEN\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        'Accept'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'application/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:#6A737D\">      // ISR : revalider toutes les 60 secondes\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      next: { revalidate: \u003C/span>\u003Cspan style=\"color:#79B8FF\">60\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (res.status \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 404\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">return\u003C/span>\u003Cspan style=\"color:#79B8FF\"> null\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\">res.ok) \u003C/span>\u003Cspan style=\"color:#F97583\">throw\u003C/span>\u003Cspan style=\"color:#F97583\"> new\u003C/span>\u003Cspan style=\"color:#B392F0\"> Error\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`API error: ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">res\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">status\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  return\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\">\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\"> generateMetadata\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  { \u003C/span>\u003Cspan style=\"color:#FFAB70\">params\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> }\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#FFAB70\">params\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#FFAB70\">slug\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> Promise\u003C/span>\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#B392F0\">Metadata\u003C/span>\u003Cspan style=\"color:#E1E4E8\">> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> product\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> getProduct\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(params.slug);\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\">product) \u003C/span>\u003Cspan style=\"color:#F97583\">return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {};\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    title: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">`${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">product\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">name\u003C/span>\u003Cspan style=\"color:#9ECBFF\">} — Acheter en ligne | MonSite`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    description: product.description.\u003C/span>\u003Cspan style=\"color:#B392F0\">slice\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">155\u003C/span>\u003Cspan style=\"color:#E1E4E8\">),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    alternates: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      canonical: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">`https://monsite.fr/products/${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">product\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> default\u003C/span>\u003Cspan style=\"color:#F97583\"> async\u003C/span>\u003Cspan style=\"color:#F97583\"> function\u003C/span>\u003Cspan style=\"color:#B392F0\"> ProductPage\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  { \u003C/span>\u003Cspan style=\"color:#FFAB70\">params\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> }\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#FFAB70\">params\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#FFAB70\">slug\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> product\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> getProduct\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(params.slug);\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\">product) \u003C/span>\u003Cspan style=\"color:#B392F0\">notFound\u003C/span>\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\"> jsonLd\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    '@context'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'https://schema.org'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    '@type'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Product'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    name: product.name,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    description: product.description,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    image: product.images.\u003C/span>\u003Cspan style=\"color:#B392F0\">map\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">img\u003C/span>\u003Cspan style=\"color:#F97583\"> =>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> img.url),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    offers: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">      '@type'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Offer'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      price: product.price,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      priceCurrency: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'EUR'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      availability: product.availability \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'in_stock'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        ?\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'https://schema.org/InStock'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        :\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'https://schema.org/OutOfStock'\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\">\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:#F97583\">    &#x3C;>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#FFAB70\">script\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        type\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"application/ld+json\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        dangerouslySetInnerHTML\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{{ \u003C/span>\u003Cspan style=\"color:#B392F0\">__html\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">JSON\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#B392F0\">stringify\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(jsonLd) }}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#B392F0\">article\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        &#x3C;\u003C/span>\u003Cspan style=\"color:#B392F0\">h1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>{product.name}\u003C/span>\u003Cspan style=\"color:#F97583\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">h1\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        &#x3C;\u003C/span>\u003Cspan style=\"color:#E1E4E8\">p className\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"price\"\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{product.price} €\u003C/span>\u003Cspan style=\"color:#F97583\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">p\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        &#x3C;\u003C/span>\u003Cspan style=\"color:#FFAB70\">div\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          className\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"description\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          dangerouslySetInnerHTML\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{{ \u003C/span>\u003Cspan style=\"color:#B392F0\">__html\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: product.description }}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        {\u003C/span>\u003Cspan style=\"color:#6A737D\">/* Le contenu est dans le HTML initial — crawlable immédiatement */\u003C/span>\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      &#x3C;/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">article\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    &#x3C;/>\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>Trois points critiques dans ce code :\u003C/p>\n\u003Col>\n\u003Cli>\u003Cstrong>L'appel API se fait côté serveur\u003C/strong> avec un token stocké en variable d'environnement. Googlebot ne voit jamais le token, et l'API ne reçoit jamais de requête depuis le WRS.\u003C/li>\n\u003Cli>\u003Cstrong>\u003Ccode>next: { revalidate: 60 }\u003C/code>\u003C/strong> active l'ISR (Incremental Static Regeneration). La page est servie depuis le cache, puis revalidée en arrière-plan toutes les 60 secondes. Pour un catalogue de 22 000 pages, cela évite de regénérer l'intégralité du site à chaque déploiement.\u003C/li>\n\u003Cli>\u003Cstrong>Le JSON-LD est injecté dans le HTML initial\u003C/strong>. Les données structurées doivent être présentes dans le HTML servi, pas générées côté client — même si Google affirme pouvoir les lire après rendering JavaScript, la fiabilité est significativement meilleure en SSR.\u003C/li>\n\u003C/ol>\n\u003Ch3>Quand le SSR systématique n'est pas viable\u003C/h3>\n\u003Cp>Le SSR a un coût serveur. Pour un site média qui génère 500 articles par jour et reçoit 2 millions de pages vues quotidiennes, le compute nécessaire peut devenir significatif. C'est là que l'ISR ou le SSG (Static Site Generation) avec revalidation prennent tout leur sens. Le choix dépend de la fréquence de mise à jour du contenu et du volume de pages — un arbitrage détaillé dans notre article sur les \u003Ca href=\"/blog/headless-cms-et-seo-avantages-et-risques-techniques\">architectures headless CMS\u003C/a>.\u003C/p>\n\u003Ch2>Pattern 2 : le cache HTTP comme couche de résilience SEO\u003C/h2>\n\u003Cp>L'API est un point de défaillance unique (SPOF). Si elle tombe, toutes vos pages deviennent des coquilles vides. La couche de cache HTTP entre votre front et l'API n'est pas un luxe — c'est une assurance contre la désindexation.\u003C/p>\n\u003Ch3>Configuration Nginx comme reverse proxy cache devant l'API\u003C/h3>\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/api-cache.conf\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">proxy_cache_path \u003C/span>\u003Cspan style=\"color:#E1E4E8\">/var/cache/nginx/api levels=1:2 \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  keys_zone=api_cache:64m max_size=2g inactive=24h use_temp_path=off;\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:#F97583\">    listen \u003C/span>\u003Cspan style=\"color:#79B8FF\">8080\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    server_name \u003C/span>\u003Cspan style=\"color:#E1E4E8\">api-cache.internal;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    location\u003C/span>\u003Cspan style=\"color:#B392F0\"> /v2/products/ \u003C/span>\u003Cspan style=\"color:#E1E4E8\">{\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        proxy_pass \u003C/span>\u003Cspan style=\"color:#E1E4E8\">https://api.headless-commerce.io;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        proxy_cache \u003C/span>\u003Cspan style=\"color:#E1E4E8\">api_cache;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        proxy_cache_valid \u003C/span>\u003Cspan style=\"color:#79B8FF\">200\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 15m\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;        \u003C/span>\u003Cspan style=\"color:#6A737D\"># Cache 200 OK pendant 15 min\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        proxy_cache_valid \u003C/span>\u003Cspan style=\"color:#79B8FF\">404\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 1m\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;         \u003C/span>\u003Cspan style=\"color:#6A737D\"># Cache 404 pendant 1 min (produit supprimé)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        proxy_cache_valid \u003C/span>\u003Cspan style=\"color:#E1E4E8\">any \u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;          \u003C/span>\u003Cspan style=\"color:#6A737D\"># Ne pas cacher les erreurs 5xx\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        proxy_cache_use_stale \u003C/span>\u003Cspan style=\"color:#79B8FF\">error\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> timeout updating http_500 http_502 http_503;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        proxy_cache_lock \u003C/span>\u003Cspan style=\"color:#79B8FF\">on\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;              \u003C/span>\u003Cspan style=\"color:#6A737D\"># Un seul request upstream en cas de cache miss simultané\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        proxy_cache_lock_timeout \u003C/span>\u003Cspan style=\"color:#79B8FF\">5s\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\">        proxy_cache_key \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"$\u003C/span>\u003Cspan style=\"color:#E1E4E8\">scheme\u003C/span>\u003Cspan style=\"color:#9ECBFF\">$\u003C/span>\u003Cspan style=\"color:#E1E4E8\">request_method\u003C/span>\u003Cspan style=\"color:#9ECBFF\">$\u003C/span>\u003Cspan style=\"color:#E1E4E8\">host\u003C/span>\u003Cspan style=\"color:#9ECBFF\">$\u003C/span>\u003Cspan style=\"color:#E1E4E8\">request_uri\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        add_header \u003C/span>\u003Cspan style=\"color:#E1E4E8\">X-Cache-Status $upstream_cache_status always;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        proxy_set_header \u003C/span>\u003Cspan style=\"color:#E1E4E8\">Authorization \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"Bearer ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">API_TOKEN\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        proxy_set_header \u003C/span>\u003Cspan style=\"color:#E1E4E8\">Accept \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"application/json\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        proxy_connect_timeout \u003C/span>\u003Cspan style=\"color:#79B8FF\">3s\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        proxy_read_timeout \u003C/span>\u003Cspan style=\"color:#79B8FF\">5s\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 directive \u003Ccode>proxy_cache_use_stale\u003C/code> est la clé. Elle dit à Nginx : \"Si l'API retourne une erreur 500 ou ne répond pas, sers la version cachée précédente.\" Du point de vue de Googlebot, la page reste intacte même pendant une panne API de plusieurs heures.\u003C/p>\n\u003Cp>Le header \u003Ccode>X-Cache-Status\u003C/code> (valeurs possibles : \u003Ccode>HIT\u003C/code>, \u003Ccode>MISS\u003C/code>, \u003Ccode>STALE\u003C/code>, \u003Ccode>BYPASS\u003C/code>) est précieux pour le debugging. En crawlant votre site avec Screaming Frog et en extrayant ce header custom, vous pouvez identifier les pages qui sont systématiquement en \u003Ccode>MISS\u003C/code> — signe que le cache n'est pas correctement dimensionné ou que l'URL contient des paramètres qui fragmentent le cache.\u003C/p>\n\u003Cp>Pour les architectures CDN, les mêmes principes s'appliquent via \u003Ca href=\"/blog/edge-seo-modifier-les-reponses-http-au-niveau-cdn\">les edge workers\u003C/a> — Cloudflare Workers ou Fastly VCL permettent d'implémenter une logique stale-while-revalidate directement au niveau du CDN.\u003C/p>\n\u003Ch2>Pattern 3 : le fallback HTML statique pour les contenus critiques\u003C/h2>\n\u003Cp>Il existe un troisième pattern, souvent négligé, qui combine les avantages du SSG et du SSR : pré-générer un HTML statique minimal pour chaque page indexable, et l'enrichir côté client avec les données temps réel de l'API.\u003C/p>\n\u003Ch3>Le principe\u003C/h3>\n\u003Cp>Pour une fiche produit, les éléments critiques pour le SEO (titre, description, images, données structurées) changent rarement. Le prix et la disponibilité changent fréquemment, mais ne sont pas des facteurs de ranking directs.\u003C/p>\n\u003Cp>L'idée : générer un HTML statique contenant le contenu SEO-critique lors du build ou via ISR, et charger les données volatiles (prix en temps réel, stock, avis récents) via un appel API côté client. Les crawlers voient le contenu stable ; les utilisateurs voient les données fraîches après hydratation.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Composant hybride : contenu statique SSR + données live côté client\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">'use client'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { useEffect, useState } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'react'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\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\"> LiveData\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  price\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> number\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  stock\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> number\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  reviewCount\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> number\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  averageRating\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> number\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\">interface\u003C/span>\u003Cspan style=\"color:#B392F0\"> Props\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  // Ces props viennent du SSR — présentes dans le HTML initial\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  productName\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  productDescription\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  staticPrice\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> number\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; \u003C/span>\u003Cspan style=\"color:#6A737D\">// Prix au moment du build/ISR\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  slug\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\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\"> function\u003C/span>\u003Cspan style=\"color:#B392F0\"> ProductLiveData\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({ \u003C/span>\u003Cspan style=\"color:#FFAB70\">productName\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">productDescription\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">staticPrice\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">slug\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> }\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> Props\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\">liveData\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">setLiveData\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#B392F0\"> useState\u003C/span>\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#B392F0\">LiveData\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#79B8FF\"> null\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>(\u003C/span>\u003Cspan style=\"color:#79B8FF\">null\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  useEffect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(() \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Cet appel API ne s'exécute que côté client\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Googlebot verra staticPrice dans le HTML initial\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`/api/products/${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}/live`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      .\u003C/span>\u003Cspan style=\"color:#B392F0\">then\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">res\u003C/span>\u003Cspan style=\"color:#F97583\"> =>\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>\u003Cspan style=\"color:#B392F0\">then\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(setLiveData)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      .\u003C/span>\u003Cspan style=\"color:#B392F0\">catch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(() \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">        // En cas d'échec, le prix statique reste affiché\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">        // Pas de contenu manquant, jamais\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  }, [slug]);\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\"> displayPrice\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> liveData?.price \u003C/span>\u003Cspan style=\"color:#F97583\">??\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> staticPrice;\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\"> (\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#B392F0\">div\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      {\u003C/span>\u003Cspan style=\"color:#6A737D\">/* Ce contenu est dans le HTML SSR */\u003C/span>\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#B392F0\">h1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>{productName}\u003C/span>\u003Cspan style=\"color:#F97583\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">h1\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#B392F0\">p\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>{productDescription}\u003C/span>\u003Cspan style=\"color:#F97583\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">p\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      {\u003C/span>\u003Cspan style=\"color:#6A737D\">/* Le prix affiché est le prix statique (SSR) puis mis à jour (client) */\u003C/span>\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#E1E4E8\">span className\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"price\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> data\u003C/span>\u003Cspan style=\"color:#F97583\">-\u003C/span>\u003Cspan style=\"color:#E1E4E8\">live\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{!!liveData}\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        {\u003C/span>\u003Cspan style=\"color:#FFAB70\">displayPrice\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#FFAB70\">toFixed\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(2)} €\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      &#x3C;/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">span\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      {\u003C/span>\u003Cspan style=\"color:#6A737D\">/* Les avis sont chargés côté client uniquement */\u003C/span>\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      {\u003C/span>\u003Cspan style=\"color:#FFAB70\">liveData\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> &#x26;&#x26; (\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        &#x3C;\u003C/span>\u003Cspan style=\"color:#FFAB70\">div\u003C/span>\u003Cspan style=\"color:#FFAB70\"> className\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"reviews\"\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          {\u003C/span>\u003Cspan style=\"color:#FFAB70\">liveData\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#FFAB70\">averageRating\u003C/span>\u003Cspan style=\"color:#E1E4E8\">}/5 ({\u003C/span>\u003Cspan style=\"color:#FFAB70\">liveData\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#FFAB70\">reviewCount\u003C/span>\u003Cspan style=\"color:#E1E4E8\">} \u003C/span>\u003Cspan style=\"color:#FFAB70\">avis\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        &#x3C;/\u003C/span>\u003Cspan style=\"color:#FFAB70\">div\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\">    &#x3C;/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">div\u003C/span>\u003Cspan style=\"color:#F97583\">>\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 trade-off est explicite : les avis ne seront pas dans le HTML initial. Si les avis sont un vecteur SEO important (rich snippets, contenu unique), il faut les inclure dans le SSR. Si ce sont des données purement UX, le chargement côté client est acceptable.\u003C/p>\n\u003Cp>C'est un arbitrage page par page. Un article sur \u003Ca href=\"/blog/contenu-duplique-causes-techniques-et-solutions\">le contenu dupliqué\u003C/a> ne se gère pas de la même manière qu'une page de listing avec \u003Ca href=\"/blog/infinite-scroll-et-seo-le-guide-technique\">infinite scroll\u003C/a>.\u003C/p>\n\u003Ch2>Scénario concret : migration API-first d'un site média (8 500 pages)\u003C/h2>\n\u003Ch3>Le contexte\u003C/h3>\n\u003Cp>Un site média B2B avec 8 500 articles, hébergé sur un WordPress monolithique. L'équipe technique migre vers une architecture headless : WordPress reste le CMS (back-end éditorial), une API REST WordPress alimente un front Nuxt 3 déployé sur Vercel.\u003C/p>\n\u003Cp>\u003Cstrong>Avant migration\u003C/strong> : WordPress sert du HTML complet. Toutes les pages sont indexées. Le site reçoit 180 000 sessions organiques par mois.\u003C/p>\n\u003Ch3>Les erreurs commises (et détectées après coup)\u003C/h3>\n\u003Cp>\u003Cstrong>Semaine 1 post-migration.\u003C/strong> Le rapport \"Pages\" de la Search Console affiche une augmentation de 3 200 pages en statut \"Discovered — currently not indexed.\" L'équipe panique.\u003C/p>\n\u003Cp>L'investigation révèle trois problèmes simultanés :\u003C/p>\n\u003Col>\n\u003Cli>\n\u003Cp>\u003Cstrong>Sitemap non mis à jour.\u003C/strong> L'ancien sitemap WordPress XML pointait vers les URLs de l'ancien domaine. Le nouveau front Nuxt n'avait pas de sitemap généré. Résultat : Googlebot découvrait les nouvelles pages uniquement via les liens internes, mais la structure de \u003Ca href=\"/blog/mega-menus-et-seo-attention-au-crawl-budget\">mega menu\u003C/a> ne liait pas les articles de plus de 6 mois.\u003C/p>\n\u003C/li>\n\u003Cli>\n\u003Cp>\u003Cstrong>Meta tags générés côté client.\u003C/strong> Le composant \u003Ccode>useHead()\u003C/code> de Nuxt 3 était utilisé dans un composant marqué \u003Ccode>'use client'\u003C/code> (par erreur — un import d'un composant interactif contaminait l'arbre). Les balises \u003Ccode>&#x3C;title>\u003C/code> et \u003Ccode>&#x3C;meta description>\u003C/code> n'étaient pas dans le HTML initial.\u003C/p>\n\u003C/li>\n\u003Cli>\n\u003Cp>\u003Cstrong>L'API WordPress rate-limitait Googlebot.\u003C/strong> Le WordPress headless tournait sur un serveur 2 vCPU / 4 GB RAM. Quand Googlebot crawlait 15 pages par seconde (comportement normal pour un site de cette taille), chaque page déclenchait 3 appels API (article + catégorie + articles liés). Le serveur WordPress atteignait 45 requêtes API par seconde et commençait à retourner des 429.\u003C/p>\n\u003C/li>\n\u003C/ol>\n\u003Ch3>La résolution\u003C/h3>\n\u003Cp>\u003Cstrong>Pour le sitemap\u003C/strong> : génération dynamique via une route API Nuxt qui interroge l'API WordPress et génère un sitemap XML avec \u003Ccode>lastmod\u003C/code> basé sur \u003Ccode>modified_date\u003C/code>. Soumission via Search Console.\u003C/p>\n\u003Cp>\u003Cstrong>Pour les meta tags\u003C/strong> : audit systématique avec un crawl Screaming Frog en mode \"JavaScript rendering\" vs \"HTML brut\". Toute page où le \u003Ccode>&#x3C;title>\u003C/code> diffère entre les deux modes est flaggée. Correction : déplacer les \u003Ccode>useHead()\u003C/code> dans les composants serveur de Nuxt (\u003Ccode>definePageMeta\u003C/code> + \u003Ccode>useAsyncData\u003C/code> dans le \u003Ccode>setup\u003C/code> du composant page, pas dans un composant enfant client).\u003C/p>\n\u003Cp>\u003Cstrong>Pour le rate limiting\u003C/strong> : mise en place du reverse proxy cache Nginx (pattern décrit plus haut) devant l'API WordPress avec \u003Ccode>proxy_cache_valid 200 10m\u003C/code>. Ajout d'un \u003Ccode>Cache-Control: public, s-maxage=600\u003C/code> sur les réponses API WordPress. Résultat : le cache absorbe 95 % des requêtes Googlebot, le serveur WordPress ne reçoit plus que 2-3 requêtes par seconde en pic de crawl.\u003C/p>\n\u003Ch3>Les métriques de récupération\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Cstrong>Semaine 2\u003C/strong> : les pages \"Discovered — not indexed\" commencent à diminuer.\u003C/li>\n\u003Cli>\u003Cstrong>Semaine 5\u003C/strong> : retour au niveau d'indexation pré-migration (8 200 pages indexées sur 8 500).\u003C/li>\n\u003Cli>\u003Cstrong>Semaine 8\u003C/strong> : le trafic organique remonte à 165 000 sessions (vs 180 000 avant). Le delta de 15 000 sessions est attribué à la perte de PageRank temporaire liée aux redirections 301 et à la réindexation progressive.\u003C/li>\n\u003Cli>\u003Cstrong>Semaine 14\u003C/strong> : 192 000 sessions. Le gain net est attribué aux Core Web Vitals améliorés (LCP passé de 3.8s sous WordPress à 1.2s sous Nuxt/Vercel) et aux données structurées JSON-LD ajoutées systématiquement (absentes sous l'ancien WordPress).\u003C/li>\n\u003C/ul>\n\u003Cp>Ce type de régression silencieuse — meta tags disparues, pages rendues partiellement — est exactement ce qu'un monitoring continu comme Seogard détecte en temps réel, avant que la Search Console ne vous le signale 2 semaines plus tard.\u003C/p>\n\u003Ch2>Validation et monitoring : fermer la boucle\u003C/h2>\n\u003Ch3>Tester le rendering avant la mise en production\u003C/h3>\n\u003Cp>Le test unitaire du rendering SSR devrait faire partie de votre pipeline CI/CD. Voici un test minimaliste avec Playwright qui vérifie que le HTML servi contient les éléments SEO critiques :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// tests/seo-ssr.spec.ts — Playwright\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { test, expect } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '@playwright/test'\u003C/span>\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\"> CRITICAL_PAGES\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  '/products/chaussure-running-pro-x1'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  '/products/montre-connectee-sport-v3'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  '/category/equipement-running'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">];\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> path\u003C/span>\u003Cspan style=\"color:#F97583\"> of\u003C/span>\u003Cspan style=\"color:#79B8FF\"> CRITICAL_PAGES\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  test\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`SSR SEO check: ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">path\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#F97583\">async\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ({ \u003C/span>\u003Cspan style=\"color:#FFAB70\">request\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> }) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Requête HTTP brute — pas de rendering JS\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // C'est ce que Googlebot voit en première phase\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> response\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> request.\u003C/span>\u003Cspan style=\"color:#B392F0\">get\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(path);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> html\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> response.\u003C/span>\u003Cspan style=\"color:#B392F0\">text\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Le title doit être dans le HTML initial\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(html).\u003C/span>\u003Cspan style=\"color:#B392F0\">toMatch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\u003C/span>\u003Cspan style=\"color:#DBEDFF\">&#x3C;title>\u003C/span>\u003Cspan style=\"color:#79B8FF\">[\u003C/span>\u003Cspan style=\"color:#F97583\">^\u003C/span>\u003Cspan style=\"color:#79B8FF\">&#x3C;]\u003C/span>\u003Cspan style=\"color:#F97583\">{10,60}\u003C/span>\u003Cspan style=\"color:#DBEDFF\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D;font-weight:bold\">\\/\u003C/span>\u003Cspan style=\"color:#DBEDFF\">title>\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:#6A737D\">    // La meta description doit être présente\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(html).\u003C/span>\u003Cspan style=\"color:#B392F0\">toMatch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">      /\u003C/span>\u003Cspan style=\"color:#DBEDFF\">&#x3C;meta name=\"description\" content=\"\u003C/span>\u003Cspan style=\"color:#79B8FF\">[\u003C/span>\u003Cspan style=\"color:#F97583\">^\u003C/span>\u003Cspan style=\"color:#79B8FF\">\"]\u003C/span>\u003Cspan style=\"color:#F97583\">{50,160}\u003C/span>\u003Cspan style=\"color:#DBEDFF\">\">\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\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\">    // Le canonical doit pointer vers la bonne URL\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(html).\u003C/span>\u003Cspan style=\"color:#B392F0\">toContain\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">      `&#x3C;link rel=\"canonical\" href=\"https://monsite.fr${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">path\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}\"`\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\">    // Le contenu principal doit être dans le HTML (pas derrière un fetch client)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(html).\u003C/span>\u003Cspan style=\"color:#B392F0\">toMatch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\u003C/span>\u003Cspan style=\"color:#DBEDFF\">&#x3C;h1>\u003C/span>\u003Cspan style=\"color:#79B8FF\">[\u003C/span>\u003Cspan style=\"color:#F97583\">^\u003C/span>\u003Cspan style=\"color:#79B8FF\">&#x3C;]\u003C/span>\u003Cspan style=\"color:#F97583\">+\u003C/span>\u003Cspan style=\"color:#DBEDFF\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D;font-weight:bold\">\\/\u003C/span>\u003Cspan style=\"color:#DBEDFF\">h1>\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:#6A737D\">    // Le JSON-LD doit être présent\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(html).\u003C/span>\u003Cspan style=\"color:#B392F0\">toContain\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'\"@type\":\"Product\"'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Vérifier l'absence de signes de rendering client-only\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(html).not.\u003C/span>\u003Cspan style=\"color:#B392F0\">toContain\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'&#x3C;div id=\"__next\">&#x3C;/div>'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">); \u003C/span>\u003Cspan style=\"color:#6A737D\">// Coquille vide Next.js\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(html).not.\u003C/span>\u003Cspan style=\"color:#B392F0\">toContain\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'loading...'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">); \u003C/span>\u003Cspan style=\"color:#6A737D\">// Placeholder non résolu\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Ce test s'exécute sans navigateur headless — il fait une requête HTTP brute et analyse le HTML retourné. C'est exactement ce que Googlebot fait en première phase. Intégrez-le dans votre \u003Ca href=\"/blog/automatiser-les-checks-seo-dans-le-ci-cd\">pipeline CI/CD\u003C/a> : chaque déploiement qui casse le SSR est bloqué avant d'atteindre la production.\u003C/p>\n\u003Ch3>Les signaux à surveiller en production\u003C/h3>\n\u003Cp>\u003Cstrong>Search Console — rapport \"Pages\".\u003C/strong> Surveillez spécifiquement les statuts \"Crawled — currently not indexed\" et \"Discovered — currently not indexed.\" Une augmentation soudaine après un déploiement indique un problème de rendering. Les \u003Ca href=\"/blog/google-search-console-les-rapports-que-vous-ignorez\">rapports souvent ignorés\u003C/a> de la Search Console donnent des indices précieux, mais avec un délai de 2 à 5 jours.\u003C/p>\n\u003Cp>\u003Cstrong>Chrome DevTools — onglet Network.\u003C/strong> Pour diagnostiquer ce que Googlebot voit, utilisez les \u003Ca href=\"/blog/chrome-devtools-pour-le-seo-astuces-avancees\">fonctionnalités avancées de Chrome DevTools\u003C/a> : désactivez JavaScript, rechargez la page. Si le contenu principal disparaît, votre page dépend du rendering côté client. C'est le test le plus rapide et le plus fiable en phase de développement.\u003C/p>\n\u003Cp>\u003Cstrong>Screaming Frog — crawl comparatif.\u003C/strong> Configurez deux crawls du même périmètre : un en mode \"HTML brut\" et un en mode \"JavaScript rendering.\" Exportez les deux rapports, comparez les \u003Ccode>&#x3C;title>\u003C/code>, les \u003Ccode>&#x3C;meta description>\u003C/code>, et les \u003Ccode>&#x3C;h1>\u003C/code>. Toute divergence est un bug SSR.\u003C/p>\n\u003Cp>\u003Cstrong>Logs serveur.\u003C/strong> Analysez les logs d'accès de votre front-end. Filtrez sur le User-Agent Googlebot. Vérifiez les codes de réponse : des 500 ou 503 intermittents indiquent que l'API upstream défaille et que votre SSR ne gère pas correctement les erreurs (il devrait retourner un 503 avec un \u003Ccode>Retry-After\u003C/code> header, pas un 200 avec un contenu vide).\u003C/p>\n\u003Ch2>Les edge cases que personne ne mentionne\u003C/h2>\n\u003Ch3>Contenu API paginé et crawl budget\u003C/h3>\n\u003Cp>Un catalogue de 22 000 produits avec une API qui retourne 50 produits par page = 440 pages de listing. Si chaque page de listing est un appel API distinct et que vous les rendez toutes en SSR, c'est 440 pages que Googlebot doit crawler. Ajoutez les pages produits individuelles, et votre \u003Ca href=\"/blog/mega-menus-et-seo-attention-au-crawl-budget\">crawl budget\u003C/a> explose.\u003C/p>\n\u003Cp>La solution : ne rendez en SSR que les pages de listing qui ont une valeur SEO réelle (pages 1 à 3 de chaque catégorie, qui concentrent 95 % du trafic). Les pages de listing au-delà de la page 5 peuvent être des \u003Ccode>noindex, follow\u003C/code> — elles servent de tremplin de découverte pour les crawlers, pas de pages de destination.\u003C/p>\n\u003Ch3>API versionnée et URLs cassées\u003C/h3>\n\u003Cp>Les APIs headless évoluent. Un passage de \u003Ccode>/v2/products/\u003C/code> à \u003Ccode>/v3/products/\u003C/code> avec un changement de structure de réponse peut casser silencieusement votre SSR si le front-end attend un champ \u003Ccode>description\u003C/code> qui a été renommé en \u003Ccode>body\u003C/code> dans la v3. Le SSR ne plante pas — il rend une page avec une description vide. Googlebot indexe cette page comme du \u003Ca href=\"/blog/contenu-genere-automatiquement-et-seo-ce-que-google-accepte\">contenu généré automatiquement\u003C/a> de mauvaise qualité.\u003C/p>\n\u003Cp>Protégez-vous avec une validation de schéma côté serveur (Zod, Joi, ou un simple type guard TypeScript) qui lève une erreur explicite si la réponse API ne correspond pas au format attendu. Mieux vaut un 500 propre qu'un 200 avec du contenu tronqué.\u003C/p>\n\u003Ch3>Le piège des previews et des environnements de staging\u003C/h3>\n\u003Cp>Les environnements de preview (Vercel Preview Deployments, Netlify Deploy Previews) génèrent des URLs uniques par branche ou par commit. Si ces URLs ne sont pas protégées par un \u003Ccode>noindex\u003C/code> ou une authentification, elles peuvent être crawlées et indexées — créant du \u003Ca href=\"/blog/contenu-duplique-causes-techniques-et-solutions\">contenu dupliqué\u003C/a> massif. Vercel ajoute automatiquement un header \u003Ccode>X-Robots-Tag: noindex\u003C/code> sur les preview deployments, mais ce n'est pas le cas de toutes les plateformes. Vérifiez.\u003C/p>\n\u003Ch2>Le contenu API-driven face aux crawlers IA\u003C/h2>\n\u003Cp>Avec l'explosion du trafic des bots IA — \u003Ca href=\"/blog/chatgpt-now-crawls-3-6x-more-than-googlebot-what-24m-requests-reveal\">ChatGPT crawle désormais 3,6 fois plus que Googlebot\u003C/a> sur certains sites — votre architecture API-first doit aussi tenir compte de ces crawlers. GPTBot, ClaudeBot et consorts ne font pas de rendering JavaScript. Ils ne voient que le HTML brut. Si votre contenu n'est pas dans le HTML initial, il n'existe pas pour les LLMs qui alimentent les réponses conversationnelles.\u003C/p>\n\u003Cp>C'est un argument supplémentaire — et de plus en plus décisif — en faveur du SSR systématique pour tout contenu que vous souhaitez voir apparaître dans les \u003Ca href=\"/blog/how-to-design-content-that-ai-systems-prefer-and-promote\">réponses des systèmes IA\u003C/a>.\u003C/p>\n\u003Ch2>La takeaway\u003C/h2>\n\u003Cp>L'architecture API-first n'est pas incompatible avec le SEO — elle exige simplement que le rendering côté serveur soit traité comme une infrastructure critique, au même titre que la base de données ou le CDN. Le pattern gagnant : SSR pour le contenu indexable, cache HTTP résilient devant l'API, tests automatisés du HTML brut dans le CI/CD, et monitoring continu des régressions. Un outil comme Seogard permet de détecter en temps réel les pages dont le contenu SSR a disparu — avant que Google ne les désindexe silencieusement.\u003C/p>",null,12,[18,19,20,21,22],"api","headless","seo","rendering","ssr","API-first et SEO : rendre le contenu crawlable","Thu Apr 09 2026 16:02:58 GMT+0000 (Coordinated Universal Time)",[26,36,50],{"_id":27,"slug":28,"__v":6,"author":7,"canonical":29,"category":10,"createdAt":30,"date":12,"description":31,"image":15,"imageAlt":15,"readingTime":16,"tags":32,"title":34,"updatedAt":35},"69d7b16aaa6b273b0c68cec1","headless-cms-et-seo-avantages-et-risques-techniques","https://seogard.io/blog/headless-cms-et-seo-avantages-et-risques-techniques","2026-04-09T14:02:18.955Z","Architectures headless CMS décryptées côté SEO : SSR, ISR, gestion des meta, crawl budget. Guide technique avec exemples de code et scénarios réels.",[19,33,18,20,22],"cms","Headless CMS et SEO : risques techniques et architectures viables","Thu Apr 09 2026 14:02:18 GMT+0000 (Coordinated Universal Time)",{"_id":37,"slug":38,"__v":6,"author":7,"canonical":39,"category":10,"createdAt":40,"date":41,"description":42,"image":15,"imageAlt":15,"readingTime":43,"tags":44,"title":48,"updatedAt":49},"69d465a4f4fa19862857f3ff","infinite-scroll-et-seo-le-guide-technique","https://seogard.io/blog/infinite-scroll-et-seo-le-guide-technique","2026-04-07T02:02:12.986Z","2026-04-07","Implémenter le défilement infini sans sacrifier l'indexation. Patterns techniques, code et pièges à éviter pour les sites à forte pagination.",14,[45,46,20,47],"infinite-scroll","pagination","javascript","Infinite scroll et SEO : le guide technique complet","Tue Apr 07 2026 02:02:12 GMT+0000 (Coordinated Universal Time)",{"_id":51,"slug":52,"__v":6,"author":7,"canonical":53,"category":10,"createdAt":54,"date":55,"description":56,"image":15,"imageAlt":15,"readingTime":43,"tags":57,"title":62,"updatedAt":63},"69d31e84f4fa19862894f593","architecture-de-site-seo-flat-vs-deep-structure","https://seogard.io/blog/architecture-de-site-seo-flat-vs-deep-structure","2026-04-06T02:46:28.396Z","2026-04-06","Flat ou deep structure ? Analyse technique de l'impact de la profondeur de clic sur le crawl budget, l'indexation et le maillage interne.",[58,59,60,61],"architecture","profondeur","crawl","internal-linking","Architecture flat vs deep : profondeur de clic et crawl SEO","Mon Apr 06 2026 12:02:16 GMT+0000 (Coordinated Universal Time)"]