Un site e-commerce de 18 000 fiches produits migre de Create React App vers Next.js en SSR. Trois semaines après la mise en production, le trafic organique augmente de 41 %. Pas grâce à de nouveaux contenus, pas grâce à des backlinks — uniquement parce que Google peut enfin crawler et indexer l'intégralité du catalogue sans attendre l'exécution JavaScript. Ce scénario n'a rien d'exceptionnel. Il illustre un problème structurel que beaucoup de stacks frontend modernes créent sans que l'équipe technique en mesure l'impact.
Ce que Googlebot voit réellement : anatomie d'un crawl CSR vs SSR
La distinction entre SSR et CSR ne se réduit pas à "le HTML est généré côté serveur ou côté client". Le vrai sujet, c'est ce que le crawler reçoit au moment du fetch initial — et ce qu'il doit faire ensuite pour accéder au contenu.
Le parcours d'un crawl CSR
Quand Googlebot envoie une requête HTTP vers une page CSR pure (React SPA, Vue SPA sans pré-rendering), voici ce qu'il reçoit :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<title>Mon App</title>
<link rel="stylesheet" href="/static/css/main.a1b2c3.css" />
</head>
<body>
<div id="root"></div>
<script src="/static/js/bundle.7d8e9f.js"></script>
<script src="/static/js/vendor.4a5b6c.js"></script>
</body>
</html>
Ce document HTML est quasi vide. Aucun contenu textuel, aucune balise <h1>, aucune meta description dynamique, aucun lien interne exploitable. Tout est généré à l'exécution du JavaScript.
Google dispose d'un Web Rendering Service (WRS) basé sur une version headless de Chrome. Il peut techniquement exécuter ce JavaScript. Mais ce processus passe par deux phases distinctes, documentées par Google dans son architecture de crawl :
- Crawl (fetch) : Googlebot récupère le HTML brut. À ce stade, il voit le
<div id="root"></div>vide. - Render : le document est placé dans une file d'attente de rendering. Le WRS exécute le JavaScript, génère le DOM final, et réinjecte le résultat dans le pipeline d'indexation.
Le problème est le délai entre ces deux phases. Google a confirmé que la file de rendering peut introduire un décalage significatif — de quelques secondes à plusieurs jours selon la charge et la priorité de l'URL. Martin Splitt, Developer Advocate chez Google, l'a expliqué en détail dans les épisodes SEO de JavaScript de la documentation officielle.
Le parcours d'un crawl SSR
Avec le SSR, la même requête HTTP retourne un document HTML complet :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<title>Chaussures de trail Gore-Tex - Marque XYZ | MonShop</title>
<meta name="description" content="Chaussures de trail imperméables Gore-Tex par XYZ. Semelle Vibram, drop 8mm. Livraison 48h." />
<link rel="canonical" href="https://monshop.fr/chaussures/trail-gore-tex-xyz" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Chaussures de trail Gore-Tex - XYZ",
"brand": { "@type": "Brand", "name": "XYZ" },
"offers": {
"@type": "Offer",
"price": "149.90",
"priceCurrency": "EUR",
"availability": "https://schema.org/InStock"
}
}
</script>
</head>
<body>
<header>
<nav>
<a href="/chaussures">Chaussures</a>
<a href="/chaussures/trail">Trail</a>
</nav>
</header>
<main>
<h1>Chaussures de trail Gore-Tex - Marque XYZ</h1>
<p>Conçue pour les terrains techniques, cette chaussure combine...</p>
<!-- Contenu complet, liens internes, images avec alt... -->
</main>
</body>
</html>
Googlebot n'a pas besoin d'exécuter quoi que ce soit. Dès le fetch initial, il accède au contenu textuel, aux metas, aux liens internes, au structured data. L'indexation peut être immédiate.
L'impact concret sur la découverte de liens
Point souvent sous-estimé : en CSR, les liens internes présents dans le DOM final (menus, fil d'Ariane, maillage produit) n'existent pas dans le HTML brut. Googlebot les découvre uniquement après rendering. Sur un catalogue de 18 000 pages, si le maillage interne repose entièrement sur du JavaScript client-side, la découverte de nouvelles URLs par le crawler est ralentie proportionnellement au délai de rendering — et certaines pages peuvent ne jamais être découvertes si elles sont à plus de 3-4 niveaux de profondeur dans une navigation JavaScript-only.
Le mythe du "Google exécute le JavaScript, donc CSR n'est plus un problème"
Cette affirmation revient constamment dans les débats techniques. Elle est partiellement vraie et massivement trompeuse.
Ce que Google peut faire
Le WRS utilise une version Evergreen de Chromium. Il exécute ES6+, les frameworks modernes (React, Vue, Angular, Svelte), les appels fetch/XHR, et même certaines interactions basiques avec le DOM. Pour une page CSR simple qui charge ses données via un seul appel API au mount, Google finira probablement par indexer le contenu.
Ce que Google ne fait pas (ou fait mal)
Les interactions utilisateur ne sont pas simulées. Si votre contenu nécessite un clic, un scroll, un hover, ou toute forme d'interaction pour apparaître dans le DOM — Googlebot ne le verra pas. Les onglets produit, les accordéons "voir plus", les filtres de catégorie qui rechargent le contenu via JavaScript : tout cela est invisible au crawl dans la majorité des cas.
Les appels API qui échouent ne sont pas retentés. Si votre API produit retourne une 500 ou un timeout au moment du rendering, le WRS indexe la page dans l'état partiel où elle se trouve. Pas de retry automatique documenté. Sur un site avec 18 000 fiches produits et un backend parfois saturé, vous pouvez avoir des centaines de pages indexées avec un contenu partiel sans le savoir.
Le budget de rendering est fini. Google n'exécute pas le JavaScript de chaque URL qu'il découvre. Il priorise. Les pages de faible PageRank interne, les URLs profondes, les pages avec peu de signaux d'engagement — elles passent en dernier dans la file de rendering, ou n'y passent jamais.
Vous pouvez vérifier ce que Googlebot voit réellement pour n'importe quelle URL via l'outil d'inspection d'URL dans la Search Console. L'onglet "HTML rendu" vous montre le DOM après exécution JavaScript. Comparez-le avec le "HTML brut" — l'écart entre les deux vous donne la mesure exacte de votre dépendance au rendering côté Google.
Scénario réel : migration CSR vers SSR d'un catalogue e-commerce
Prenons un cas concret et détaillons les métriques.
Contexte
- Site e-commerce de chaussures outdoor : 18 200 pages (produits, catégories, guides)
- Stack initiale : React SPA (Create React App) + API REST
- Hébergement : CDN Cloudflare + API sur AWS
- Trafic organique pré-migration : ~34 000 sessions/mois
- Pages indexées dans la Search Console : 6 800 sur 18 200 (37 %)
Le diagnostic
En lançant un crawl Screaming Frog en mode "JavaScript rendering" vs "HTML only", l'écart est flagrant :
- HTML only : 94 % des fiches produit retournent un
<title>générique ("Mon App"), 0 balise<h1>détectée, 0 lien interne dans le<body>. - JavaScript rendering : contenu complet, metas correctes, maillage interne présent.
La configuration Screaming Frog pour faire cette comparaison :
# Screaming Frog - Configuration rendering JavaScript
Configuration > Spider > Rendering > JavaScript
- JavaScript rendering : Activer
- AJAX Timeout : 10 secondes
- Allow Cookies : Oui
- Crawl Embedded Resources : Oui
# Lancer deux crawls :
# 1. Rendering = "Old Googlebot" (HTML only)
# 2. Rendering = "JavaScript" (Chromium)
# Exporter les deux rapports et comparer :
# - Title, H1, Meta Description, Word Count, Internal Links
Le Word Count moyen en HTML only était de 12 mots par page (le chrome de l'app). En JavaScript rendering : 847 mots. Google devait donc exécuter le JS pour accéder à 99 % du contenu.
Dans la Search Console, le Coverage Report montrait 11 400 URLs "Discovered – currently not indexed". Ces pages étaient dans la file d'attente du WRS, certaines depuis plus de 8 semaines.
La migration
L'équipe migre vers Next.js avec getServerSideProps pour les fiches produit et getStaticProps avec ISR (Incremental Static Regeneration) pour les catégories. Le SSR garantit que chaque requête Googlebot reçoit un HTML complet. L'ISR permet de ne pas régénérer les 380 pages de catégories à chaque déploiement.
// pages/product/[slug].tsx — Next.js SSR pour les fiches produit
import type { GetServerSideProps } from 'next';
import Head from 'next/head';
interface Product {
slug: string;
name: string;
description: string;
price: number;
brand: string;
inStock: boolean;
category: { name: string; slug: string };
}
interface Props {
product: Product;
}
export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
const { slug } = context.params!;
const res = await fetch(`${process.env.API_URL}/products/${slug}`, {
headers: { 'Cache-Control': 'stale-while-revalidate=60' },
});
if (!res.ok) {
return { notFound: true }; // Retourne une 404 propre au lieu d'une page vide
}
const product: Product = await res.json();
// Header Cache-Control pour le CDN — réduit la charge serveur
context.res.setHeader(
'Cache-Control',
'public, s-maxage=300, stale-while-revalidate=600'
);
return { props: { product } };
};
export default function ProductPage({ product }: Props) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
brand: { '@type': 'Brand', name: product.brand },
offers: {
'@type': 'Offer',
price: product.price.toFixed(2),
priceCurrency: 'EUR',
availability: product.inStock
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
},
};
return (
<>
<Head>
<title>{`${product.name} - ${product.brand} | MonShop`}</title>
<meta name="description" content={product.description.slice(0, 155)} />
<link rel="canonical" href={`https://monshop.fr/product/${product.slug}`} />
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
</Head>
<nav aria-label="Fil d'Ariane">
<a href="/">Accueil</a> >
<a href={`/category/${product.category.slug}`}>{product.category.name}</a> >
<span>{product.name}</span>
</nav>
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
</main>
</>
);
}
Points techniques à noter dans ce code :
- Le
notFound: trueretourne un vrai statut HTTP 404 si l'API ne trouve pas le produit. En CSR, une page produit inexistante retourne souvent un 200 avec un message "Produit non trouvé" rendu en JavaScript — Googlebot indexe alors une page vide ou un soft 404. - Le header
Cache-Controlavecs-maxageetstale-while-revalidatepermet au CDN de servir des réponses cachées tout en maintenant un SSR frais. Sans cela, le SSR à 18 000 pages génère une charge serveur conséquente. - Le structured data est injecté dans le HTML initial, pas ajouté en JavaScript client-side.
Les résultats à 3 semaines
- Pages indexées : de 6 800 à 16 900 (93 % du catalogue)
- URLs "Discovered – currently not indexed" : de 11 400 à 1 200
- Trafic organique : +41 % sur la période (34 000 → 48 000 sessions/mois)
- Temps moyen de première indexation d'une nouvelle fiche produit : de 12 jours à 1,8 jour
Le gain de trafic n'est pas venu de meilleures positions sur les pages déjà indexées. Il est venu presque intégralement des pages qui n'étaient tout simplement pas indexées avant la migration.
Les zones grises : quand le CSR ne pose pas de problème SEO
Il serait malhonnête de prétendre que le SSR est toujours nécessaire. Plusieurs cas rendent le CSR parfaitement viable :
Sites à faible volume de pages
Un SaaS B2B avec 40 pages marketing n'a pas de problème de crawl budget. Google finira par exécuter le JavaScript de toutes les pages, même avec un délai. La file de rendering du WRS pose problème à l'échelle — à 40 pages, l'impact est négligeable.
Pages derrière authentification
Votre dashboard utilisateur, vos pages de compte, vos outils internes : aucun intérêt à les indexer. Le CSR y est parfaitement adapté et simplifie l'architecture.
Contenus mis à jour en temps réel côté client
Des éléments comme un compteur de stock, un prix dynamique lié à une devise, ou un composant de chat : ces éléments doivent rester en CSR même sur une page SSR. L'approche hybride (SSR pour le contenu principal + hydration côté client pour les éléments dynamiques) est exactement ce que proposent Next.js, Nuxt et SvelteKit.
Le vrai critère de décision
La question n'est pas "SSR ou CSR ?". C'est : le contenu que Google doit indexer est-il présent dans le HTML initial ? Si oui, peu importe que des parties secondaires de la page soient en CSR. Si non, vous avez un problème de rendering — et sa gravité est proportionnelle au nombre de pages concernées.
Diagnostiquer votre propre stack : la méthode en 4 étapes
Étape 1 : comparer le HTML brut et le DOM rendu
Utilisez Chrome DevTools pour voir le HTML tel que le serveur l'envoie, avant toute exécution JavaScript :
# Récupérer le HTML brut avec curl (ce que Googlebot voit au fetch initial)
curl -s -A "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" \
"https://monshop.fr/product/trail-gore-tex-xyz" \
| grep -E "<title>|<meta name=\"description\"|<h1>|<a href="
# Comparer avec le DOM rendu via Puppeteer ou Chrome headless
npx puppeteer-cli screenshot \
--url "https://monshop.fr/product/trail-gore-tex-xyz" \
--html \
| grep -E "<title>|<meta name=\"description\"|<h1>|<a href="
Si le curl retourne un <title> générique et aucun <h1>, tandis que le rendering Puppeteer retourne le contenu complet — votre contenu SEO critique dépend entièrement de l'exécution JavaScript.
Étape 2 : vérifier dans la Search Console
L'outil d'inspection d'URL montre deux visions : le HTML brut récupéré par Googlebot et le HTML après rendering. Pour un échantillon de 20-30 URLs stratégiques (pages produit à fort volume, pages catégories), comparez les deux. Cherchez spécifiquement :
- Le
<title>est-il identique dans les deux versions ? - Les liens internes du maillage principal sont-ils présents dans le HTML brut ?
- Le contenu textuel principal (h1, paragraphes produit) apparaît-il avant rendering ?
Étape 3 : analyser le rapport de couverture
Dans la Search Console, le rapport "Pages" (anciennement "Couverture") révèle les URLs bloquées à différentes étapes. Les statuts à surveiller :
- "Discovered – currently not indexed" : Google connaît l'URL mais ne l'a pas encore crawlée/rendue. Un volume élevé (>10 % du total de vos URLs) sur une longue période suggère un problème de priorisation, souvent aggravé par le CSR.
- "Crawled – currently not indexed" : Google a récupéré la page mais a décidé de ne pas l'indexer. Sur un site CSR, cela peut signifier que le contenu après rendering est jugé insuffisant ou dupliqué (pages qui retournent toutes le même shell HTML).
Étape 4 : crawl complet avec Screaming Frog
Lancez deux crawls parallèles : un en mode "HTML only", un en mode "JavaScript rendering". Exportez les deux datasets et comparez les métriques page par page. Les écarts critiques à chercher :
- Word count : si le HTML only affiche <50 mots et le JS rendering >500 mots, votre contenu est 100 % JavaScript-dependent.
- Nombre de liens internes : si le HTML only montre 2-3 liens (header/footer statiques) et le JS rendering en montre 40+, votre maillage interne est invisible au premier crawl.
- Statuts HTTP : certaines pages peuvent retourner un 200 en HTML only alors que le contenu JS échoue silencieusement à charger — un soft 404 invisible.
ISR, SSG, streaming SSR : les alternatives au SSR classique et leurs implications SEO
Le SSR "pur" (rendu à chaque requête) pose un problème de performance serveur à l'échelle. Plusieurs stratégies de rendering existent, chacune avec des implications SEO spécifiques.
Static Site Generation (SSG)
Les pages sont générées au build time. Le HTML est servi depuis un CDN, TTFB quasi nul. Idéal pour le SEO — le HTML est complet et la performance est maximale.
Limite : un catalogue de 18 000 produits qui change quotidiennement (stocks, prix, nouveaux produits) ne peut pas être régénéré intégralement à chaque déploiement. Le build prendrait des heures.
Incremental Static Regeneration (ISR)
Next.js propose un compromis : les pages sont générées statiquement mais se régénèrent en arrière-plan après un intervalle défini. Le premier visiteur après l'expiration reçoit la version cachée (stale) pendant que la nouvelle est générée.
Pour le SEO, l'ISR est excellent : le HTML est toujours complet, le TTFB reste bas, et le contenu est raisonnablement frais. Le piège : si la régénération échoue (API down, timeout), la page stale continue d'être servie — ce qui peut être préférable à une 500, mais peut aussi servir un contenu obsolète à Googlebot.
Streaming SSR (React Server Components)
Avec Next.js App Router et React Server Components, le HTML est envoyé en streaming. Le navigateur reçoit le shell immédiatement, puis les parties dynamiques arrivent progressivement. Pour Googlebot, le comportement dépend de l'implémentation : les Suspense boundaries sont résolues côté serveur avant envoi, donc le HTML final est complet. Mais si certains composants utilisent 'use client' et chargent leurs données côté client, on retombe dans les problèmes du CSR pour ces portions.
La règle reste la même : tout contenu SEO critique (titre, meta, contenu textuel principal, liens internes) doit être dans un Server Component, pas dans un Client Component qui fetch ses données au mount.
Configurer le monitoring de votre rendering en production
Le piège le plus dangereux n'est pas de lancer un site en CSR. C'est de migrer vers le SSR, de valider que tout fonctionne, puis de subir une régression silencieuse 3 mois plus tard quand un développeur ajoute un 'use client' sur un composant critique ou quand un déploiement casse le SSR sans que personne ne s'en aperçoive.
Les signaux d'alerte à monitorer en continu :
- Le
<title>et la<meta description>changent entre le HTML brut et le DOM rendu — indique que ces éléments sont injectés côté client, probablement via un hookuseEffect. - Le word count du HTML brut chute brutalement après un déploiement — le SSR est peut-être cassé et le serveur retourne le shell client-side.
- Le nombre de pages "Discovered – currently not indexed" augmente dans la Search Console — Google a des difficultés à traiter vos pages, possiblement à cause d'une dégradation du rendering.
Un outil de monitoring comme Seogard détecte automatiquement ces régressions en comparant le HTML servi à chaque crawl avec les versions précédentes. Quand une meta description disparaît du HTML initial ou qu'un <h1> passe d'un rendu serveur à un rendu client, l'alerte part avant que l'impact SEO ne soit mesurable dans la Search Console — où les données ont toujours 2-3 jours de retard minimum.
La combinaison gagnante : un crawl Screaming Frog mensuel complet pour l'audit de fond, la Search Console pour les tendances macro, et un monitoring continu pour les régressions entre les audits.
Ce qu'il faut retenir
Le choix SSR vs CSR n'est pas un débat d'architecture frontend — c'est une décision SEO avec des conséquences mesurables sur l'indexation et le trafic, proportionnelles au nombre de pages de votre site. Le critère décisif est simple : le contenu que Google doit indexer doit être dans le HTML initial. Tout le reste est négociable.
Vérifiez aujourd'hui ce que curl retourne sur vos 10 pages les plus stratégiques. Si le HTML est vide, vous laissez votre indexation entre les mains de la file d'attente du Web Rendering Service de Google — et cette file, vous ne la contrôlez pas.