Un site e-commerce de 28 000 produits migre de Create React App vers Next.js. L'équipe choisit SSG par défaut, lance le build : 47 minutes. Le déploiement suivant échoue parce qu'un appel API timeout sur le 14 000e produit. Le CTO bascule tout en SSR. Le Time to First Byte passe de 80ms à 1,2 seconde. Le crawl rate de Googlebot chute de 40% en deux semaines. Le vrai problème n'a jamais été le framework — c'est le choix du mode de rendering, page par page, en fonction de ce que chaque URL exige réellement.
Ce que SSG, SSR et ISR signifient pour Googlebot
Avant de choisir, il faut comprendre ce que chaque mode produit du point de vue du crawler — pas du point de vue du développeur.
SSG : HTML figé au build time
Static Site Generation pré-rend chaque page en HTML lors du next build (ou nuxt generate). Le fichier HTML est servi directement depuis le CDN. Googlebot reçoit un document complet sans exécuter une seule ligne de JavaScript.
C'est le cas idéal pour l'indexation : latence minimale, contenu déterministe, aucune dépendance runtime. Mais le contenu est figé jusqu'au prochain build. Pour un site de 200 pages "à propos", c'est parfait. Pour un catalogue produit avec des prix qui changent toutes les heures, c'est un piège.
SSR : HTML généré à chaque requête
Server-Side Rendering construit le HTML à chaque requête serveur. Googlebot reçoit du HTML frais, toujours à jour. Le coût : chaque requête sollicite le serveur (appels BDD, APIs tierces, rendu React/Vue côté serveur). Le TTFB dépend directement de la performance de votre stack backend.
Un SSR mal optimisé qui répond en 2+ secondes envoie un signal négatif à Googlebot. Le crawler a un crawl budget limité — s'il passe trop de temps à attendre vos réponses, il réduit la fréquence de crawl. C'est documenté par Google.
ISR : le compromis entre fraîcheur et performance
Incremental Static Regeneration sert une page statique depuis le cache, puis la régénère en arrière-plan après une durée configurable (revalidate). Googlebot reçoit du HTML statique rapide, et le contenu se met à jour sans rebuild complet.
L'ISR résout le problème du build de 47 minutes mentionné en introduction. Mais il introduit une subtilité : pendant la fenêtre de revalidation, Googlebot peut indexer du contenu périmé. Si votre revalidate est à 3600 secondes et qu'un produit passe en rupture de stock, Google peut afficher "En stock" pendant une heure. Ce n'est pas un bug — c'est un trade-off architectural.
Implémentation concrète : Next.js et Nuxt
Next.js App Router (React Server Components)
Depuis Next.js 13+ avec l'App Router, le modèle a changé. Par défaut, les composants sont des Server Components rendus côté serveur. Le contrôle du caching se fait via les segments de route et les options de fetch.
SSG (statique par défaut) :
// app/about/page.tsx
// Par défaut, cette page est statiquement générée au build time
// Aucune donnée dynamique, aucun revalidate
export default function AboutPage() {
return (
<main>
<h1>À propos de MarchandFrançais.fr</h1>
<p>Fondée en 2019, notre marketplace...</p>
</main>
);
}
// Force le comportement statique explicitement si nécessaire
export const dynamic = 'force-static';
ISR avec revalidation :
// app/products/[slug]/page.tsx
import { notFound } from 'next/navigation';
// Génère les 500 produits les plus populaires au build time
export async function generateStaticParams() {
const topProducts = await fetch('https://api.marchandfrancais.fr/products/top500');
const products = await topProducts.json();
return products.map((p: { slug: string }) => ({ slug: p.slug }));
}
async function getProduct(slug: string) {
const res = await fetch(`https://api.marchandfrancais.fr/products/${slug}`, {
next: { revalidate: 1800 } // Revalide toutes les 30 minutes
});
if (!res.ok) return null;
return res.json();
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
const product = await getProduct(params.slug);
if (!product) return {};
return {
title: `${product.name} - Achat en ligne | MarchandFrançais.fr`,
description: product.metaDescription,
alternates: {
canonical: `https://marchandfrancais.fr/products/${params.slug}`,
},
};
}
export default async function ProductPage({ params }: { params: { slug: string } }) {
const product = await getProduct(params.slug);
if (!product) notFound();
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
<span>{product.price} €</span>
{/* Le JSON-LD est critique pour les rich snippets */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "Product",
name: product.name,
description: product.description,
offers: {
"@type": "Offer",
price: product.price,
priceCurrency: "EUR",
availability: product.inStock
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock",
},
}),
}}
/>
</main>
);
}
Le revalidate: 1800 est le paramètre central. Trop court (60s) et vous surchargez vos APIs backend à chaque visite. Trop long (86400s) et vous servez du contenu périmé à Googlebot pendant 24h.
SSR pur (pas de cache) :
// app/search/page.tsx
// Page de résultats de recherche interne : toujours dynamique
export const dynamic = 'force-dynamic';
// Équivalent : export const revalidate = 0;
export default async function SearchPage({
searchParams,
}: {
searchParams: { q?: string; page?: string };
}) {
const query = searchParams.q || '';
const page = parseInt(searchParams.page || '1');
const results = await fetch(
`https://api.marchandfrancais.fr/search?q=${encodeURIComponent(query)}&page=${page}`,
{ cache: 'no-store' }
);
const data = await results.json();
return (
<main>
<h1>Résultats pour "{query}"</h1>
{/* Noindex sur les pages de recherche :
elles n'ont aucune valeur SEO et gaspillent du crawl budget */}
{data.results.map((item: any) => (
<article key={item.id}>
<a href={`/products/${item.slug}`}>{item.name}</a>
</article>
))}
</main>
);
}
Nuxt 3 (Vue)
Nuxt 3 offre un contrôle similaire avec les route rules dans nuxt.config.ts :
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// Pages statiques : SSG pur
'/about': { prerender: true },
'/mentions-legales': { prerender: true },
// Fiches produits : ISR avec revalidation toutes les 30 minutes
'/products/**': { swr: 1800 },
// Pages catégories : ISR avec revalidation plus longue
'/categories/**': { swr: 3600 },
// Recherche : SSR pur, pas de cache
'/search': { ssr: true, cache: false },
// Dashboard utilisateur : CSR only, pas besoin d'indexation
'/dashboard/**': { ssr: false },
},
});
L'approche Nuxt est déclarative : vous définissez la stratégie par pattern d'URL dans un seul fichier. C'est plus lisible qu'éparpiller des export const dynamic dans chaque fichier de route Next.js, surtout sur un site de plus de 100 templates.
Matrice de décision par type de site
Le choix du rendering ne se fait pas par framework. Il se fait par type de page, en croisant trois critères : fréquence de mise à jour du contenu, nombre de pages, et criticité SEO.
E-commerce (5 000 à 50 000 produits)
| Type de page | Mode recommandé | Revalidate | Justification |
|---|---|---|---|
| Fiches produits | ISR | 1800s - 3600s | Contenu semi-stable, volume élevé, build complet impossible |
| Pages catégories | ISR | 3600s - 7200s | Mises à jour modérées, forte valeur SEO (mots-clés transactionnels) |
| Homepage | ISR | 300s - 600s | Contenu promotionnel changeant, crawlée très fréquemment |
| Pages CMS (à propos, CGV) | SSG | — | Contenu figé, modifié 2 fois par an |
| Recherche interne | SSR (noindex) | — | Dynamique par nature, aucune valeur SEO directe |
| Panier / compte | CSR | — | Contenu privé, pas d'indexation |
Le piège classique : mettre les pages catégories en SSR "parce qu'elles changent quand on ajoute un produit". En réalité, ajouter un produit à une catégorie ne justifie pas un rendu serveur à chaque requête. ISR avec 1h de revalidation couvre largement le besoin.
Média / éditeur de contenu (10 000 à 100 000 articles)
| Type de page | Mode recommandé | Revalidate | Justification |
|---|---|---|---|
| Articles récents (< 48h) | SSR ou ISR 300s | 300s | Contenu chaud, corrections fréquentes, mises à jour live |
| Articles anciens (> 48h) | SSG ou ISR 86400s | 86400s | Contenu stable, rarement modifié |
| Pages thématiques / tags | ISR | 3600s | Agrégation d'articles, mises à jour modérées |
| Homepage | ISR | 120s - 300s | Rotation éditoriale rapide |
L'insight ici : un site média devrait utiliser une stratégie hybride basée sur l'âge du contenu. Les articles de moins de 48h sont chauds — corrections, ajouts de paragraphes, mises à jour du titre. Passé ce délai, le contenu se stabilise. Next.js permet ça avec un middleware ou une logique dans generateStaticParams.
SaaS / site vitrine (50 à 500 pages)
SSG pur pour la quasi-totalité du site. Build de 30 secondes à 2 minutes. Aucune infrastructure serveur à maintenir. Déploiement sur Vercel, Netlify ou Cloudflare Pages.
La seule exception : si vous avez un blog avec des composants interactifs (calculateurs, démos) qui nécessitent une hydratation complexe, surveillez les erreurs d'hydratation qui peuvent générer un contenu différent entre le HTML statique et le rendu client.
Le scénario réel : migration d'un e-commerce de 28 000 pages
Reprenons le cas d'ouverture avec des chiffres concrets.
Contexte : MarchandFrançais.fr, 28 000 fiches produits, 1 200 pages catégories, 45 pages CMS. Stack existante : React SPA (Create React App) avec rendering client. Googlebot voyait une page blanche sur 30% des pages — confirmé via l'inspection d'URL dans Search Console.
Phase 1 — Tentative SSG intégral : next build génère les 28 000 pages produit. Build time : 47 minutes. La CI/CD (GitHub Actions, runner 4 vCPU) timeout à chaque déploiement. L'API catalogue retourne des 429 (rate limit) à partir de la 12 000e page. Le build échoue 3 fois sur 5.
Phase 2 — Bascule SSR intégral : TTFB moyen passe à 1,1s (appel API produit + rendu React côté serveur). L'infra serveur (3 instances t3.medium sur AWS) sature à 200 requêtes/seconde. Le crawl de Googlebot génère 15 000 requêtes/jour — combiné au trafic organique, le serveur monte à 85% CPU. Le crawl rate visible dans Search Console (rapport "Statistiques d'exploration") diminue de 40% en deux semaines. Les nouvelles pages ne sont indexées qu'après 8-12 jours au lieu de 2-3.
Phase 3 — Architecture hybride ISR :
- 500 produits best-sellers : pré-rendus au build via
generateStaticParams, revalidation 30 min - 27 500 produits restants : ISR on-demand (première visite déclenche le rendu, mise en cache ensuite), revalidation 2h
- 1 200 catégories : ISR, revalidation 1h
- 45 pages CMS : SSG pur
Résultat : build time ramené à 3 minutes (500 pages pré-rendues + pages CMS). TTFB moyen : 120ms (pages servies depuis le cache CDN). Charge serveur divisée par 8. Le crawl rate remonte en 10 jours. L'indexation des nouvelles pages revient à 2-3 jours.
Le delta entre la phase 2 et la phase 3, en termes de trafic organique, s'est manifesté sur 6 semaines : +23% de pages indexées (visible dans le rapport "Pages" de Search Console), et une amélioration du TTFB médian de 1,1s à 120ms mesuré via Chrome UX Report.
Pièges techniques et edge cases
Le stale content en ISR
Quand Googlebot crawle une page ISR dont le revalidate a expiré, il reçoit la version en cache (stale). La régénération se fait en arrière-plan, et la version fraîche ne sera servie qu'à la requête suivante. Si Googlebot ne repasse pas avant le prochain cycle de revalidation, il peut indexer du contenu qui a une heure, un jour, ou une semaine de retard.
Pour les pages où la fraîcheur est critique (prix, disponibilité), Next.js propose le on-demand revalidation :
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const { secret, path } = await request.json();
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ message: 'Invalid secret' }, { status: 401 });
}
revalidatePath(path);
return NextResponse.json({ revalidated: true, path });
}
Votre PIM ou votre back-office appelle ce endpoint quand un prix change ou qu'un produit passe en rupture. La page est régénérée immédiatement, pas au prochain cycle de revalidation. C'est la différence entre "ISR naïf" et "ISR piloté par les événements métier".
Le fallback: 'blocking' et ses implications SEO
En Next.js Pages Router (legacy mais encore très utilisé), le paramètre fallback dans getStaticPaths détermine ce qui se passe quand Googlebot demande une page qui n'a pas été pré-rendue :
fallback: false→ 404. Googlebot reçoit un 404 pour toute page non listée dansgetStaticPaths. Catastrophique si vous n'avez pas listé tous vos produits.fallback: true→ la page est servie sans données (état de chargement), puis hydratée côté client. Googlebot reçoit le squelette vide. Même problème qu'un CSR classique.fallback: 'blocking'→ le serveur attend la fin du rendu avant d'envoyer la réponse. Googlebot reçoit le HTML complet. C'est le seul choix viable pour le SEO.
Avec l'App Router, ce problème disparaît : les pages non pré-rendues sont automatiquement rendues en mode "blocking" — le serveur renvoie le HTML complet. Mais si vous êtes encore sur le Pages Router, vérifiez chaque getStaticPaths.
Hydratation et contenu divergent
Un piège subtil avec SSR et ISR : le HTML envoyé par le serveur et le DOM produit par l'hydratation client peuvent diverger. Un composant qui affiche new Date() ou qui lit window.innerWidth produira un contenu différent côté serveur et côté client. React 18 émet un warning dans la console, mais le vrai risque SEO est que Googlebot indexe le HTML serveur tandis que l'utilisateur voit autre chose.
Ce type de bug est détaillé ici. Pour le détecter à l'échelle, comparez le rendu Googlebot (via l'onglet "Tester l'URL en ligne" de Search Console) avec le DOM réel dans Chrome DevTools. Sur un site de plus de 1 000 pages, cette vérification manuelle est impraticable — un outil de monitoring comme Seogard automatise cette comparaison en détectant les divergences HTML entre le rendu serveur initial et le DOM post-hydratation.
Les headers HTTP que vous oubliez
Le mode de rendering impacte les headers HTTP, et les headers impactent le comportement du crawler.
Vérifiez systématiquement avec curl :
# Vérifier les headers d'une page ISR
curl -sI https://marchandfrancais.fr/products/chaussures-randonnee-gore-tex | grep -iE 'cache-control|x-nextjs|age|x-vercel'
# Résultat attendu pour une page ISR sur Vercel :
# cache-control: s-maxage=1800, stale-while-revalidate
# x-vercel-cache: HIT
# age: 847
Si vous voyez cache-control: no-store sur une page censée être ISR, votre configuration est cassée. Si age est systématiquement à 0, le cache CDN ne fonctionne pas. Sur un hébergement custom (pas Vercel/Netlify), vous devez configurer le reverse proxy vous-même :
# nginx.conf — cache des pages ISR
location /products/ {
proxy_pass http://nextjs_upstream;
proxy_cache nextjs_cache;
proxy_cache_valid 200 30m;
proxy_cache_use_stale updating error timeout;
proxy_cache_background_update on;
proxy_cache_lock on;
# Header pour le debug
add_header X-Cache-Status $upstream_cache_status;
}
Le proxy_cache_use_stale updating est l'équivalent Nginx du stale-while-revalidate — il sert le cache périmé pendant que la nouvelle version est générée en arrière-plan. Sans ça, votre ISR custom se comporte comme du SSR pur pendant la régénération.
Auditer et monitorer le rendering en production
Le choix du rendering ne se valide pas au déploiement. Il se valide quand Googlebot le confirme.
Vérification immédiate post-déploiement
-
Search Console > Inspection d'URL : testez 5-10 URLs représentatives de chaque type de page. Vérifiez que le HTML rendu contient le contenu attendu (title, h1, structured data). Le screenshot montre ce que Googlebot "voit".
-
Screaming Frog en mode JavaScript rendering : crawlez votre site avec le rendering JavaScript activé (Configuration > Spider > Rendering > JavaScript). Comparez le HTML brut (onglet "Response Body") avec le HTML rendu (onglet "Rendered Page"). Sur des pages SSR/SSG/ISR bien configurées, les deux doivent être quasi-identiques. Une divergence significative signale un problème d'hydratation ou de CSR résiduel.
-
Chrome DevTools > Network : chargez une page produit. Désactivez JavaScript (DevTools > Settings > Debugger > Disable JavaScript). Rechargez. Si le contenu disparaît, votre page dépend du JavaScript client malgré le SSR/ISR — signe que le rendering serveur est incomplet ou que des composants critiques sont lazy-loaded côté client.
Monitoring continu
Le rendering peut régresser silencieusement. Un développeur ajoute un 'use client' en haut d'un composant qui contenait des metadata. Un build ISR échoue partiellement et certaines pages retombent en SSR avec un TTFB dégradé. Une mise à jour de dépendance casse le Server Component qui génère le JSON-LD.
Ces régressions ne se manifestent pas immédiatement dans le trafic. Elles apparaissent 2-4 semaines plus tard quand Googlebot a recrawlé les pages affectées et mis à jour son index. À ce stade, le lien de cause à effet est quasi impossible à établir sans un historique de monitoring. Seogard détecte ce type de régression en temps réel — disparition de meta tags, changement de status code, dégradation du HTML servi — avant que l'impact SEO ne se matérialise dans les données de Search Console.
Les métriques à surveiller dans Search Console
Dans le rapport "Statistiques d'exploration" (Settings > Crawl stats), surveillez trois indicateurs après un changement de stratégie de rendering :
- Temps de réponse moyen : doit baisser si vous passez de SSR à ISR. Une hausse indique que le cache CDN ne fonctionne pas correctement.
- Nombre de requêtes par jour : un TTFB plus bas encourage Googlebot à crawler plus. Si le nombre de requêtes stagne malgré un TTFB amélioré, vérifiez votre sitemap et votre maillage interne.
- Pourcentage de codes 200 : après une migration, un pic de 404 ou 500 signale des pages qui n'ont pas été migrées ou des routes ISR qui échouent à la régénération.
Quand le rendering seul ne suffit pas
Le mode de rendering optimise la manière dont Googlebot reçoit votre HTML. Mais il ne corrige pas un contenu thin, une architecture d'information défaillante, ou un maillage interne cassé.
Un site 100% SSG avec un TTFB de 50ms mais des pages produits sans contenu unique (juste un titre et un prix) ne rankera pas mieux qu'un SSR à 800ms avec des descriptions riches de 300 mots, des avis clients, et du structured data complet.
Le rendering est une condition nécessaire — Googlebot doit recevoir le HTML — mais pas suffisante. Avant de passer trois sprints à optimiser votre stratégie ISR, vérifiez que le contenu servi mérite d'être indexé.
Le choix SSG/SSR/ISR est une décision d'architecture qui se prend page par page, pas globalement. La bonne approche : SSG pour le contenu figé, ISR avec revalidation événementielle pour le contenu semi-dynamique, SSR uniquement pour les pages où le cache n'a aucun sens. Surveillez le comportement réel de Googlebot via Search Console et un monitoring automatisé pour détecter les régressions avant qu'elles n'affectent votre trafic.