[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fXL6ofNzcOHMcO0u0SLdnHLs4X_VuKva0d7NPacPzrcU":3,"$f7AO-B6_-GZrfaCjbrqo-JwytxqiR8oEAIBdRCJmyeeg":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},"69d7b16aaa6b273b0c68cec1","headless-cms-et-seo-avantages-et-risques-techniques",0,"Equipe Seogard","Un site e-commerce de 22 000 fiches produits migre de Magento monolithique vers un front Next.js branché sur Strapi. Trois mois après la mise en production, 40 % des pages produits ont disparu de l'index Google. La cause : un rendu client-side pur que Googlebot n'a jamais réussi à parser correctement, combiné à des balises canonical générées dynamiquement qui pointaient toutes vers la homepage. Le headless CMS n'était pas le problème — c'est l'absence de réflexion SEO dans l'architecture frontend qui a tout cassé.\n\n## Ce que \"headless\" change réellement pour le crawl et l'indexation\n\nUn CMS traditionnel (WordPress, Drupal, Magento natif) génère le HTML côté serveur. Le crawler reçoit une page complète. Avec un headless CMS, le contenu est servi via API (REST ou GraphQL), et c'est le frontend — souvent un framework JavaScript — qui assemble la page.\n\nLe problème fondamental : Googlebot exécute le JavaScript, mais pas de la même manière qu'un navigateur utilisateur. Le [processus de rendering de Google](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics) est en deux phases. D'abord, le crawl récupère le HTML initial. Ensuite, le rendering exécute le JS dans une file d'attente séparée, avec un délai qui peut aller de quelques secondes à plusieurs jours selon la charge de la Web Rendering Service (WRS).\n\n### Le piège du Client-Side Rendering (CSR) pur\n\nSi votre front est une Single Page Application React/Vue/Angular qui fait tous ses appels API dans le navigateur, voici ce que Googlebot voit en phase 1 :\n\n```html\n\u003C!DOCTYPE html>\n\u003Chtml>\n\u003Chead>\n  \u003Ctitle>Mon site\u003C/title>\n  \u003C!-- Aucune meta description, aucun canonical, aucune balise OG -->\n\u003C/head>\n\u003Cbody>\n  \u003Cdiv id=\"root\">\u003C/div>\n  \u003Cscript src=\"/static/js/bundle.a3f8e2.js\">\u003C/script>\n\u003C/body>\n\u003C/html>\n```\n\nCe shell vide ne contient aucune information exploitable. Google doit attendre la phase de rendering pour découvrir le contenu. Et si l'appel API vers votre headless CMS échoue pendant ce rendering (timeout réseau, rate limiting, erreur 5xx), la page reste vide. Google la considère comme du [thin content](/blog/thin-content-quand-vos-pages-nuisent-au-seo-global) et finit par la désindexer.\n\n### Les trois modes de rendu viables pour le SEO\n\n**SSR (Server-Side Rendering)** : le serveur Node.js exécute le framework, appelle l'API du CMS, et renvoie du HTML complet. C'est le mode le plus sûr pour le SEO, mais il impose une charge serveur proportionnelle au trafic.\n\n**SSG (Static Site Generation)** : les pages sont pré-générées au build time. Idéal pour un blog de 500 articles, ingérable pour un catalogue produit de 20 000 SKUs qui change quotidiennement.\n\n**ISR (Incremental Static Regeneration)** : compromis entre SSR et SSG. Les pages sont générées statiquement puis revalidées en arrière-plan après un délai configurable. C'est souvent le sweet spot pour les sites e-commerce headless.\n\nLe choix dépend de trois facteurs : le volume de pages, la fréquence de mise à jour du contenu, et votre budget infrastructure. Un média qui publie 50 articles/jour a des contraintes différentes d'un SaaS avec 30 pages marketing.\n\n## Gestion des meta tags dans une architecture découplée\n\nC'est là que la majorité des implémentations headless échouent silencieusement. Dans un WordPress classique, Yoast ou Rank Math gèrent les meta tags directement dans le template PHP. En headless, les meta tags doivent transiter par l'API, être récupérées par le frontend, et injectées dans le `\u003Chead>` — le tout avant que le HTML ne soit envoyé au crawler.\n\n### Structurer les meta dans l'API du CMS\n\nDans Strapi, Contentful ou Sanity, vous devez modéliser les champs SEO explicitement. Voici un exemple de type de contenu dans Strapi v5 :\n\n```typescript\n// src/api/product/content-types/product/schema.json\n{\n  \"kind\": \"collectionType\",\n  \"collectionName\": \"products\",\n  \"attributes\": {\n    \"name\": { \"type\": \"string\", \"required\": true },\n    \"slug\": { \"type\": \"uid\", \"targetField\": \"name\" },\n    \"description\": { \"type\": \"richtext\" },\n    \"seo\": {\n      \"type\": \"component\",\n      \"component\": \"shared.seo\",\n      \"required\": true\n    }\n  }\n}\n\n// src/components/shared/seo.json\n{\n  \"collectionName\": \"components_shared_seo\",\n  \"attributes\": {\n    \"metaTitle\": {\n      \"type\": \"string\",\n      \"required\": true,\n      \"maxLength\": 60\n    },\n    \"metaDescription\": {\n      \"type\": \"string\",\n      \"required\": true,\n      \"maxLength\": 160\n    },\n    \"canonicalUrl\": { \"type\": \"string\" },\n    \"noIndex\": { \"type\": \"boolean\", \"default\": false },\n    \"structuredData\": { \"type\": \"json\" }\n  }\n}\n```\n\nLe piège classique : rendre le composant SEO optionnel. Si un rédacteur oublie de remplir la meta description, la page part en production sans. Dans un monolithe, Yoast affiche un warning impossible à rater. En headless, personne ne le voit sauf si vous avez mis en place une validation stricte côté CMS **et** un monitoring côté production.\n\n### Injection côté Next.js avec `generateMetadata`\n\nDepuis Next.js 13+ (App Router), la gestion des meta passe par la fonction `generateMetadata` :\n\n```typescript\n// app/produits/[slug]/page.tsx\nimport { Metadata } from 'next';\n\ntype Props = { params: { slug: string } };\n\nexport async function generateMetadata({ params }: Props): Promise\u003CMetadata> {\n  const product = await fetch(\n    `${process.env.STRAPI_URL}/api/products?filters[slug][$eq]=${params.slug}&populate=seo`,\n    { next: { revalidate: 3600 } }\n  ).then(res => res.json());\n\n  const seo = product.data?.[0]?.attributes?.seo;\n\n  if (!seo) {\n    // Fallback critique — ne jamais renvoyer un head vide\n    return {\n      title: product.data?.[0]?.attributes?.name || 'Produit',\n      robots: { index: false }\n    };\n  }\n\n  return {\n    title: seo.metaTitle,\n    description: seo.metaDescription,\n    alternates: {\n      canonical: seo.canonicalUrl || `https://monsite.fr/produits/${params.slug}`\n    },\n    robots: {\n      index: !seo.noIndex,\n      follow: true\n    }\n  };\n}\n\nexport default async function ProductPage({ params }: Props) {\n  // ... rendu de la page\n}\n```\n\nTrois points critiques dans ce code :\n\n1. **Le fallback quand `seo` est null** : plutôt que de renvoyer un `\u003Chead>` vide, on injecte un `noindex`. C'est une approche défensive — mieux vaut désindexer temporairement une page que de laisser Google indexer un title générique identique sur 5 000 produits, ce qui crée du [contenu dupliqué](/blog/contenu-duplique-causes-techniques-et-solutions) massif.\n\n2. **Le `revalidate: 3600`** : les meta sont mises en cache 1h côté serveur via ISR. Si un rédacteur corrige une meta description dans Strapi, le changement ne sera visible par Googlebot qu'après expiration du cache. Pour les correctifs urgents, il faut un mécanisme de purge on-demand (webhook Strapi → API route Next.js qui appelle `revalidatePath`).\n\n3. **Le canonical** : s'il n'est pas défini dans le CMS, on le génère automatiquement à partir du slug. C'est un filet de sécurité essentiel. Un canonical manquant est moins grave qu'un canonical qui pointe au mauvais endroit, mais sur un catalogue de milliers de pages, l'absence systématique de canonical peut mener à des problèmes d'indexation de variantes d'URL. Vérifiez que vos [structures d'URL](/blog/url-structure-bonnes-pratiques-seo-pour-les-urls) sont cohérentes entre le CMS et le frontend.\n\n## Sitemap, routing et crawl budget en architecture headless\n\n### Génération dynamique du sitemap\n\nDans un monolithe, le sitemap est souvent généré par un plugin qui requête directement la base de données. En headless, le sitemap doit interroger l'API du CMS, ce qui peut poser des problèmes de performance à grande échelle.\n\nPour un catalogue de 22 000 produits, un seul appel API ne suffit pas (pagination obligatoire). Voici une implémentation Next.js qui gère la pagination et le sitemap index :\n\n```typescript\n// app/sitemap.ts\nimport { MetadataRoute } from 'next';\n\nconst STRAPI_URL = process.env.STRAPI_URL;\nconst SITE_URL = 'https://monsite.fr';\nconst PAGE_SIZE = 1000;\n\nasync function getProductCount(): Promise\u003Cnumber> {\n  const res = await fetch(\n    `${STRAPI_URL}/api/products?pagination[pageSize]=1&fields[0]=id`\n  );\n  const data = await res.json();\n  return data.meta.pagination.total;\n}\n\nasync function getProducts(page: number) {\n  const res = await fetch(\n    `${STRAPI_URL}/api/products?` +\n    `pagination[page]=${page}&pagination[pageSize]=${PAGE_SIZE}` +\n    `&fields[0]=slug&fields[1]=updatedAt` +\n    `&filters[seo][noIndex][$ne]=true`\n  );\n  return res.json();\n}\n\nexport default async function sitemap(): Promise\u003CMetadataRoute.Sitemap> {\n  const total = await getProductCount();\n  const pages = Math.ceil(total / PAGE_SIZE);\n\n  const allProducts = await Promise.all(\n    Array.from({ length: pages }, (_, i) => getProducts(i + 1))\n  );\n\n  const productUrls = allProducts.flatMap(response =>\n    response.data.map((product: any) => ({\n      url: `${SITE_URL}/produits/${product.attributes.slug}`,\n      lastModified: new Date(product.attributes.updatedAt),\n      changeFrequency: 'weekly' as const,\n      priority: 0.8\n    }))\n  );\n\n  return [\n    { url: SITE_URL, lastModified: new Date(), priority: 1.0 },\n    ...productUrls\n  ];\n}\n```\n\nLe filtre `filters[seo][noIndex][$ne]=true` est crucial : il exclut du sitemap les pages marquées noindex dans le CMS. Soumettre à Google un sitemap contenant des URLs noindex est un signal contradictoire qui gaspille du [crawl budget](/blog/mega-menus-et-seo-attention-au-crawl-budget).\n\nPour les catalogues au-delà de 50 000 URLs, vous devrez implémenter un sitemap index avec des sitemaps enfants paginés. Next.js supporte nativement `generateSitemaps()` pour ce cas.\n\n### Le problème des routes orphelines\n\nEn headless, le routing est géré côté frontend. Si un produit est supprimé dans Strapi mais que la route existe encore côté Next.js (via un cache ISR périmé), vous servez une page avec un appel API qui retourne 404 — mais le serveur HTTP renvoie un 200. C'est un soft 404, l'un des problèmes les plus courants et les plus insidieux en architecture headless.\n\nLa solution : dans votre page dynamique, vérifiez explicitement le retour API et appelez `notFound()` de Next.js :\n\n```typescript\nimport { notFound } from 'next/navigation';\n\nexport default async function ProductPage({ params }: Props) {\n  const res = await fetch(\n    `${process.env.STRAPI_URL}/api/products?filters[slug][$eq]=${params.slug}&populate=*`\n  );\n  const data = await res.json();\n\n  if (!data.data || data.data.length === 0) {\n    notFound(); // Renvoie un vrai HTTP 404\n  }\n\n  // ... rendu normal\n}\n```\n\nSurveillez le rapport \"Pages non indexées\" dans [Google Search Console](/blog/google-search-console-les-rapports-que-vous-ignorez) pour détecter les soft 404. Un outil de monitoring comme Seogard peut automatiser cette détection en crawlant vos pages régulièrement et en comparant le status code HTTP avec le contenu effectif de la page.\n\n## Scénario réel : migration headless d'un média à 18 000 pages\n\nUn site média publiant 30 articles/jour migre de WordPress (thème custom) vers un front Nuxt 3 + Directus comme headless CMS. Le site totalise 18 000 URLs indexées, avec un trafic organique de 450 000 sessions/mois.\n\n### Phase 1 : audit pré-migration\n\nAvant de toucher au code, l'équipe crawle le site existant avec Screaming Frog pour établir une baseline :\n\n- 18 247 URLs indexées (rapport Coverage de Search Console)\n- Temps de crawl moyen par page : 320 ms (log serveur)\n- 97 % des pages ont un title unique et une meta description\n- 612 redirections 301 existantes à préserver\n- Maillage interne dense : moyenne de 42 liens internes par page (menu, sidebar, articles liés)\n\n### Phase 2 : les erreurs commises\n\nLe front Nuxt 3 est déployé avec SSR activé (`ssr: true` dans `nuxt.config.ts`). Jusque-là, correct. Mais trois problèmes apparaissent dans les semaines suivantes :\n\n**Problème 1 : latence API.** Chaque page SSR fait 3 appels API à Directus (contenu article, articles liés, menu de navigation). En charge, le TTFB monte à 2,8 secondes. Googlebot, qui a un crawl budget limité, réduit la fréquence de crawl. Le nombre de pages crawlées par jour chute de 1 200 à 340 (visible dans le rapport \"Statistiques d'exploration\" de Search Console).\n\n**Problème 2 : liens internes cassés.** L'ancien WordPress générait des URLs en `/2024/03/titre-article.html`. Le nouveau front utilise `/articles/titre-article`. Les redirections 301 sont en place, mais les liens internes dans le contenu des articles (stockés dans Directus en rich text) pointent encore vers les anciennes URLs. Résultat : des milliers de liens internes passent par une 301 au lieu de pointer directement vers l'URL finale. Google suit les redirections, mais ça ralentit le crawl et dilue le budget.\n\n**Problème 3 : les pages de pagination.** L'ancien WordPress avait `/page/2/`, `/page/3/` etc. avec des balises `rel=\"prev\"` et `rel=\"next\"`. Le nouveau front implémente un [infinite scroll](/blog/infinite-scroll-et-seo-le-guide-technique) côté client sans aucune URL de pagination accessible au crawler. Les articles au-delà de la page 1 perdent leur point d'entrée pour le crawl.\n\n### Phase 3 : correction et récupération\n\nL'équipe met en place un cache Redis entre le frontend Nuxt et l'API Directus. Le TTFB redescend à 380 ms. Un script de migration parcourt tous les contenus dans Directus pour réécrire les liens internes vers les nouvelles URLs. Les pages de pagination sont rétablies sous forme de pages statiques `/articles/page/2` accessibles au crawler, avec l'infinite scroll maintenu comme enhancement progressif côté client.\n\nAprès 6 semaines de corrections, le crawl rate remonte à 980 pages/jour. Après 3 mois, 94 % des 18 247 URLs sont réindexées sous leurs nouvelles URLs. Le trafic organique, qui avait chuté de 35 % post-migration, revient à 92 % du niveau initial. Les 8 % restants sont attribués à la perte de link equity sur les backlinks externes qui pointent encore vers les anciennes URLs via 301.\n\nLa leçon : la migration technique était propre côté rendu. C'est la couche \"contenu\" (liens internes dans les articles, pagination, performance API) qui a causé les dégâts.\n\n## Performance API et impact sur le crawl\n\nLa dépendance à une API externe est le risque structurel de toute architecture headless. Si l'API du CMS est lente, indisponible, ou rate-limitée, le SSR échoue et le crawler reçoit soit une erreur 5xx, soit une page incomplète.\n\n### Stratégies de résilience\n\n**Cache multi-niveaux.** Ne comptez pas uniquement sur le cache de Next.js/Nuxt. Ajoutez un cache applicatif (Redis, Memcached) entre votre frontend et l'API CMS, et un cache CDN (Cloudflare, Fastly) en amont du frontend. Pour les pages dont le contenu change rarement, un CDN avec des headers `s-maxage` agressifs réduit drastiquement la charge sur votre stack :\n\n```nginx\n# Configuration Nginx en reverse proxy devant le frontend Node.js\nlocation /produits/ {\n    proxy_pass http://nodejs_upstream;\n    proxy_cache_valid 200 1h;\n    proxy_cache_valid 404 5m;\n\n    # Headers pour le CDN en amont\n    add_header Cache-Control \"public, s-maxage=3600, stale-while-revalidate=86400\";\n\n    # Si le backend Node.js est down, servir le cache périmé\n    proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;\n}\n```\n\nLa directive `stale-while-revalidate` est particulièrement utile : elle permet de servir une version en cache périmé pendant que le serveur régénère la page en arrière-plan. Googlebot reçoit toujours du contenu, même si l'API CMS est temporairement indisponible.\n\n**Circuit breaker côté frontend.** Si l'API Directus/Strapi/Contentful ne répond pas en moins de 2 secondes, le frontend doit avoir un fallback — même si c'est un contenu partiel. Renvoyer un 503 avec un header `Retry-After` est préférable à un timeout de 30 secondes qui gaspille le crawl budget de Googlebot.\n\n**Monitoring des temps de réponse API.** Instrumentez les appels API du frontend avec des métriques (latence p50, p95, p99, taux d'erreur). Un pic de latence sur l'API CMS se traduit directement en baisse de crawl rate dans les jours suivants. Croisez les données de [Search Console](/blog/search-console-api-automatiser-le-reporting-seo) avec vos métriques APM pour corréler les deux.\n\n## Structured Data et données enrichies depuis un headless CMS\n\nLes données structurées (JSON-LD) sont souvent le parent pauvre des implémentations headless. Dans WordPress, un plugin comme Yoast génère automatiquement le schema `Article`, `Product`, `BreadcrumbList`. En headless, c'est à vous de le construire.\n\n### Approche recommandée : générer le JSON-LD côté serveur\n\nNe générez jamais les données structurées en JavaScript côté client. Google les prend en compte, mais les risques d'échec de rendering s'ajoutent. Construisez le JSON-LD dans votre couche SSR à partir des données de l'API :\n\n```typescript\n// lib/structured-data.ts\ninterface ProductData {\n  name: string;\n  description: string;\n  slug: string;\n  price: number;\n  currency: string;\n  sku: string;\n  image: string;\n  brand: string;\n  availability: 'InStock' | 'OutOfStock';\n  reviewCount: number;\n  ratingValue: number;\n}\n\nexport function generateProductJsonLd(product: ProductData): string {\n  const schema = {\n    '@context': 'https://schema.org',\n    '@type': 'Product',\n    name: product.name,\n    description: product.description,\n    image: product.image,\n    sku: product.sku,\n    brand: {\n      '@type': 'Brand',\n      name: product.brand\n    },\n    offers: {\n      '@type': 'Offer',\n      price: product.price,\n      priceCurrency: product.currency,\n      availability: `https://schema.org/${product.availability}`,\n      url: `https://monsite.fr/produits/${product.slug}`\n    },\n    ...(product.reviewCount > 0 && {\n      aggregateRating: {\n        '@type': 'AggregateRating',\n        ratingValue: product.ratingValue,\n        reviewCount: product.reviewCount\n      }\n    })\n  };\n\n  return JSON.stringify(schema);\n}\n```\n\nTestez systématiquement avec le [Rich Results Test](https://search.google.com/test/rich-results) de Google et validez dans [Chrome DevTools](/blog/chrome-devtools-pour-le-seo-astuces-avancees) que le JSON-LD est présent dans le HTML initial (pas uniquement après exécution du JS).\n\nUn point souvent négligé : la cohérence entre les données structurées et le contenu visible. Si votre schema `Product` indique un prix de 49,99 € mais que l'API renvoie 54,99 € sur le rendu visible (à cause d'un cache désynchronisé entre deux appels), Google peut considérer les données structurées comme trompeuses et retirer les rich snippets.\n\n## Automatiser les checks SEO sur un frontend headless\n\nL'architecture découplée multiplie les points de défaillance. Un changement dans le schéma API de Strapi peut casser silencieusement les meta tags sur 100 % des pages. Un déploiement frontend peut introduire une régression sur les canonicals. Il faut [automatiser les vérifications](/blog/automatiser-les-checks-seo-dans-le-ci-cd) dans la pipeline CI/CD.\n\n### Tests essentiels à intégrer\n\n- **Vérification du HTML rendu** : pour chaque type de page (produit, catégorie, article), faire un `curl` sur l'URL SSR et vérifier la présence du title, de la meta description, du canonical, et du JSON-LD dans le HTML brut (sans exécution JS).\n- **Vérification des status codes** : s'assurer qu'une URL de produit supprimé renvoie bien un 404, pas un 200 avec un contenu vide.\n- **Vérification de la cohérence API/rendu** : comparer le title dans la réponse API du CMS avec le title dans le HTML rendu. Toute divergence signale un bug de mapping.\n- **Smoke test du sitemap** : vérifier que le sitemap est accessible, parsable, et que le nombre d'URLs est dans une fourchette attendue (± 5 % du dernier déploiement).\n\nUn outil de monitoring continu comme Seogard complète ces checks CI/CD en surveillant la production 24/7. Un test CI attrape les régressions avant le déploiement ; un monitoring attrape celles qui passent entre les mailles — API CMS qui change de comportement, cache CDN qui sert une version corrompue, rate limiting qui se déclenche en pic de crawl.\n\n## Quand le headless n'est pas le bon choix\n\nLe headless CMS est devenu un choix par défaut dans beaucoup d'équipes tech, souvent pour des raisons de stack préférentielle plutôt que de besoin réel. Quelques cas où il ajoute de la complexité sans bénéfice SEO :\n\n**Sites de moins de 200 pages avec peu de logique frontend.** Un WordPress bien optimisé avec un bon hébergement surpassera en performance SEO un setup Contentful + Next.js déployé sur Vercel, pour une fraction du coût et de la complexité. Le rendu SSR natif de PHP est plus simple à debugger qu'une chaîne API → Node.js → CDN.\n\n**Équipes sans compétence frontend senior.** L'architecture headless transfère la responsabilité SEO du CMS vers le frontend. Si votre équipe ne maîtrise pas le SSR, la gestion du cache, et les edge cases du rendering JavaScript par les crawlers, vous allez accumuler de la dette technique SEO invisible.\n\n**Sites où le [contenu est principalement éditorial](/blog/contenu-genere-automatiquement-et-seo-ce-que-google-accepte) et linéaire.** Un blog d'entreprise, une base de connaissances — le headless n'apporte pas de valeur ajoutée si le frontend n'a pas besoin d'interactivité riche.\n\nEn revanche, le headless prend tout son sens quand le même contenu doit alimenter un site web, une app mobile et des flux tiers (marketplaces, agrégateurs), ou quand le frontend nécessite des interactions complexes (configurateurs produit, dashboards, expériences immersives) que les templates d'un CMS monolithique ne peuvent pas supporter.\n\n---\n\nL'architecture headless ne dégrade pas le SEO par nature. Ce sont les implémentations bâclées — rendu client-side, meta tags oubliées, sitemap désynchronisé, API lente sans cache — qui causent les dégâts. La clé : traiter le SEO comme une contrainte d'architecture dès le jour 0, pas comme un patch post-lancement. Et mettre en place un monitoring automatisé qui détecte les régressions avant que Google ne les découvre.\n```","https://seogard.io/blog/headless-cms-et-seo-avantages-et-risques-techniques","Architecture","2026-04-09T14:02:18.955Z","2026-04-09","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.","\u003Cp>Un site e-commerce de 22 000 fiches produits migre de Magento monolithique vers un front Next.js branché sur Strapi. Trois mois après la mise en production, 40 % des pages produits ont disparu de l'index Google. La cause : un rendu client-side pur que Googlebot n'a jamais réussi à parser correctement, combiné à des balises canonical générées dynamiquement qui pointaient toutes vers la homepage. Le headless CMS n'était pas le problème — c'est l'absence de réflexion SEO dans l'architecture frontend qui a tout cassé.\u003C/p>\n\u003Ch2>Ce que \"headless\" change réellement pour le crawl et l'indexation\u003C/h2>\n\u003Cp>Un CMS traditionnel (WordPress, Drupal, Magento natif) génère le HTML côté serveur. Le crawler reçoit une page complète. Avec un headless CMS, le contenu est servi via API (REST ou GraphQL), et c'est le frontend — souvent un framework JavaScript — qui assemble la page.\u003C/p>\n\u003Cp>Le problème fondamental : Googlebot exécute le JavaScript, mais pas de la même manière qu'un navigateur utilisateur. Le \u003Ca href=\"https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics\">processus de rendering de Google\u003C/a> est en deux phases. D'abord, le crawl récupère le HTML initial. Ensuite, le rendering exécute le JS dans une file d'attente séparée, avec un délai qui peut aller de quelques secondes à plusieurs jours selon la charge de la Web Rendering Service (WRS).\u003C/p>\n\u003Ch3>Le piège du Client-Side Rendering (CSR) pur\u003C/h3>\n\u003Cp>Si votre front est une Single Page Application React/Vue/Angular qui fait tous ses appels API dans le navigateur, voici ce que Googlebot voit en phase 1 :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;!\u003C/span>\u003Cspan style=\"color:#85E89D\">DOCTYPE\u003C/span>\u003Cspan style=\"color:#B392F0\"> html\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">html\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">head\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">title\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Mon site&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">title\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  &#x3C;!-- Aucune meta description, aucun canonical, aucune balise OG -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">head\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">body\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">div\u003C/span>\u003Cspan style=\"color:#B392F0\"> id\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"root\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">div\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">script\u003C/span>\u003Cspan style=\"color:#B392F0\"> src\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"/static/js/bundle.a3f8e2.js\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">script\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">body\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">html\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Ce shell vide ne contient aucune information exploitable. Google doit attendre la phase de rendering pour découvrir le contenu. Et si l'appel API vers votre headless CMS échoue pendant ce rendering (timeout réseau, rate limiting, erreur 5xx), la page reste vide. Google la considère comme du \u003Ca href=\"/blog/thin-content-quand-vos-pages-nuisent-au-seo-global\">thin content\u003C/a> et finit par la désindexer.\u003C/p>\n\u003Ch3>Les trois modes de rendu viables pour le SEO\u003C/h3>\n\u003Cp>\u003Cstrong>SSR (Server-Side Rendering)\u003C/strong> : le serveur Node.js exécute le framework, appelle l'API du CMS, et renvoie du HTML complet. C'est le mode le plus sûr pour le SEO, mais il impose une charge serveur proportionnelle au trafic.\u003C/p>\n\u003Cp>\u003Cstrong>SSG (Static Site Generation)\u003C/strong> : les pages sont pré-générées au build time. Idéal pour un blog de 500 articles, ingérable pour un catalogue produit de 20 000 SKUs qui change quotidiennement.\u003C/p>\n\u003Cp>\u003Cstrong>ISR (Incremental Static Regeneration)\u003C/strong> : compromis entre SSR et SSG. Les pages sont générées statiquement puis revalidées en arrière-plan après un délai configurable. C'est souvent le sweet spot pour les sites e-commerce headless.\u003C/p>\n\u003Cp>Le choix dépend de trois facteurs : le volume de pages, la fréquence de mise à jour du contenu, et votre budget infrastructure. Un média qui publie 50 articles/jour a des contraintes différentes d'un SaaS avec 30 pages marketing.\u003C/p>\n\u003Ch2>Gestion des meta tags dans une architecture découplée\u003C/h2>\n\u003Cp>C'est là que la majorité des implémentations headless échouent silencieusement. Dans un WordPress classique, Yoast ou Rank Math gèrent les meta tags directement dans le template PHP. En headless, les meta tags doivent transiter par l'API, être récupérées par le frontend, et injectées dans le \u003Ccode>&#x3C;head>\u003C/code> — le tout avant que le HTML ne soit envoyé au crawler.\u003C/p>\n\u003Ch3>Structurer les meta dans l'API du CMS\u003C/h3>\n\u003Cp>Dans Strapi, Contentful ou Sanity, vous devez modéliser les champs SEO explicitement. Voici un exemple de type de contenu dans Strapi v5 :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// src/api/product/content-types/product/schema.json\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">{\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  \"kind\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"collectionType\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  \"collectionName\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"products\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  \"attributes\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"name\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: { \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"type\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"string\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"required\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"slug\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: { \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"type\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"uid\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"targetField\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"name\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"description\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: { \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"type\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"richtext\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"seo\"\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\">\"component\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">      \"component\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"shared.seo\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">      \"required\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    }\u003C/span>\u003C/span>\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:#6A737D\">// src/components/shared/seo.json\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">{\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  \"collectionName\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"components_shared_seo\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  \"attributes\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"metaTitle\"\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\">\"string\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">      \"required\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">      \"maxLength\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">60\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"metaDescription\"\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\">\"string\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">      \"required\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">      \"maxLength\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">160\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"canonicalUrl\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: { \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"type\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"string\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"noIndex\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: { \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"type\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"boolean\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"default\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">false\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"structuredData\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: { \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"type\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"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:#E1E4E8\">}\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le piège classique : rendre le composant SEO optionnel. Si un rédacteur oublie de remplir la meta description, la page part en production sans. Dans un monolithe, Yoast affiche un warning impossible à rater. En headless, personne ne le voit sauf si vous avez mis en place une validation stricte côté CMS \u003Cstrong>et\u003C/strong> un monitoring côté production.\u003C/p>\n\u003Ch3>Injection côté Next.js avec \u003Ccode>generateMetadata\u003C/code>\u003C/h3>\n\u003Cp>Depuis Next.js 13+ (App Router), la gestion des meta passe par la fonction \u003Ccode>generateMetadata\u003C/code> :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// app/produits/[slug]/page.tsx\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\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">type\u003C/span>\u003Cspan style=\"color:#B392F0\"> Props\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\">\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>\u003Cspan style=\"color:#FFAB70\">params\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>\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\"> fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    `${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">process\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">env\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#79B8FF\">STRAPI_URL\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}/api/products?filters[slug][$eq]=${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">params\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}&#x26;populate=seo`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    { next: { revalidate: \u003C/span>\u003Cspan style=\"color:#79B8FF\">3600\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\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> seo\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> product.data?.[\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]?.attributes?.seo;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">seo) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Fallback critique — ne jamais renvoyer un head vide\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      title: product.data?.[\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]?.attributes?.name \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'Produit'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      robots: { index: \u003C/span>\u003Cspan style=\"color:#79B8FF\">false\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:#E1E4E8\">    title: seo.metaTitle,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    description: seo.metaDescription,\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: seo.canonicalUrl \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> `https://monsite.fr/produits/${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">params\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    robots: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      index: \u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">seo.noIndex,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      follow: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  };\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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>\u003Cspan style=\"color:#FFAB70\">params\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:#6A737D\">  // ... rendu de la page\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>\n\u003Cp>\u003Cstrong>Le fallback quand \u003Ccode>seo\u003C/code> est null\u003C/strong> : plutôt que de renvoyer un \u003Ccode>&#x3C;head>\u003C/code> vide, on injecte un \u003Ccode>noindex\u003C/code>. C'est une approche défensive — mieux vaut désindexer temporairement une page que de laisser Google indexer un title générique identique sur 5 000 produits, ce qui crée du \u003Ca href=\"/blog/contenu-duplique-causes-techniques-et-solutions\">contenu dupliqué\u003C/a> massif.\u003C/p>\n\u003C/li>\n\u003Cli>\n\u003Cp>\u003Cstrong>Le \u003Ccode>revalidate: 3600\u003C/code>\u003C/strong> : les meta sont mises en cache 1h côté serveur via ISR. Si un rédacteur corrige une meta description dans Strapi, le changement ne sera visible par Googlebot qu'après expiration du cache. Pour les correctifs urgents, il faut un mécanisme de purge on-demand (webhook Strapi → API route Next.js qui appelle \u003Ccode>revalidatePath\u003C/code>).\u003C/p>\n\u003C/li>\n\u003Cli>\n\u003Cp>\u003Cstrong>Le canonical\u003C/strong> : s'il n'est pas défini dans le CMS, on le génère automatiquement à partir du slug. C'est un filet de sécurité essentiel. Un canonical manquant est moins grave qu'un canonical qui pointe au mauvais endroit, mais sur un catalogue de milliers de pages, l'absence systématique de canonical peut mener à des problèmes d'indexation de variantes d'URL. Vérifiez que vos \u003Ca href=\"/blog/url-structure-bonnes-pratiques-seo-pour-les-urls\">structures d'URL\u003C/a> sont cohérentes entre le CMS et le frontend.\u003C/p>\n\u003C/li>\n\u003C/ol>\n\u003Ch2>Sitemap, routing et crawl budget en architecture headless\u003C/h2>\n\u003Ch3>Génération dynamique du sitemap\u003C/h3>\n\u003Cp>Dans un monolithe, le sitemap est souvent généré par un plugin qui requête directement la base de données. En headless, le sitemap doit interroger l'API du CMS, ce qui peut poser des problèmes de performance à grande échelle.\u003C/p>\n\u003Cp>Pour un catalogue de 22 000 produits, un seul appel API ne suffit pas (pagination obligatoire). Voici une implémentation Next.js qui gère la pagination et le sitemap index :\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/sitemap.ts\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { MetadataRoute } \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\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> STRAPI_URL\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> process.env.\u003C/span>\u003Cspan style=\"color:#79B8FF\">STRAPI_URL\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\"> SITE_URL\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'https://monsite.fr'\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\"> PAGE_SIZE\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 1000\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">async\u003C/span>\u003Cspan style=\"color:#F97583\"> function\u003C/span>\u003Cspan style=\"color:#B392F0\"> getProductCount\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:#79B8FF\">number\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:#79B8FF\">STRAPI_URL\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}/api/products?pagination[pageSize]=1&#x26;fields[0]=id`\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> data\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> res.\u003C/span>\u003Cspan style=\"color:#B392F0\">json\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> data.meta.pagination.total;\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\"> getProducts\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">page\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:#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:#79B8FF\">STRAPI_URL\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}/api/products?`\u003C/span>\u003Cspan style=\"color:#F97583\"> +\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    `pagination[page]=${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">page\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}&#x26;pagination[pageSize]=${\u003C/span>\u003Cspan style=\"color:#79B8FF\">PAGE_SIZE\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#F97583\"> +\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    `&#x26;fields[0]=slug&#x26;fields[1]=updatedAt`\u003C/span>\u003Cspan style=\"color:#F97583\"> +\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    `&#x26;filters[seo][noIndex][$ne]=true`\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\"> default\u003C/span>\u003Cspan style=\"color:#F97583\"> async\u003C/span>\u003Cspan style=\"color:#F97583\"> function\u003C/span>\u003Cspan style=\"color:#B392F0\"> sitemap\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\">MetadataRoute\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#B392F0\">Sitemap\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\"> total\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> getProductCount\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\"> pages\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> Math.\u003C/span>\u003Cspan style=\"color:#B392F0\">ceil\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(total \u003C/span>\u003Cspan style=\"color:#F97583\">/\u003C/span>\u003Cspan style=\"color:#79B8FF\"> PAGE_SIZE\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\"> allProducts\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#79B8FF\"> Promise\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#B392F0\">all\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    Array.\u003C/span>\u003Cspan style=\"color:#B392F0\">from\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({ length: pages }, (\u003C/span>\u003Cspan style=\"color:#FFAB70\">_\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">i\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#B392F0\"> getProducts\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(i \u003C/span>\u003Cspan style=\"color:#F97583\">+\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">))\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> productUrls\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> allProducts.\u003C/span>\u003Cspan style=\"color:#B392F0\">flatMap\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">response\u003C/span>\u003Cspan style=\"color:#F97583\"> =>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    response.data.\u003C/span>\u003Cspan style=\"color:#B392F0\">map\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">product\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> any\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      url: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">`${\u003C/span>\u003Cspan style=\"color:#79B8FF\">SITE_URL\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}/produits/${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">product\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">attributes\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\">      lastModified: \u003C/span>\u003Cspan style=\"color:#F97583\">new\u003C/span>\u003Cspan style=\"color:#B392F0\"> Date\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(product.attributes.updatedAt),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      changeFrequency: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'weekly'\u003C/span>\u003Cspan style=\"color:#F97583\"> as\u003C/span>\u003Cspan style=\"color:#F97583\"> const\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      priority: \u003C/span>\u003Cspan style=\"color:#79B8FF\">0.8\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:#E1E4E8\">    { url: \u003C/span>\u003Cspan style=\"color:#79B8FF\">SITE_URL\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, lastModified: \u003C/span>\u003Cspan style=\"color:#F97583\">new\u003C/span>\u003Cspan style=\"color:#B392F0\"> Date\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(), priority: \u003C/span>\u003Cspan style=\"color:#79B8FF\">1.0\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    ...\u003C/span>\u003Cspan style=\"color:#E1E4E8\">productUrls\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 filtre \u003Ccode>filters[seo][noIndex][$ne]=true\u003C/code> est crucial : il exclut du sitemap les pages marquées noindex dans le CMS. Soumettre à Google un sitemap contenant des URLs noindex est un signal contradictoire qui gaspille du \u003Ca href=\"/blog/mega-menus-et-seo-attention-au-crawl-budget\">crawl budget\u003C/a>.\u003C/p>\n\u003Cp>Pour les catalogues au-delà de 50 000 URLs, vous devrez implémenter un sitemap index avec des sitemaps enfants paginés. Next.js supporte nativement \u003Ccode>generateSitemaps()\u003C/code> pour ce cas.\u003C/p>\n\u003Ch3>Le problème des routes orphelines\u003C/h3>\n\u003Cp>En headless, le routing est géré côté frontend. Si un produit est supprimé dans Strapi mais que la route existe encore côté Next.js (via un cache ISR périmé), vous servez une page avec un appel API qui retourne 404 — mais le serveur HTTP renvoie un 200. C'est un soft 404, l'un des problèmes les plus courants et les plus insidieux en architecture headless.\u003C/p>\n\u003Cp>La solution : dans votre page dynamique, vérifiez explicitement le retour API et appelez \u003Ccode>notFound()\u003C/code> de Next.js :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\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\">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>\u003Cspan style=\"color:#FFAB70\">params\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:#79B8FF\"> res\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    `${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">process\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">env\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#79B8FF\">STRAPI_URL\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}/api/products?filters[slug][$eq]=${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">params\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}&#x26;populate=*`\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> data\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> res.\u003C/span>\u003Cspan style=\"color:#B392F0\">json\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">data.data \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> data.data.\u003C/span>\u003Cspan style=\"color:#79B8FF\">length\u003C/span>\u003Cspan style=\"color:#F97583\"> ===\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    notFound\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(); \u003C/span>\u003Cspan style=\"color:#6A737D\">// Renvoie un vrai HTTP 404\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  // ... rendu normal\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Surveillez le rapport \"Pages non indexées\" dans \u003Ca href=\"/blog/google-search-console-les-rapports-que-vous-ignorez\">Google Search Console\u003C/a> pour détecter les soft 404. Un outil de monitoring comme Seogard peut automatiser cette détection en crawlant vos pages régulièrement et en comparant le status code HTTP avec le contenu effectif de la page.\u003C/p>\n\u003Ch2>Scénario réel : migration headless d'un média à 18 000 pages\u003C/h2>\n\u003Cp>Un site média publiant 30 articles/jour migre de WordPress (thème custom) vers un front Nuxt 3 + Directus comme headless CMS. Le site totalise 18 000 URLs indexées, avec un trafic organique de 450 000 sessions/mois.\u003C/p>\n\u003Ch3>Phase 1 : audit pré-migration\u003C/h3>\n\u003Cp>Avant de toucher au code, l'équipe crawle le site existant avec Screaming Frog pour établir une baseline :\u003C/p>\n\u003Cul>\n\u003Cli>18 247 URLs indexées (rapport Coverage de Search Console)\u003C/li>\n\u003Cli>Temps de crawl moyen par page : 320 ms (log serveur)\u003C/li>\n\u003Cli>97 % des pages ont un title unique et une meta description\u003C/li>\n\u003Cli>612 redirections 301 existantes à préserver\u003C/li>\n\u003Cli>Maillage interne dense : moyenne de 42 liens internes par page (menu, sidebar, articles liés)\u003C/li>\n\u003C/ul>\n\u003Ch3>Phase 2 : les erreurs commises\u003C/h3>\n\u003Cp>Le front Nuxt 3 est déployé avec SSR activé (\u003Ccode>ssr: true\u003C/code> dans \u003Ccode>nuxt.config.ts\u003C/code>). Jusque-là, correct. Mais trois problèmes apparaissent dans les semaines suivantes :\u003C/p>\n\u003Cp>\u003Cstrong>Problème 1 : latence API.\u003C/strong> Chaque page SSR fait 3 appels API à Directus (contenu article, articles liés, menu de navigation). En charge, le TTFB monte à 2,8 secondes. Googlebot, qui a un crawl budget limité, réduit la fréquence de crawl. Le nombre de pages crawlées par jour chute de 1 200 à 340 (visible dans le rapport \"Statistiques d'exploration\" de Search Console).\u003C/p>\n\u003Cp>\u003Cstrong>Problème 2 : liens internes cassés.\u003C/strong> L'ancien WordPress générait des URLs en \u003Ccode>/2024/03/titre-article.html\u003C/code>. Le nouveau front utilise \u003Ccode>/articles/titre-article\u003C/code>. Les redirections 301 sont en place, mais les liens internes dans le contenu des articles (stockés dans Directus en rich text) pointent encore vers les anciennes URLs. Résultat : des milliers de liens internes passent par une 301 au lieu de pointer directement vers l'URL finale. Google suit les redirections, mais ça ralentit le crawl et dilue le budget.\u003C/p>\n\u003Cp>\u003Cstrong>Problème 3 : les pages de pagination.\u003C/strong> L'ancien WordPress avait \u003Ccode>/page/2/\u003C/code>, \u003Ccode>/page/3/\u003C/code> etc. avec des balises \u003Ccode>rel=\"prev\"\u003C/code> et \u003Ccode>rel=\"next\"\u003C/code>. Le nouveau front implémente un \u003Ca href=\"/blog/infinite-scroll-et-seo-le-guide-technique\">infinite scroll\u003C/a> côté client sans aucune URL de pagination accessible au crawler. Les articles au-delà de la page 1 perdent leur point d'entrée pour le crawl.\u003C/p>\n\u003Ch3>Phase 3 : correction et récupération\u003C/h3>\n\u003Cp>L'équipe met en place un cache Redis entre le frontend Nuxt et l'API Directus. Le TTFB redescend à 380 ms. Un script de migration parcourt tous les contenus dans Directus pour réécrire les liens internes vers les nouvelles URLs. Les pages de pagination sont rétablies sous forme de pages statiques \u003Ccode>/articles/page/2\u003C/code> accessibles au crawler, avec l'infinite scroll maintenu comme enhancement progressif côté client.\u003C/p>\n\u003Cp>Après 6 semaines de corrections, le crawl rate remonte à 980 pages/jour. Après 3 mois, 94 % des 18 247 URLs sont réindexées sous leurs nouvelles URLs. Le trafic organique, qui avait chuté de 35 % post-migration, revient à 92 % du niveau initial. Les 8 % restants sont attribués à la perte de link equity sur les backlinks externes qui pointent encore vers les anciennes URLs via 301.\u003C/p>\n\u003Cp>La leçon : la migration technique était propre côté rendu. C'est la couche \"contenu\" (liens internes dans les articles, pagination, performance API) qui a causé les dégâts.\u003C/p>\n\u003Ch2>Performance API et impact sur le crawl\u003C/h2>\n\u003Cp>La dépendance à une API externe est le risque structurel de toute architecture headless. Si l'API du CMS est lente, indisponible, ou rate-limitée, le SSR échoue et le crawler reçoit soit une erreur 5xx, soit une page incomplète.\u003C/p>\n\u003Ch3>Stratégies de résilience\u003C/h3>\n\u003Cp>\u003Cstrong>Cache multi-niveaux.\u003C/strong> Ne comptez pas uniquement sur le cache de Next.js/Nuxt. Ajoutez un cache applicatif (Redis, Memcached) entre votre frontend et l'API CMS, et un cache CDN (Cloudflare, Fastly) en amont du frontend. Pour les pages dont le contenu change rarement, un CDN avec des headers \u003Ccode>s-maxage\u003C/code> agressifs réduit drastiquement la charge sur votre stack :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Configuration Nginx en reverse proxy devant le frontend Node.js\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">location\u003C/span>\u003Cspan style=\"color:#B392F0\"> /produits/ \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\">http://nodejs_upstream;\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\"> 1h\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\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\"> 5m\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # Headers pour le CDN en amont\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    add_header \u003C/span>\u003Cspan style=\"color:#E1E4E8\">Cache-Control \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"public, s-maxage=3600, stale-while-revalidate=86400\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # Si le backend Node.js est down, servir le cache périmé\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 http_500 http_502 http_503 http_504;\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>stale-while-revalidate\u003C/code> est particulièrement utile : elle permet de servir une version en cache périmé pendant que le serveur régénère la page en arrière-plan. Googlebot reçoit toujours du contenu, même si l'API CMS est temporairement indisponible.\u003C/p>\n\u003Cp>\u003Cstrong>Circuit breaker côté frontend.\u003C/strong> Si l'API Directus/Strapi/Contentful ne répond pas en moins de 2 secondes, le frontend doit avoir un fallback — même si c'est un contenu partiel. Renvoyer un 503 avec un header \u003Ccode>Retry-After\u003C/code> est préférable à un timeout de 30 secondes qui gaspille le crawl budget de Googlebot.\u003C/p>\n\u003Cp>\u003Cstrong>Monitoring des temps de réponse API.\u003C/strong> Instrumentez les appels API du frontend avec des métriques (latence p50, p95, p99, taux d'erreur). Un pic de latence sur l'API CMS se traduit directement en baisse de crawl rate dans les jours suivants. Croisez les données de \u003Ca href=\"/blog/search-console-api-automatiser-le-reporting-seo\">Search Console\u003C/a> avec vos métriques APM pour corréler les deux.\u003C/p>\n\u003Ch2>Structured Data et données enrichies depuis un headless CMS\u003C/h2>\n\u003Cp>Les données structurées (JSON-LD) sont souvent le parent pauvre des implémentations headless. Dans WordPress, un plugin comme Yoast génère automatiquement le schema \u003Ccode>Article\u003C/code>, \u003Ccode>Product\u003C/code>, \u003Ccode>BreadcrumbList\u003C/code>. En headless, c'est à vous de le construire.\u003C/p>\n\u003Ch3>Approche recommandée : générer le JSON-LD côté serveur\u003C/h3>\n\u003Cp>Ne générez jamais les données structurées en JavaScript côté client. Google les prend en compte, mais les risques d'échec de rendering s'ajoutent. Construisez le JSON-LD dans votre couche SSR à partir des données de l'API :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// lib/structured-data.ts\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">interface\u003C/span>\u003Cspan style=\"color:#B392F0\"> ProductData\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\">  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\">  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\">  currency\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\">  sku\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\">  image\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\">  brand\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\"> 'InStock'\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'OutOfStock'\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\">  ratingValue\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\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> function\u003C/span>\u003Cspan style=\"color:#B392F0\"> generateProductJsonLd\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">product\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> ProductData\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\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:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> schema\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.image,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    sku: product.sku,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    brand: {\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\">'Brand'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      name: product.brand\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    },\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: product.currency,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      availability: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">`https://schema.org/${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">product\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">availability\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      url: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">`https://monsite.fr/produits/${\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>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    ...\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(product.reviewCount \u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0\u003C/span>\u003Cspan style=\"color:#F97583\"> &#x26;&#x26;\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      aggregateRating: {\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\">'AggregateRating'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        ratingValue: product.ratingValue,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        reviewCount: product.reviewCount\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\">  return\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\">(schema);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Testez systématiquement avec le \u003Ca href=\"https://search.google.com/test/rich-results\">Rich Results Test\u003C/a> de Google et validez dans \u003Ca href=\"/blog/chrome-devtools-pour-le-seo-astuces-avancees\">Chrome DevTools\u003C/a> que le JSON-LD est présent dans le HTML initial (pas uniquement après exécution du JS).\u003C/p>\n\u003Cp>Un point souvent négligé : la cohérence entre les données structurées et le contenu visible. Si votre schema \u003Ccode>Product\u003C/code> indique un prix de 49,99 € mais que l'API renvoie 54,99 € sur le rendu visible (à cause d'un cache désynchronisé entre deux appels), Google peut considérer les données structurées comme trompeuses et retirer les rich snippets.\u003C/p>\n\u003Ch2>Automatiser les checks SEO sur un frontend headless\u003C/h2>\n\u003Cp>L'architecture découplée multiplie les points de défaillance. Un changement dans le schéma API de Strapi peut casser silencieusement les meta tags sur 100 % des pages. Un déploiement frontend peut introduire une régression sur les canonicals. Il faut \u003Ca href=\"/blog/automatiser-les-checks-seo-dans-le-ci-cd\">automatiser les vérifications\u003C/a> dans la pipeline CI/CD.\u003C/p>\n\u003Ch3>Tests essentiels à intégrer\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Cstrong>Vérification du HTML rendu\u003C/strong> : pour chaque type de page (produit, catégorie, article), faire un \u003Ccode>curl\u003C/code> sur l'URL SSR et vérifier la présence du title, de la meta description, du canonical, et du JSON-LD dans le HTML brut (sans exécution JS).\u003C/li>\n\u003Cli>\u003Cstrong>Vérification des status codes\u003C/strong> : s'assurer qu'une URL de produit supprimé renvoie bien un 404, pas un 200 avec un contenu vide.\u003C/li>\n\u003Cli>\u003Cstrong>Vérification de la cohérence API/rendu\u003C/strong> : comparer le title dans la réponse API du CMS avec le title dans le HTML rendu. Toute divergence signale un bug de mapping.\u003C/li>\n\u003Cli>\u003Cstrong>Smoke test du sitemap\u003C/strong> : vérifier que le sitemap est accessible, parsable, et que le nombre d'URLs est dans une fourchette attendue (± 5 % du dernier déploiement).\u003C/li>\n\u003C/ul>\n\u003Cp>Un outil de monitoring continu comme Seogard complète ces checks CI/CD en surveillant la production 24/7. Un test CI attrape les régressions avant le déploiement ; un monitoring attrape celles qui passent entre les mailles — API CMS qui change de comportement, cache CDN qui sert une version corrompue, rate limiting qui se déclenche en pic de crawl.\u003C/p>\n\u003Ch2>Quand le headless n'est pas le bon choix\u003C/h2>\n\u003Cp>Le headless CMS est devenu un choix par défaut dans beaucoup d'équipes tech, souvent pour des raisons de stack préférentielle plutôt que de besoin réel. Quelques cas où il ajoute de la complexité sans bénéfice SEO :\u003C/p>\n\u003Cp>\u003Cstrong>Sites de moins de 200 pages avec peu de logique frontend.\u003C/strong> Un WordPress bien optimisé avec un bon hébergement surpassera en performance SEO un setup Contentful + Next.js déployé sur Vercel, pour une fraction du coût et de la complexité. Le rendu SSR natif de PHP est plus simple à debugger qu'une chaîne API → Node.js → CDN.\u003C/p>\n\u003Cp>\u003Cstrong>Équipes sans compétence frontend senior.\u003C/strong> L'architecture headless transfère la responsabilité SEO du CMS vers le frontend. Si votre équipe ne maîtrise pas le SSR, la gestion du cache, et les edge cases du rendering JavaScript par les crawlers, vous allez accumuler de la dette technique SEO invisible.\u003C/p>\n\u003Cp>\u003Cstrong>Sites où le \u003Ca href=\"/blog/contenu-genere-automatiquement-et-seo-ce-que-google-accepte\">contenu est principalement éditorial\u003C/a> et linéaire.\u003C/strong> Un blog d'entreprise, une base de connaissances — le headless n'apporte pas de valeur ajoutée si le frontend n'a pas besoin d'interactivité riche.\u003C/p>\n\u003Cp>En revanche, le headless prend tout son sens quand le même contenu doit alimenter un site web, une app mobile et des flux tiers (marketplaces, agrégateurs), ou quand le frontend nécessite des interactions complexes (configurateurs produit, dashboards, expériences immersives) que les templates d'un CMS monolithique ne peuvent pas supporter.\u003C/p>\n\u003Chr>\n\u003Cp>L'architecture headless ne dégrade pas le SEO par nature. Ce sont les implémentations bâclées — rendu client-side, meta tags oubliées, sitemap désynchronisé, API lente sans cache — qui causent les dégâts. La clé : traiter le SEO comme une contrainte d'architecture dès le jour 0, pas comme un patch post-lancement. Et mettre en place un monitoring automatisé qui détecte les régressions avant que Google ne les découvre.\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,12,[18,19,20,21,22],"headless","cms","api","seo","ssr","Headless CMS et SEO : risques techniques et architectures viables","Thu Apr 09 2026 14:02:18 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},"69d7cdb2aa6b273b0c7f638b","api-first-et-seo-servir-du-contenu-crawlable-depuis-une-api","https://seogard.io/blog/api-first-et-seo-servir-du-contenu-crawlable-depuis-une-api","2026-04-09T16:02:58.656Z","Patterns techniques pour servir du contenu SEO-friendly depuis une architecture API-first : SSR, ISR, stale-while-revalidate et monitoring.",[20,18,21,33,22],"rendering","API-first et SEO : rendre le contenu crawlable","Thu Apr 09 2026 16:02:58 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,21,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)"]