Un e-commerce de 18 000 fiches produit migre d'un monolithe PHP vers une SPA React. Trois mois plus tard, 40 % des pages ont disparu de l'index Google. Le contenu est là, le HTML source ne l'est pas — Googlebot n'a jamais exécuté le JavaScript qui l'injecte. Ce scénario n'est pas théorique. Il se produit chaque semaine sur des sites à fort enjeu.
Le pipeline de crawl-render-index de Google : deux passes, pas une
La plupart des documentations simplifient le fonctionnement de Googlebot en un unique passage. La réalité est un pipeline en deux phases distinctes, avec une file d'attente entre les deux.
Première passe : le crawl HTML brut
Googlebot télécharge le HTML retourné par le serveur. Il extrait immédiatement les liens (<a href>), les balises <title>, <meta>, les canonical, le hreflang, les données structurées présentes dans le HTML statique. Tout ce qui est lisible dans le code source sans exécution JavaScript est traité à ce stade.
Cette première passe alimente directement la découverte d'URLs et la file de crawl. Si vos liens de navigation ou de pagination sont générés côté client, ils ne sont pas découverts ici — ce qui retarde considérablement l'exploration de pans entiers de votre site.
Deuxième passe : le Web Rendering Service (WRS)
Le WRS est un headless Chromium maintenu par l'équipe Google. Il exécute le JavaScript de la page, attend un certain temps que le DOM se stabilise, puis capture un snapshot du DOM final. Ce snapshot est ce qui est réellement indexé.
Le problème : la file d'attente entre crawl et rendering peut prendre de quelques secondes à plusieurs jours. Google le dit explicitement dans sa documentation sur le rendering JavaScript : "There may be a delay between when content is crawled and when it is rendered." Sur un site de 15 000+ pages avec du contenu qui change fréquemment (prix, stock, promotions), ce délai est un risque business direct.
Pour comprendre l'ensemble des étapes qui peuvent bloquer l'indexation de vos pages avant même le rendering, consultez les gates d'infrastructure derrière le crawl, le render et l'index.
Ce que cela signifie concrètement
Si votre page retourne ce HTML au serveur :
<!DOCTYPE html>
<html lang="fr">
<head>
<title>Mon App</title>
<meta name="description" content="">
</head>
<body>
<div id="root"></div>
<script src="/static/js/bundle.a4f8e.js"></script>
</body>
</html>
Lors de la première passe, Google voit : un title générique "Mon App", une meta description vide, aucun contenu, aucun lien interne. C'est exactement pourquoi Google voit une page blanche sur votre SPA. Même si le WRS finit par exécuter le bundle et récupérer le contenu, la première impression est désastreuse — et cette première impression conditionne la priorité de rendering dans la file d'attente.
Ce que le WRS de Google peut réellement exécuter
Le WRS exécute un Chromium "evergreen" — toujours à jour avec la dernière version stable de Chrome. Depuis 2019, ce n'est plus le vieux Chrome 41 headless qui cassait la moitié des frameworks. En théorie, tout ce que Chrome stable supporte, le WRS le supporte.
Ce qui fonctionne
- ES2024+ : async/await, optional chaining, nullish coalescing, top-level await — tout passe.
- Frameworks SPA : React, Vue, Angular, Svelte — le WRS exécute le JavaScript et capture le DOM résultant.
- Dynamic imports :
import()fonctionne. Le WRS résout les chunks chargés dynamiquement. - Canvas / WebGL : exécutés, mais le contenu visuel n'est évidemment pas "lu" sémantiquement.
- Shadow DOM : le WRS peut accéder au contenu du Shadow DOM (light DOM et shadow DOM sont rendus).
Ce qui échoue ou pose problème
Les APIs qui nécessitent une interaction utilisateur ne se déclenchent jamais. Le WRS ne scrolle pas, ne clique pas, ne survole pas. C'est le point le plus sous-estimé du JavaScript SEO.
Concrètement, ce pattern très courant en e-commerce est un piège :
// ❌ Ce contenu ne sera JAMAIS vu par Googlebot
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadProductReviews(entry.target.dataset.productId);
}
});
});
document.querySelectorAll('.reviews-placeholder').forEach(el => {
observer.observe(el);
});
Les avis produit chargés via IntersectionObserver sur un élément situé sous la ligne de flottaison ne seront jamais rendus. Le WRS charge la page avec un viewport de 411x731 pixels (Nexus 5X) et ne scrolle pas. Tout élément dont le chargement dépend du scroll reste invisible.
Les requêtes nécessitant une authentification échouent silencieusement. Le WRS ne gère pas les cookies de session, les tokens OAuth, ni le localStorage. Si votre contenu dépend d'un état connecté, il n'existe pas pour Google.
Les timeouts longs sont coupés. Le WRS attend que le réseau se calme (pas de requêtes en vol) et que le DOM soit stable. La documentation Google ne donne pas de durée exacte, mais les tests empiriques de la communauté (Martin Splitt l'a confirmé dans plusieurs Google Search Central live) montrent un timeout autour de 5 secondes après le dernier changement réseau. Si votre API met 8 secondes à répondre en P95, le WRS n'attendra pas.
Les WebSockets et Server-Sent Events : le WRS ne maintient pas de connexion long-polling. Le contenu qui arrive via WebSocket après le chargement initial n'est pas capturé.
Les erreurs JavaScript qui tuent silencieusement l'indexation
Un TypeError: Cannot read property 'map' of undefined qui se produit avant le rendu du contenu principal signifie zéro contenu indexé. Le WRS n'a pas de mécanisme de retry pour les erreurs JS — si le script casse, le DOM reste dans l'état où il était au moment du crash.
Diagnostic avec l'URL Inspection de Search Console
L'outil le plus fiable reste le test en direct de l'URL Inspection dans Search Console. Il vous montre exactement ce que le WRS voit : le HTML rendu, la capture d'écran, et — surtout — les erreurs console.
Pour aller plus loin dans l'automatisation de ce diagnostic, vous pouvez utiliser l'URL Inspection API pour monitorer vos pages à grande échelle.
Les erreurs les plus fréquentes et les plus sournoises
1. Les variables d'environnement manquantes en production
// ❌ NEXT_PUBLIC_API_URL non défini en production → crash au rendering
const API_BASE = process.env.NEXT_PUBLIC_API_URL; // undefined
async function getProducts(categoryId) {
// TypeError: Failed to construct 'URL': Invalid URL
const res = await fetch(`${API_BASE}/api/products?cat=${categoryId}`);
const data = await res.json();
return data;
}
Ce bug ne se manifeste pas en développement (la variable est dans .env.local), pas en staging (elle est dans le CI), mais casse le rendering en production si le déploiement oublie la variable. Le WRS reçoit une erreur, le contenu ne se rend jamais.
2. Le blocage de ressources critiques via robots.txt
Si votre robots.txt bloque les fichiers JS ou CSS, le WRS ne peut pas exécuter la page. Vérifiez votre configuration robots.txt pour vous assurer que les ressources nécessaires au rendering sont accessibles.
# ❌ Ceci empêche le rendering complet de vos pages
User-agent: Googlebot
Disallow: /static/js/
Disallow: /_next/
# ✅ Autorisez les ressources nécessaires au rendering
User-agent: Googlebot
Allow: /static/js/
Allow: /_next/
Disallow: /api/internal/
3. Les erreurs CORS sur les appels API
Le WRS exécute les requêtes depuis les IP de Google. Si votre CDN ou WAF bloque ces requêtes (rate limiting, géoblocage, vérification de User-Agent mal configurée), les appels fetch() échouent avec des erreurs CORS ou des 403. Le DOM résultant est un squelette vide.
Testez avec curl en simulant le User-Agent de Googlebot :
curl -H "User-Agent: Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.175 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" \
-I "https://votre-domaine.fr/api/products?cat=chaussures"
Si cette commande retourne un 403 ou un 429, le WRS obtient la même chose.
Scénario réel : migration SPA vers SSR d'un catalogue de 15 000 pages
Un site e-commerce spécialisé en pièces automobiles — 15 200 fiches produit, 340 pages catégorie, 80 pages marque — tourne sur une SPA Vue.js avec vue-router en mode history. Le HTML source de chaque page est identique : un <div id="app"></div> et un bundle JS de 420 Ko (gzippé).
L'état initial
- Pages indexées (Search Console) : 4 800 sur 15 620 URLs soumises via sitemap
- Couverture : 30,7 % d'indexation
- Erreurs : 2 100 pages "Discovered – currently not indexed", 6 400 pages "Crawled – currently not indexed"
- Trafic organique : 12 000 sessions/mois sur un potentiel estimé à 45 000+
Le statut "Crawled – currently not indexed" est le signal le plus révélateur. Google a crawlé le HTML, trouvé une page vide (ou quasi-vide), et décidé de ne pas indexer. Le WRS n'a jamais été déclenché pour ces pages, ou il a échoué. Pour comprendre ce mécanisme en détail, voir pourquoi Google n'indexe pas vos pages.
La migration vers Nuxt 3 en mode SSR
L'équipe migre vers Nuxt 3 avec ssr: true. Chaque page est rendue côté serveur : le HTML retourné contient directement les données produit, les balises meta, les liens internes, les données structurées JSON-LD.
La config Nuxt :
// nuxt.config.ts
export default defineNuxtConfig({
ssr: true,
routeRules: {
// Pages produit : SSR + cache CDN 1h
'/produit/**': {
swr: 3600,
headers: { 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400' }
},
// Pages catégorie : SSR + cache CDN 30min
'/categorie/**': {
swr: 1800
},
// Pages statiques : ISR 24h
'/a-propos': { prerender: true },
'/contact': { prerender: true }
},
nitro: {
prerender: {
// Pré-rendre le sitemap et les pages critiques au build
routes: ['/sitemap.xml']
}
}
})
Pour un comparatif détaillé entre SSR et CSR et les implications SEO de chaque approche, consultez SSR vs CSR : impact réel sur le SEO.
Les résultats sur 8 semaines
- Semaine 2 : les pages catégorie commencent à être réindexées avec le contenu complet. Le taux de "Crawled – currently not indexed" chute de 6 400 à 3 100.
- Semaine 4 : 11 800 pages indexées (75,5 % de couverture). Le trafic organique passe à 22 000 sessions/mois.
- Semaine 8 : 14 600 pages indexées (93,5 %). Trafic à 38 000 sessions/mois. Les pages restantes non indexées sont principalement des fiches produit en rupture de stock avec peu de contenu différenciant — un problème de qualité, pas de rendering.
Le point clé : la migration SSR n'a pas simplement "accéléré" l'indexation. Elle a rendu possible l'indexation de pages que le WRS n'atteignait jamais. La file de rendering étant une ressource partagée entre tous les sites du web, les pages à faible PageRank d'un e-commerce de niche passent tout simplement à la trappe.
Stratégies de rendering pour le SEO : SSR, SSG, ISR et prerendering
Il n'existe pas de solution unique. Le choix dépend du nombre de pages, de la fréquence de mise à jour du contenu, et de vos contraintes d'infrastructure.
SSR (Server-Side Rendering)
Le serveur exécute le JavaScript à chaque requête et retourne un HTML complet. Adapté aux pages dont le contenu change fréquemment (prix, stock, avis). Le coût est la charge serveur — chaque requête Googlebot consomme du CPU.
Pour les sites à fort trafic Googlebot (100 000+ crawls/jour), mettez un cache HTTP devant le SSR. Un stale-while-revalidate sur Cloudflare ou Fastly permet de servir un HTML pré-rendu à Googlebot tout en gardant le contenu frais pour les utilisateurs.
SSG (Static Site Generation)
Pré-génération de toutes les pages au moment du build. Idéal pour les sites de moins de 5 000 pages avec un contenu qui change rarement (documentation, blog). Au-delà, les temps de build deviennent prohibitifs : un SSG de 15 000 pages avec des appels API pour chaque page prend facilement 20-30 minutes.
ISR (Incremental Static Regeneration)
Le compromis de Next.js et Nuxt 3. Les pages sont générées statiquement, puis régénérées en arrière-plan après expiration d'un TTL. Attention au cold start : la première visite après le TTL retourne la version stale pendant que la nouvelle se génère. Si Googlebot est ce premier visiteur, il indexe la version périmée — acceptable dans la plupart des cas, mais à surveiller pour les contenus sensibles au prix.
Dynamic rendering / prerendering sélectif
Servir un HTML pré-rendu spécifiquement aux bots, tout en gardant la SPA pour les utilisateurs. Google ne considère pas cette approche comme du cloaking tant que le contenu est identique. C'est une solution de transition, pas une architecture cible.
Pour une analyse complète de quand cette approche se justifie, voir prerendering : quand et comment l'utiliser pour le SEO.
Auditer le rendering JS de votre site : workflow concret
Voici le workflow qu'un Lead SEO technique devrait suivre pour identifier les problèmes de rendering JavaScript.
Étape 1 : comparer HTML source et HTML rendu à grande échelle
Screaming Frog permet de crawler en deux modes : sans exécution JavaScript (mode par défaut) et avec exécution JavaScript (Configuration > Spider > Rendering > JavaScript). Lancez les deux crawls sur le même périmètre et comparez :
- Les
<title>et<meta description>: identiques entre les deux ? Si le title est "Mon App" sans JS et "Chaussures running Nike Air Max – MonSite" avec JS, vous avez un problème. - Le nombre de liens internes découverts : si le crawl JS découvre 3x plus d'URLs, votre maillage interne est invisible à la première passe de Googlebot.
- Le nombre de mots sur la page : une page à 0 mots sans JS et 800 avec JS signifie que tout le contenu est client-rendered.
Étape 2 : vérifier dans Chrome DevTools ce que Googlebot voit
Ouvrez DevTools, onglet Network, et désactivez JavaScript (Settings > Debugger > Disable JavaScript). Rechargez la page. Ce que vous voyez est une approximation de ce que Google voit en première passe.
Pour la deuxième passe (WRS), le test en direct de Search Console reste la référence, car il utilise exactement le même Chromium et les mêmes contraintes réseau.
Étape 3 : monitorer les régressions en continu
Un déploiement qui casse le SSR passe facilement inaperçu pendant des semaines. Le site fonctionne parfaitement pour les utilisateurs (le JS côté client prend le relais via l'hydratation), mais Googlebot reçoit un HTML vide ou cassé.
C'est exactement le type de régression qu'un outil de monitoring comme SEOGard détecte automatiquement : un changement dans le HTML servi (meta disparues, contenu passant de 800 mots à 0, canonical modifié) déclenche une alerte immédiate, avant que la Search Console ne reflète la perte d'indexation 2-3 semaines plus tard.
Étape 4 : surveiller le rapport de couverture Search Console
Segmentez par type de page (produit, catégorie, article) via les sitemaps dédiés. Un pic soudain de "Crawled – currently not indexed" sur un segment est le signal d'une régression de rendering. Assurez-vous que votre sitemap est correctement configuré pour permettre ce type d'analyse segmentée.
Les pièges spécifiques aux frameworks modernes
Next.js App Router et les Server Components
Avec l'App Router de Next.js 13+, les React Server Components sont rendus côté serveur par défaut. Le HTML envoyé au client (et à Googlebot) contient le contenu. Mais dès que vous ajoutez "use client" en haut d'un composant, celui-ci est rendu côté client. Si ce composant contient du contenu SEO-critical (description produit, avis, prix), vous venez de le rendre invisible à la première passe.
// ✅ Server Component (défaut) — contenu dans le HTML source
async function ProductDescription({ productId }: { productId: string }) {
const product = await getProduct(productId); // exécuté côté serveur
return (
<section>
<h1>{product.name}</h1>
<p>{product.description}</p>
<span className="price">{product.price} €</span>
</section>
);
}
// ❌ Client Component — contenu invisible en première passe
"use client";
function ProductReviews({ productId }: { productId: string }) {
const [reviews, setReviews] = useState([]);
useEffect(() => {
fetch(`/api/reviews/${productId}`)
.then(r => r.json())
.then(setReviews);
}, [productId]);
return (
<section>
{reviews.map(r => <ReviewCard key={r.id} review={r} />)}
</section>
);
}
La règle : tout contenu qui influence le ranking (texte principal, prix, avis, données structurées) doit être dans un Server Component ou pré-rendu côté serveur. Les Client Components sont réservés à l'interactivité pure : boutons "Ajouter au panier", filtres, modales.
Nuxt 3 et l'hydratation lazy
Nuxt 3 propose <LazyComponent> pour différer l'hydratation. Attention : si le composant est <Lazy> ET que le contenu n'est pas dans le HTML initial (chargé via un useFetch client-side uniquement), ce contenu disparaît du HTML servi.
SvelteKit et le load côté serveur
SvelteKit gère bien le SSR par défaut. Le piège se situe dans les fonctions load : une fonction load dans +page.ts s'exécute côté serveur ET client. Une fonction load dans +page.server.ts s'exécute uniquement côté serveur. Si vous appelez une API interne qui n'est accessible que depuis le réseau local, utilisez +page.server.ts — sinon le WRS de Google essaiera d'appeler cette API depuis ses propres serveurs et obtiendra un timeout.
Données structurées et JavaScript : un double risque
Les données structurées JSON-LD injectées via JavaScript sont traitées par le WRS. Google le confirme. Mais elles sont soumises aux mêmes contraintes de délai et de fiabilité que le reste du contenu JS.
Si votre Product schema est généré dynamiquement et que le WRS échoue (timeout API, erreur JS), vous perdez non seulement le contenu mais aussi les rich results. Double peine.
La recommandation : injectez le JSON-LD dans le HTML source côté serveur. C'est le seul moyen de garantir que les données structurées sont toujours présentes, indépendamment du rendering JS. Consultez le guide pratique JSON-LD pour les données structurées et le guide Product schema pour l'e-commerce pour les implémentations détaillées.
<!-- ✅ JSON-LD dans le HTML source, généré côté serveur -->
<head>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Plaquettes de frein Brembo P06077",
"description": "Plaquettes de frein avant pour BMW Série 3 (F30/F31)",
"sku": "BRE-P06077",
"offers": {
"@type": "Offer",
"price": "42.90",
"priceCurrency": "EUR",
"availability": "https://schema.org/InStock"
}
}
</script>
</head>
Le rendering JS n'est pas un problème Google — c'est un problème d'architecture
Google peut rendre le JavaScript. La question n'a jamais été "est-ce que ça marche ?" mais "est-ce que ça marche de manière fiable, rapide et prévisible pour l'ensemble de vos pages ?". La réponse est non pour tout site dépassant quelques centaines de pages en CSR pur.
La stratégie gagnante est simple dans son principe : le HTML que votre serveur envoie doit contenir tout ce dont Google a besoin pour indexer correctement la page — title, meta description, canonical, contenu textuel principal, liens internes, données structurées. Le JavaScript ne doit ajouter que de l'interactivité, pas du contenu indexable.
Pour les sites existants en SPA qui ne peuvent pas migrer immédiatement vers du SSR, un monitoring continu du HTML source servi par le serveur — comparé au contenu attendu — est la seule manière de détecter les régressions avant qu'elles ne se traduisent en perte de trafic. SEOGard automatise exactement cette surveillance, en alertant dès qu'une page critique perd son contenu serveur ou ses balises SEO.