Un site e-commerce de 25 000 produits migre vers un framework JavaScript moderne. Trois mois après, 40% des pages produits disparaissent de l'index Google. Le trafic organique chute de 35%. Le problème n'est pas le contenu — c'est le rendering. Les grands acteurs du e-commerce comme Chewy, Harrods ou Under Armour gèrent des catalogues massifs en JavaScript sans jamais perdre une page. Voici comment ils font, code à l'appui.
Leçon 1 : le SSR hybride comme standard, pas comme option
La première leçon est aussi la plus fondamentale. Aucun des grands e-commerces analysés ne repose sur du client-side rendering (CSR) pur pour ses pages indexables. Tous utilisent une forme de server-side rendering (SSR) ou de static generation — mais jamais de manière uniforme sur l'ensemble du site.
Le pattern hybride de Chewy
Chewy gère un catalogue de plus de 100 000 produits pour animaux. Leur approche : SSR pour toutes les pages de listing (catégories, sous-catégories) et les pages produit, mais CSR pour les composants interactifs (avis clients avec filtres, comparateur de prix, panier).
En inspectant le source HTML retourné par leur serveur (via curl ou View Source, pas l'inspecteur Chrome qui affiche le DOM post-rendering), on constate que le contenu critique — titre produit, description, prix, disponibilité — est présent dans la réponse initiale.
# Vérifier le HTML servi par le serveur (avant exécution JS)
curl -s "https://www.chewy.com/dp/12345" | grep -E '<h1|<meta name="description"|itemprop="price"'
# Comparer avec le DOM rendu par le navigateur
# Dans Chrome DevTools > Console :
# document.querySelector('h1').textContent
# Puis comparer avec le source brut
Ce que cette approche révèle : le SSR n'a pas besoin d'être total. Le pattern gagnant consiste à pré-rendre côté serveur tout ce que Googlebot doit indexer, et à déléguer au client les éléments interactifs qui n'ont aucune valeur SEO (filtres dynamiques, état du panier, popups).
Pourquoi le dynamic rendering est un piège à long terme
Certains sites optent pour le dynamic rendering — servir du HTML pré-rendu à Googlebot et du CSR aux utilisateurs. Google a explicitement indiqué que cette approche est une "workaround" temporaire, pas une solution pérenne. Le risque : un delta croissant entre ce que Googlebot voit et ce que l'utilisateur voit, ce qui peut déclencher des problèmes de cloaking involontaire.
Under Armour illustre bien la bonne approche. Leur stack Next.js utilise getServerSideProps pour les pages produit critiques, garantissant que le HTML complet est généré à chaque requête :
// Pattern type Next.js pour une page produit e-commerce
// Le contenu SEO-critique est fetché côté serveur
export const getServerSideProps: GetServerSideProps = async (context) => {
const { slug } = context.params;
// Fetch depuis l'API catalogue — exécuté côté serveur uniquement
const product = await catalogApi.getProduct(slug as string);
if (!product) {
return { notFound: true }; // Retourne un vrai 404, pas une page vide
}
// Les avis sont chargés côté client — pas critiques pour l'indexation
return {
props: {
product: {
name: product.name,
description: product.description,
price: product.price,
availability: product.inStock ? 'InStock' : 'OutOfStock',
images: product.images,
// Structured data pré-calculée côté serveur
jsonLd: buildProductJsonLd(product),
},
},
};
};
Le trade-off à connaître : le SSR ajoute de la latence serveur (TTFB plus élevé) comparé au static generation (SSG). Pour un catalogue de 50 000 pages produits, l'ISR (Incremental Static Regeneration) de Next.js ou l'équivalent chez Nuxt offre un bon compromis — pages pré-générées avec revalidation périodique. Mais attention : l'ISR pose des problèmes de cache stale quand les prix ou la disponibilité changent fréquemment. Chewy semble résoudre ce point avec un TTL de revalidation court (quelques minutes) couplé à un webhook de purge de cache sur les mises à jour de stock.
Leçon 2 : la navigation facettée sans dilution de crawl budget
La navigation facettée est le cauchemar SEO des e-commerces JavaScript. Un catalogue de 5 000 produits avec 8 filtres combinables peut générer des millions d'URLs potentielles. Harrods, avec ses catégories luxe ultra-segmentées, gère ce problème de manière instructive.
Le pattern canonique + noindex sélectif
L'approche la plus robuste observée sur ces sites combine trois mécanismes :
- Canonical vers la page de catégorie mère pour les combinaisons de filtres non-stratégiques
- Pages indexables dédiées pour les combinaisons à fort volume de recherche (ex : "manteau femme laine")
- Blocage par robots.txt des patterns d'URL à paramètres combinés au-delà de 2 filtres
<!-- Page catégorie principale — indexable, canonique vers elle-même -->
<link rel="canonical" href="https://www.harrods.com/en-gb/shopping/women-coats" />
<!-- Filtre unique stratégique — indexable avec son propre canonical -->
<!-- URL : /en-gb/shopping/women-coats?material=wool -->
<link rel="canonical" href="https://www.harrods.com/en-gb/shopping/women-wool-coats" />
<!-- Note : l'URL à paramètre redirige 301 vers l'URL propre -->
<!-- Combinaison multi-filtres non stratégique — canonical vers le parent -->
<!-- URL : /en-gb/shopping/women-coats?material=wool&color=red&size=m -->
<link rel="canonical" href="https://www.harrods.com/en-gb/shopping/women-coats" />
<meta name="robots" content="noindex, follow" />
Le piège JavaScript ici : si le canonical et le meta robots sont injectés via JavaScript côté client, Googlebot peut ne pas les voir lors du premier crawl. La page est d'abord crawlée en HTML brut, ajoutée à la file de rendering, puis re-crawlée après exécution du JS. Pendant cet intervalle — qui peut durer des jours pour les sites à gros volume — la page est potentiellement indexée sans les bonnes directives.
La solution : toujours servir les balises canonical et meta robots dans la réponse HTTP initiale, côté serveur. Jamais côté client.
Gérer les paramètres en JavaScript sans polluer le crawl
Un problème récurrent sur les e-commerces SPA : le routage côté client modifie l'URL (via history.pushState) quand l'utilisateur applique un filtre, mais le serveur ne connaît pas ces routes. Si Googlebot découvre ces URLs dans le DOM ou via des liens internes, il tentera de les crawler — et recevra soit une 404, soit un shell HTML vide.
Ce point rejoint directement les problématiques de tracking parameters dans les liens internes qui polluent le crawl budget de manière similaire.
La solution observée chez Under Armour : les interactions de filtrage côté client ne modifient pas l'URL pour les combinaisons non indexables. Elles utilisent le state du composant React sans toucher à window.location. Seules les combinaisons stratégiques (qui ont une page SSR dédiée) génèrent un vrai lien <a href> vers une URL serveur.
Leçon 3 : les structured data injectées côté serveur, pas côté client
Le structured data (JSON-LD) des pages produit est un facteur d'éligibilité aux rich results Google — extraits enrichis avec prix, avis, disponibilité. Sur un e-commerce, l'impact CTR des rich results peut atteindre 20-30% sur les requêtes transactionnelles.
Le timing du JSON-LD compte
Google peut techniquement parser du JSON-LD injecté par JavaScript. C'est documenté. Mais dans la pratique, les grands e-commerces ne prennent pas ce risque. Chewy, Harrods, Under Armour — tous trois injectent le JSON-LD Product dans le HTML initial.
La raison est pragmatique : le rendering JavaScript par Googlebot passe par une file d'attente (le Web Rendering Service). Un site de 30 000 pages produit dont le structured data dépend du JS ajoute une incertitude systémique. Si le WRS est surchargé, si un script tiers timeout et bloque le rendering, si une dépendance CDN est lente — le JSON-LD n'est pas vu.
<!-- Pattern observé : JSON-LD injecté dans le <head> côté serveur -->
<head>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Wilderness Trail Mix Dry Dog Food",
"image": "https://img.chewy.com/product/12345.jpg",
"description": "Grain-free formula with real salmon...",
"sku": "CHW-12345",
"brand": {
"@type": "Brand",
"name": "Blue Buffalo"
},
"offers": {
"@type": "Offer",
"url": "https://www.chewy.com/dp/12345",
"priceCurrency": "USD",
"price": "52.99",
"availability": "https://schema.org/InStock",
"seller": {
"@type": "Organization",
"name": "Chewy"
}
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.7",
"reviewCount": "2841"
}
}
</script>
</head>
L'erreur classique du JSON-LD dynamique
Scénario vécu : un e-commerce mode de 15 000 SKUs utilise React pour rendre ses pages produit. Le JSON-LD est généré dans un composant React <Head> (via react-helmet ou next/head). Le problème : le prix affiché à l'utilisateur provient d'une API pricing appelée côté client après le montage du composant. Le HTML servi par le serveur contient donc un JSON-LD avec un prix placeholder (ou pire, null). Googlebot voit un structured data invalide et ne génère aucun rich result.
La solution : le prix doit être résolu côté serveur au moment du rendering. Si votre API pricing est lente (>200ms), mettez en cache le dernier prix connu et servez-le en SSR. La fraîcheur du prix dans le JSON-LD peut tolérer quelques minutes de décalage — Google ne crawle pas vos pages toutes les secondes.
Vérifiez la validité de vos structured data en production avec le Rich Results Test de Google en mode "URL" (pas "code"), car il exécute le JavaScript et montre ce que Googlebot voit réellement.
Leçon 4 : le lazy loading intelligent — ce qu'on charge, ce qu'on ne charge pas
Les grands e-commerces chargent des dizaines de scripts tiers : analytics, A/B testing, personnalisation, chat, retargeting. Chaque script alourdit le main thread et peut retarder le rendering du contenu critique. L'impact SEO est double : dégradation des Core Web Vitals (LCP, INP) et risque de timeout du WRS de Googlebot.
La stratégie d'Under Armour : priorisation agressive
En auditant le waterfall réseau d'Under Armour avec Chrome DevTools (onglet Performance), on observe un pattern clair :
- Critique (chargé synchrone) : HTML SSR + CSS critique inline + polices système
- Important (chargé en defer) : bundle JS principal (navigation, interactivité produit)
- Non-critique (chargé après interaction) : scripts tiers (analytics, chat, social proof)
<!-- Pattern de chargement priorisé -->
<head>
<!-- CSS critique inliné — pas de requête bloquante -->
<style>
/* CSS critique pour le above-the-fold : header, hero produit, prix */
.product-hero { display: grid; grid-template-columns: 1fr 1fr; }
.product-title { font-size: 1.5rem; font-weight: 700; }
.product-price { font-size: 1.25rem; color: #c41230; }
</style>
<!-- Preload des ressources critiques -->
<link rel="preload" href="/fonts/ua-brand.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/images/product/12345-hero.webp" as="image" />
<!-- Bundle JS principal — defer, jamais async pour garantir l'ordre -->
<script src="/js/main.bundle.js" defer></script>
</head>
<body>
<!-- Contenu SSR complet ici -->
<!-- Scripts tiers chargés après l'événement load -->
<script>
window.addEventListener('load', () => {
// Analytics — attend que la page soit interactive
const gtm = document.createElement('script');
gtm.src = 'https://www.googletagmanager.com/gtm.js?id=GTM-XXXXX';
document.head.appendChild(gtm);
// Chat widget — chargé uniquement après scroll ou délai
let chatLoaded = false;
const loadChat = () => {
if (chatLoaded) return;
chatLoaded = true;
const chat = document.createElement('script');
chat.src = 'https://widget.intercom.io/widget/xxxxx';
document.body.appendChild(chat);
};
window.addEventListener('scroll', loadChat, { once: true });
setTimeout(loadChat, 5000); // Fallback si pas de scroll
});
</script>
</body>
L'impact mesurable sur le crawl
Un cas concret pour illustrer : un e-commerce lifestyle de 18 000 pages (stack Nuxt.js) chargeait son widget d'avis Trustpilot de manière synchrone sur toutes les pages produit. Le script Trustpilot (180KB gzippé) ajoutait ~800ms au Time to Interactive. Après passage en lazy loading (chargement au scroll vers la section avis), le LCP moyen est passé de 3.2s à 2.1s, et le crawl rate dans Search Console a augmenté de 22% sur le mois suivant.
Pourquoi le crawl rate augmente-t-il ? Parce que Googlebot a un budget temps par page. Si le rendering est plus rapide, il peut crawler plus de pages dans le même intervalle. Pour un catalogue de 18 000 pages, cela signifie concrètement que les nouvelles pages produit et les mises à jour de prix sont détectées plus rapidement.
Leçon 5 : les liens internes en JavaScript — le piège invisible
C'est probablement le problème JavaScript SEO le plus sous-estimé. Googlebot suit les liens <a href="...">. Il ne suit pas les navigations déclenchées par onClick, window.location, ou router.push sans balise <a> sous-jacente.
Le pattern SPA qui casse la découverte de pages
Dans une application React ou Vue classique, la navigation entre pages utilise souvent un composant <Link> du framework (React Router, Vue Router, Next.js Link). Ces composants rendent bien un <a href> dans le DOM — en théorie. Mais certaines implémentations custom remplacent le <a> par un <div> ou un <button> avec un handler onClick qui appelle router.push().
Harrods utilise un pattern rigoureux : chaque élément de navigation cliquable dans le catalogue est un véritable <a href> avec une URL complète. Les menus de catégories, les liens produit dans les listings, les breadcrumbs — tout est un lien HTML natif. Le JavaScript de leur SPA intercepte le clic pour faire une navigation client-side fluide, mais le <a href> est toujours présent dans le HTML initial pour Googlebot.
Vérifiez ce point sur votre site :
# Screaming Frog : crawl en mode "JavaScript rendering"
# Config > Spider > Rendering > JavaScript
# Comparer les liens découverts avec le crawl HTML-only
# Alternative CLI avec puppeteer pour extraire les liens post-rendering
node -e "
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://votre-ecommerce.com/category/shoes', {
waitUntil: 'networkidle0'
});
// Extraire tous les liens <a> avec href
const links = await page.evaluate(() =>
Array.from(document.querySelectorAll('a[href]'))
.map(a => ({ text: a.textContent.trim(), href: a.href }))
.filter(l => l.href.includes('/product/'))
);
console.log(JSON.stringify(links, null, 2));
console.log('Total product links found:', links.length);
await browser.close();
})();
"
Le cas des mega-menus JavaScript
Les mega-menus e-commerce contiennent souvent des centaines de liens de catégories. Si ce menu est rendu uniquement côté client (un composant React qui fetch les catégories via API au clic), Googlebot ne verra aucun de ces liens. L'arborescence entière du site devient invisible.
La solution adoptée par les grands sites : le mega-menu est rendu dans le HTML initial côté serveur. Même s'il est visuellement masqué (CSS display:none jusqu'au hover), les liens sont dans le DOM et accessibles au crawl. Google a confirmé qu'il suit les liens dans le HTML même s'ils ne sont pas visibles à l'écran.
Ce point est particulièrement critique pour les sites qui dépendent d'une architecture de SEO programmatique sémantique où la structure de liens internes est le principal vecteur de distribution de l'autorité.
La méthodologie d'audit JavaScript SEO en 5 étapes
Plutôt qu'une simple checklist, voici la séquence d'audit que ces leçons impliquent pour votre propre e-commerce :
Étape 1 : comparer HTML brut vs DOM rendu
Pour chaque type de page template (accueil, catégorie, produit, recherche interne), récupérez le HTML brut (curl ou wget) et le DOM rendu (Puppeteer, ou l'onglet "Rendered HTML" de Screaming Frog). Comparez :
- Le
<title>est-il identique ? - Le
<link rel="canonical">est-il présent dans les deux ? - Le contenu textuel principal (H1, description produit) est-il dans le HTML brut ?
- Le JSON-LD est-il complet dans le HTML brut ?
Si une de ces réponses est "non", vous avez un problème de rendering critique.
Étape 2 : auditer le budget crawl consommé par les URLs parasites
Dans Search Console > Paramètres > Statistiques d'exploration, identifiez les URLs les plus crawlées. Si des URLs facettées à paramètres multiples consomment une part significative du budget crawl, vos directives canonical/noindex ne sont pas efficaces — probablement parce qu'elles sont injectées côté client.
Étape 3 : tester le rendering Googlebot réel
L'outil "Inspection d'URL" de Search Console (puis "Tester l'URL en direct") montre le DOM tel que Googlebot le voit après rendering. Testez systématiquement un échantillon de chaque type de page. Attention : cet outil utilise la version la plus récente du WRS, qui peut différer de ce que le crawler normal voit en conditions réelles (timeout, ressources bloquées).
Étape 4 : monitorer les régressions en continu
Un déploiement front-end qui change le composant <Head> peut silencieusement supprimer les canonicals de 10 000 pages produit. Un changement de version du framework peut modifier le comportement SSR. Ces régressions sont invisibles dans les tests manuels — elles n'apparaissent qu'à l'échelle.
Un outil de monitoring comme Seogard détecte ce type de régression automatiquement en comparant le HTML servi avant et après chaque déploiement, alertant dès qu'une meta disparaît ou qu'un pattern de rendering change.
Étape 5 : mesurer l'impact SEO réel
Corrélation n'est pas causalité, mais suivez ces métriques avant/après vos corrections JavaScript :
- Taux de pages indexées : Search Console > Pages > comparaison sur 30 jours
- Crawl rate : Search Console > Paramètres > Statistiques d'exploration
- Rich results éligibles : rapport "Améliorations" dans Search Console
- LCP/INP : rapport Core Web Vitals (données terrain CrUX)
Le facteur émergent : les bots IA et le JavaScript
Un angle que les analyses classiques de JavaScript SEO ignorent : les crawlers IA (GPTBot, ClaudeBot, PerplexityBot) ne font pas de rendering JavaScript. Ils fonctionnent comme un curl — ils lisent le HTML brut. Si votre contenu produit est uniquement accessible après exécution JavaScript, il est invisible pour les moteurs de réponse IA.
Cette réalité renforce l'argument du SSR. L'activité de crawl d'OpenAI a triplé récemment, et Google encourage les développeurs à construire pour les agents IA. Un e-commerce dont le contenu est accessible en HTML brut se positionne à la fois pour le SEO classique et pour la visibilité dans les réponses IA.
Les cinq leçons convergent vers un principe unique : le JavaScript est un outil de rendu d'interface, pas un outil de publication de contenu. Tout ce que Googlebot (et désormais les bots IA) doit voir — texte, liens, structured data, directives d'indexation — doit être dans le HTML initial. Le JS gère l'interactivité, pas la découvrabilité. Si votre stack ne garantit pas cette séparation par défaut, vous accumulez une dette technique SEO invisible — jusqu'au jour où elle se manifeste par une chute de trafic que personne dans l'équipe ne peut expliquer.