Un catalogue e-commerce de 40 000 SKUs déployé en SSG met 47 minutes à builder. Chaque correction de faute de frappe sur une fiche produit relance le cycle complet. À l'inverse, un blog corporate de 200 articles servi en SSR paie du compute serveur pour des pages dont le contenu ne change qu'une fois par mois. Le choix du mode de rendering n'est pas une préférence d'architecture — c'est une décision qui impacte directement le crawl budget, le TTFB, la freshness du contenu indexé, et in fine votre trafic organique.
Les trois modes en 60 secondes : ce qui arrive réellement côté serveur
Avant de choisir, il faut comprendre ce qui se passe entre la requête de Googlebot et le HTML qu'il reçoit. Pas la version marketing — la mécanique réelle.
SSG (Static Site Generation)
Le HTML est généré au moment du build. Chaque URL correspond à un fichier .html pré-rendu, servi depuis un CDN ou un serveur de fichiers statiques. Googlebot reçoit un document complet sans aucune exécution côté serveur au moment de la requête.
Le TTFB est minimal (souvent < 50ms depuis un edge CDN). Le contenu est celui du dernier build — ni plus, ni moins.
SSR (Server-Side Rendering)
Le HTML est généré à chaque requête. Le serveur exécute le framework (Next.js, Nuxt, etc.), fait les appels API nécessaires, produit le HTML complet, et le renvoie. Googlebot reçoit un document à jour, mais le TTFB dépend de la charge serveur, de la latence des API en amont, et de la complexité du rendu.
Pour ceux qui viennent d'un contexte SPA et veulent comprendre la différence fondamentale avec le rendu côté client, l'article SSR vs CSR : impact réel sur le SEO détaille les mécanismes d'indexation de chaque approche.
ISR (Incremental Static Regeneration)
Un hybride introduit par Next.js (et adopté sous diverses formes par Nuxt avec routeRules). La page est servie depuis un cache statique, mais se régénère en arrière-plan après un intervalle défini (revalidate). La première requête après expiration sert le stale content pendant que le serveur regénère la page. Les requêtes suivantes reçoivent la version fraîche.
C'est du SSG avec un mécanisme de revalidation. Le TTFB reste celui d'une page statique dans la majorité des cas.
Matrice de décision : quel rendering pour quel type de site
Il n'existe pas de réponse universelle. Mais il existe des patterns clairs selon trois axes : le volume de pages, la fréquence de mise à jour du contenu, et le degré de personnalisation.
| Critère | SSG | ISR | SSR |
|---|---|---|---|
| Volume de pages | < 5 000 | 5 000 - 100 000+ | Tout volume |
| Fréquence de MAJ | Faible (hebdo/mensuelle) | Moyenne (quotidienne) | Haute (temps réel) |
| Personnalisation | Aucune | Aucune (ou edge middleware) | Par requête |
| TTFB typique | 20-80ms | 20-80ms (cache hit) / 200-800ms (miss) | 200-1500ms |
| Coût infra | Minimal | Modéré | Élevé |
| Freshness indexée | Build time | revalidate interval | Temps réel |
Blogs, sites documentaires, landing pages (< 2 000 pages)
SSG sans hésitation. Le build prend quelques minutes au maximum. Chaque page est servie instantanément. Le contenu change rarement. Il n'y a aucune raison de payer du compute serveur pour des pages qui ne bougent pas.
Avec Next.js 14+ (App Router) :
// app/blog/[slug]/page.tsx
// Cette fonction indique à Next.js de pré-générer toutes les routes au build
export async function generateStaticParams() {
const posts = await getAllPosts(); // appel CMS ou filesystem
return posts.map((post) => ({
slug: post.slug,
}));
}
// Pas de revalidate = page 100% statique
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.contentHtml }} />
</article>
);
}
Le generateStaticParams remplace l'ancien getStaticPaths. À noter : si vous n'exportez pas cette fonction, Next.js en App Router génère les pages à la demande (on-demand SSG), ce qui se rapproche de l'ISR.
E-commerce, marketplaces (5 000 - 50 000+ pages)
C'est le terrain de jeu naturel de l'ISR. Le catalogue change quotidiennement (prix, stocks, nouveaux produits), mais pas à la seconde. Un revalidate de 3600 secondes (1 heure) couvre la majorité des cas.
Le scénario concret : MaisonDeco.fr, un e-commerce de mobilier avec 18 000 fiches produit et 1 200 pages catégorie. Avant la migration, le site tournait en SSR pur sur Next.js 13.
Problèmes constatés :
- TTFB moyen de 1.2s sur les fiches produit (API PIM lente + rendering React)
- Googlebot crawlait environ 4 500 pages/jour selon les logs serveur
- 35% des fiches produit n'étaient pas indexées dans Search Console (discovered - currently not indexed)
Après passage en ISR avec revalidate: 3600 :
- TTFB moyen de 65ms (cache hit CDN)
- Le crawl rate est monté à 11 000 pages/jour en 3 semaines
- Le taux d'indexation est passé de 65% à 89% en 6 semaines
L'explication est mécanique : Googlebot a un budget de crawl fini. Si chaque requête prend 1.2s au lieu de 65ms, il crawle mécaniquement moins de pages dans le même intervalle. Google a documenté ce lien entre vitesse de réponse et crawl dans sa documentation sur le crawl budget.
// app/produit/[slug]/page.tsx — Next.js App Router avec ISR
// On pré-génère les 500 produits les plus populaires au build
export async function generateStaticParams() {
const topProducts = await getTopProducts(500);
return topProducts.map((p) => ({ slug: p.slug }));
}
// Les autres seront générés on-demand au premier accès
// et mis en cache pour 1 heure
export const revalidate = 3600;
// Ou pour un contrôle plus fin avec on-demand revalidation :
// export const revalidate = false; // désactive le time-based
// puis utiliser revalidatePath('/produit/mon-slug') depuis un webhook
export default async function ProductPage({ params }: { params: { slug: string } }) {
const product = await getProduct(params.slug);
if (!product) {
notFound(); // retourne un vrai 404
}
const jsonLd = {
"@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"
}
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<h1>{product.name}</h1>
{/* ... */}
</>
);
}
Sites d'actualité, médias, contenu temps réel
SSR, mais avec une couche de cache intelligente. Un article d'actualité doit être indexable dans les minutes qui suivent sa publication. L'ISR avec un revalidate de 60 secondes pourrait convenir, mais le problème est le stale content : pendant cet intervalle, Googlebot pourrait recevoir une version obsolète ou pire, une page 404 qui a été remplacée par une redirection.
Pour les médias, le pattern recommandé est SSR + cache HTTP avec stale-while-revalidate au niveau CDN :
# Configuration Nginx en tant que reverse proxy cache devant le serveur Next.js/Nuxt
proxy_cache_path /var/cache/nginx/ssr levels=1:2
keys_zone=ssr_cache:100m
max_size=10g
inactive=24h
use_temp_path=off;
server {
listen 443 ssl http2;
server_name www.media-actu.fr;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_cache ssr_cache;
# Cache pendant 5 minutes, sert le stale pendant 1 heure
# pendant que le backend régénère
proxy_cache_valid 200 5m;
proxy_cache_use_stale updating error timeout http_500 http_502;
# Header pour le debug — visible dans Chrome DevTools
add_header X-Cache-Status $upstream_cache_status always;
# Bypass du cache pour les previews éditoriaux
proxy_cache_bypass $arg_nocache;
# Clé de cache basée sur l'URL (pas les cookies)
proxy_cache_key $scheme$request_method$host$request_uri;
}
# Endpoint pour purger le cache quand un article est mis à jour
location /purge {
allow 10.0.0.0/8; # réseau interne uniquement
deny all;
proxy_cache_purge ssr_cache $scheme$request_method$host$arg_url;
}
}
Ce pattern donne le meilleur des deux mondes : le contenu est toujours frais (généré dynamiquement), le TTFB est celui du cache (< 100ms) dans 95% des cas, et vous gardez le contrôle sur la purge via un webhook depuis votre CMS.
Les pièges SEO spécifiques à chaque mode
ISR : le problème du stale-while-revalidate pour Googlebot
Quand ISR sert une page stale pendant la régénération, le contenu que Googlebot reçoit peut être obsolète. Dans la majorité des cas, c'est anodin (un prix qui a changé il y a 30 minutes). Mais il y a des cas critiques :
- Une page produit qui a été supprimée : Googlebot reçoit un 200 avec le contenu obsolète au lieu d'un 404 ou d'une 301
- Un canonical qui a changé : l'ancien canonical est servi pendant l'intervalle de revalidation
- Une meta noindex ajoutée en urgence : elle ne sera effective qu'après régénération
La solution : combiner le time-based revalidation avec l'on-demand revalidation pour les opérations critiques.
// app/api/revalidate/route.ts — webhook appelé depuis le PIM/CMS
import { revalidatePath } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const secret = request.headers.get('x-revalidate-secret');
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ error: 'Invalid secret' }, { status: 401 });
}
const body = await request.json();
// Revalidation immédiate quand un produit est supprimé ou modifié
if (body.event === 'product.deleted' || body.event === 'product.updated') {
revalidatePath(`/produit/${body.slug}`);
// Revalider aussi la catégorie parente
if (body.categorySlug) {
revalidatePath(`/categorie/${body.categorySlug}`);
}
}
// Revalidation de masse pour les mises à jour de prix
if (body.event === 'prices.bulk_update') {
for (const slug of body.slugs) {
revalidatePath(`/produit/${slug}`);
}
}
return NextResponse.json({ revalidated: true });
}
SSG : le problème des builds qui échouent silencieusement
Un build SSG qui échoue partiellement est un scénario courant et dangereux. Votre CMS renvoie une erreur 500 pour 200 produits sur 18 000 pendant le build. Next.js les skip silencieusement (ou génère des pages d'erreur selon votre config). Le déploiement passe. Vous avez 200 pages en moins sans le savoir.
La parade : vérifier le nombre de pages générées dans votre pipeline CI/CD et alerter en cas de déviation significative.
#!/bin/bash
# Script de validation post-build dans votre pipeline CI
BUILD_DIR=".next/server/app"
EXPECTED_MIN_PAGES=17500 # seuil d'alerte
# Compter les fichiers .html générés (SSG)
GENERATED=$(find $BUILD_DIR -name "*.html" | wc -l)
echo "Pages générées : $GENERATED (minimum attendu : $EXPECTED_MIN_PAGES)"
if [ "$GENERATED" -lt "$EXPECTED_MIN_PAGES" ]; then
echo "ALERTE : le build a généré moins de pages que prévu."
echo "Delta : $(($EXPECTED_MIN_PAGES - $GENERATED)) pages manquantes."
# Envoyer une alerte Slack/PagerDuty
curl -X POST "$SLACK_WEBHOOK" \
-H 'Content-Type: application/json' \
-d "{\"text\":\"⚠️ Build SEO: seulement $GENERATED pages générées (attendu: $EXPECTED_MIN_PAGES+)\"}"
exit 1 # bloquer le déploiement
fi
Un outil de monitoring comme SEOGard détecte ce type de régression en comparant le nombre de pages indexables entre deux crawls, mais le filet de sécurité au niveau du build reste la première ligne de défense.
SSR : le TTFB qui se dégrade sous charge
Le piège classique du SSR : tout fonctionne en staging avec 10 requêtes/seconde, puis le site passe en production avec 500 requêtes simultanées pendant le Black Friday. Le TTFB monte à 3-4 secondes. Googlebot, qui crawle en continu, commence à recevoir des timeouts. Le crawl rate s'effondre.
Vérifiez votre TTFB réel en conditions de charge avec les rapports de temps de réponse dans la Search Console (Paramètres > Statistiques sur l'exploration). Si vous voyez des pics réguliers au-dessus de 1.5s, c'est un signal d'alarme.
Dans Chrome DevTools, l'onglet Network vous donne le détail pour une requête unitaire, mais pour une vue d'ensemble en production, corréllez les données de la Search Console avec vos logs serveur. Screaming Frog, configuré pour crawler à une vitesse réaliste (5 URLs/seconde), révèle aussi les pages dont le TTFB diverge significativement de la moyenne.
Nuxt 3 : les spécificités du rendering hybride
Nuxt 3 a une approche différente de Next.js pour le rendering hybride. Au lieu de configurer le rendering page par page dans le code du composant, tout se fait dans nuxt.config.ts via les routeRules.
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// Pages statiques : pré-rendues au build, jamais régénérées
'/a-propos': { prerender: true },
'/mentions-legales': { prerender: true },
// Blog : ISR avec revalidation toutes les heures
'/blog/**': { isr: 3600 },
// Fiches produit : ISR avec revalidation toutes les 30 minutes
'/produit/**': { isr: 1800 },
// Pages catégorie : ISR plus agressif (10 minutes)
// car les filtres et le tri changent fréquemment
'/categorie/**': { isr: 600 },
// Dashboard utilisateur : SSR pur, pas de cache
// (contenu personnalisé, pas indexé de toute façon)
'/dashboard/**': { ssr: true, headers: { 'X-Robots-Tag': 'noindex' } },
// API : pas de pre-rendering
'/api/**': { cors: true, headers: { 'cache-control': 'no-store' } },
},
});
L'avantage de cette approche centralisée : vous avez une vue d'ensemble de la stratégie de rendering de tout le site dans un seul fichier. L'inconvénient : un changement de stratégie sur un pattern d'URL nécessite un redéploiement, là où l'on-demand revalidation de Next.js peut être déclenchée à chaud via API.
Pour les sites Nuxt en production, la documentation officielle détaille les options de routeRules sur nuxt.com/docs/guide/concepts/rendering.
L'hydration mismatch : le piège commun à SSR et ISR
Quel que soit le mode choisi, si votre composant produit un HTML différent côté serveur et côté client, vous avez un hydration mismatch. React ou Vue remplacera le HTML serveur par le HTML client. Googlebot, qui exécute JavaScript, pourrait voir le contenu post-hydration — ou pas, selon le timing.
Le scénario le plus fréquent : un composant qui utilise Date.now(), Math.random(), ou qui lit window.innerWidth pour conditionner l'affichage de contenu SEO-critique (titres, descriptions produit, liens internes).
Ce sujet mérite un article entier — et il existe : Hydration mismatch : le bug invisible qui tue votre SEO couvre les patterns de détection et de correction en détail.
Comment vérifier ce que Googlebot voit réellement
La théorie c'est bien. Mais la seule chose qui compte, c'est le HTML que Googlebot indexe. Voici le workflow de vérification :
1. Inspection d'URL dans la Search Console : l'outil "Tester l'URL en direct" exécute le rendering complet (JavaScript inclus) et vous montre le HTML rendu. Comparez-le avec le HTML brut que votre serveur retourne. S'ils sont identiques, votre SSR/SSG fonctionne. S'ils divergent, vous avez un problème d'hydration ou de client-side rendering résiduel.
2. curl pour voir le HTML brut :
# Voir exactement ce que le serveur retourne (sans exécution JS)
curl -s -H "User-Agent: Googlebot" https://www.votresite.fr/produit/canape-velours | head -100
# Vérifier le TTFB
curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" \
https://www.votresite.fr/produit/canape-velours
3. Screaming Frog en mode JavaScript rendering : configurez le rendu JavaScript dans Configuration > Spider > Rendering, puis comparez la colonne "Title 1" (HTML brut) avec "Rendered Title 1" (après exécution JS). Toute divergence est suspecte.
Si votre SPA ne renvoie aucun contenu côté serveur, Googlebot voit littéralement une page blanche — un problème documenté en détail dans Pourquoi Google voit une page blanche sur votre SPA.
La grille de décision finale
Voici l'arbre de décision simplifié :
Votre contenu change-t-il plus d'une fois par heure ?
→ Oui : SSR + cache CDN avec stale-while-revalidate
→ Non : continuez ci-dessous
Avez-vous plus de 5 000 pages ? → Non : SSG pur. Le build est rapide, le résultat est optimal. → Oui : continuez ci-dessous
Les mises à jour doivent-elles être visibles en moins de 5 minutes ?
→ Oui : ISR avec revalidate court (60-300s) + on-demand revalidation via webhook
→ Non : ISR avec revalidate long (1800-3600s)
Le contenu est-il personnalisé par utilisateur ? → Oui pour le contenu SEO-critique (title, H1, body text) : SSR obligatoire → Oui uniquement pour des éléments non-SEO (panier, recommandations) : ISR/SSG + hydration client pour les parties dynamiques
Un point souvent négligé : vous pouvez — et devriez — mixer les modes au sein d'un même site. Next.js et Nuxt supportent parfaitement le rendering hybride. Vos pages /blog/* en SSG, vos fiches produit en ISR, votre page d'accueil en SSR avec cache court. Chaque section du site a ses propres contraintes de fraîcheur et de volume.
Le monitoring post-déploiement : là où la plupart des équipes échouent
Choisir le bon mode de rendering est la première étape. S'assurer qu'il fonctionne comme prévu en production en est une autre. Les régressions de rendering sont silencieuses : un SSR qui tombe en fallback CSR à cause d'une erreur API, un ISR dont le cache ne se régénère plus suite à un changement d'infra, un build SSG qui perd 500 pages à cause d'un timeout CMS.
Ce qu'il faut monitorer en continu :
- Le TTFB par template de page (pas la moyenne globale — une dégradation sur
/produit/*peut être masquée par un/blog/*rapide) - La présence des balises meta critiques dans le HTML servi (title, description, canonical, robots)
- Le diff du nombre de pages entre deux crawls successifs
- Les headers HTTP (le
X-Nextjs-Cachede Next.js indique HIT, MISS, ou STALE — un taux de MISS anormalement élevé signale un problème de revalidation)
SEOGard automatise cette surveillance et alerte en moins de 24h quand une régression de rendering est détectée — exactement le type de signal qu'un humain ne capte qu'en vérifiant manuellement la Search Console, souvent trop tard.
Le mode de rendering est une décision d'architecture, mais c'est aussi une décision SEO. Prenez-la en fonction de vos données réelles (volume de pages, fréquence de mise à jour, budget infra), pas en fonction de ce qui est à la mode sur Twitter. Et surtout, vérifiez que ce que vous avez déployé produit réellement le HTML que vous attendez — en production, sous charge, sur la durée.