[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fSHbZcBdDG3RjfmJG3iT31GhKMjaAnyWPtQ1u5IkJxTU":3,"$fuII7bvGHQwsVwu69a275y0TlZscMoPqnxeK9pmfe3Co":25},{"_id":4,"slug":5,"__v":6,"author":7,"body":8,"canonical":9,"category":10,"createdAt":11,"date":12,"description":13,"htmlContent":14,"image":15,"imageAlt":15,"readingTime":16,"tags":17,"title":23,"updatedAt":24},"69fca993aa6b273b0c226420","5-javascript-seo-lessons-from-top-ecommerce-sites",0,"Equipe Seogard","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.\n\n## Leçon 1 : le SSR hybride comme standard, pas comme option\n\nLa 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.\n\n### Le pattern hybride de Chewy\n\nChewy 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).\n\nEn 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.\n\n```bash\n# Vérifier le HTML servi par le serveur (avant exécution JS)\ncurl -s \"https://www.chewy.com/dp/12345\" | grep -E '\u003Ch1|\u003Cmeta name=\"description\"|itemprop=\"price\"'\n\n# Comparer avec le DOM rendu par le navigateur\n# Dans Chrome DevTools > Console :\n# document.querySelector('h1').textContent\n# Puis comparer avec le source brut\n```\n\nCe 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).\n\n### Pourquoi le dynamic rendering est un piège à long terme\n\nCertains sites optent pour le dynamic rendering — servir du HTML pré-rendu à Googlebot et du CSR aux utilisateurs. Google a [explicitement indiqué](https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering) 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.\n\nUnder 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 :\n\n```typescript\n// Pattern type Next.js pour une page produit e-commerce\n// Le contenu SEO-critique est fetché côté serveur\nexport const getServerSideProps: GetServerSideProps = async (context) => {\n  const { slug } = context.params;\n  \n  // Fetch depuis l'API catalogue — exécuté côté serveur uniquement\n  const product = await catalogApi.getProduct(slug as string);\n  \n  if (!product) {\n    return { notFound: true }; // Retourne un vrai 404, pas une page vide\n  }\n\n  // Les avis sont chargés côté client — pas critiques pour l'indexation\n  return {\n    props: {\n      product: {\n        name: product.name,\n        description: product.description,\n        price: product.price,\n        availability: product.inStock ? 'InStock' : 'OutOfStock',\n        images: product.images,\n        // Structured data pré-calculée côté serveur\n        jsonLd: buildProductJsonLd(product),\n      },\n    },\n  };\n};\n```\n\nLe 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.\n\n## Leçon 2 : la navigation facettée sans dilution de crawl budget\n\nLa 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.\n\n### Le pattern canonique + noindex sélectif\n\nL'approche la plus robuste observée sur ces sites combine trois mécanismes :\n\n1. **Canonical vers la page de catégorie mère** pour les combinaisons de filtres non-stratégiques\n2. **Pages indexables dédiées** pour les combinaisons à fort volume de recherche (ex : \"manteau femme laine\")\n3. **Blocage par robots.txt** des patterns d'URL à paramètres combinés au-delà de 2 filtres\n\n```html\n\u003C!-- Page catégorie principale — indexable, canonique vers elle-même -->\n\u003Clink rel=\"canonical\" href=\"https://www.harrods.com/en-gb/shopping/women-coats\" />\n\n\u003C!-- Filtre unique stratégique — indexable avec son propre canonical -->\n\u003C!-- URL : /en-gb/shopping/women-coats?material=wool -->\n\u003Clink rel=\"canonical\" href=\"https://www.harrods.com/en-gb/shopping/women-wool-coats\" />\n\u003C!-- Note : l'URL à paramètre redirige 301 vers l'URL propre -->\n\n\u003C!-- Combinaison multi-filtres non stratégique — canonical vers le parent -->\n\u003C!-- URL : /en-gb/shopping/women-coats?material=wool&color=red&size=m -->\n\u003Clink rel=\"canonical\" href=\"https://www.harrods.com/en-gb/shopping/women-coats\" />\n\u003Cmeta name=\"robots\" content=\"noindex, follow\" />\n```\n\nLe 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.\n\nLa solution : toujours servir les balises `canonical` et `meta robots` dans la réponse HTTP initiale, côté serveur. Jamais côté client.\n\n### Gérer les paramètres en JavaScript sans polluer le crawl\n\nUn 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.\n\nCe point rejoint directement les problématiques de [tracking parameters dans les liens internes](/blog/why-tracking-parameters-in-internal-links-hurt-your-seo-and-how-to-fix-them) qui polluent le crawl budget de manière similaire.\n\nLa 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 `\u003Ca href>` vers une URL serveur.\n\n## Leçon 3 : les structured data injectées côté serveur, pas côté client\n\nLe 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.\n\n### Le timing du JSON-LD compte\n\nGoogle peut techniquement parser du JSON-LD injecté par JavaScript. C'est [documenté](https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data). 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.\n\nLa 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.\n\n```html\n\u003C!-- Pattern observé : JSON-LD injecté dans le \u003Chead> côté serveur -->\n\u003Chead>\n  \u003Cscript type=\"application/ld+json\">\n  {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"Product\",\n    \"name\": \"Wilderness Trail Mix Dry Dog Food\",\n    \"image\": \"https://img.chewy.com/product/12345.jpg\",\n    \"description\": \"Grain-free formula with real salmon...\",\n    \"sku\": \"CHW-12345\",\n    \"brand\": {\n      \"@type\": \"Brand\",\n      \"name\": \"Blue Buffalo\"\n    },\n    \"offers\": {\n      \"@type\": \"Offer\",\n      \"url\": \"https://www.chewy.com/dp/12345\",\n      \"priceCurrency\": \"USD\",\n      \"price\": \"52.99\",\n      \"availability\": \"https://schema.org/InStock\",\n      \"seller\": {\n        \"@type\": \"Organization\",\n        \"name\": \"Chewy\"\n      }\n    },\n    \"aggregateRating\": {\n      \"@type\": \"AggregateRating\",\n      \"ratingValue\": \"4.7\",\n      \"reviewCount\": \"2841\"\n    }\n  }\n  \u003C/script>\n\u003C/head>\n```\n\n### L'erreur classique du JSON-LD dynamique\n\nScé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 `\u003CHead>` (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.\n\nLa 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.\n\nVérifiez la validité de vos structured data en production avec le [Rich Results Test](https://search.google.com/test/rich-results) de Google en mode \"URL\" (pas \"code\"), car il exécute le JavaScript et montre ce que Googlebot voit réellement.\n\n## Leçon 4 : le lazy loading intelligent — ce qu'on charge, ce qu'on ne charge pas\n\nLes 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.\n\n### La stratégie d'Under Armour : priorisation agressive\n\nEn auditant le waterfall réseau d'Under Armour avec Chrome DevTools (onglet Performance), on observe un pattern clair :\n\n- **Critique (chargé synchrone)** : HTML SSR + CSS critique inline + polices système\n- **Important (chargé en defer)** : bundle JS principal (navigation, interactivité produit)\n- **Non-critique (chargé après interaction)** : scripts tiers (analytics, chat, social proof)\n\n```html\n\u003C!-- Pattern de chargement priorisé -->\n\u003Chead>\n  \u003C!-- CSS critique inliné — pas de requête bloquante -->\n  \u003Cstyle>\n    /* CSS critique pour le above-the-fold : header, hero produit, prix */\n    .product-hero { display: grid; grid-template-columns: 1fr 1fr; }\n    .product-title { font-size: 1.5rem; font-weight: 700; }\n    .product-price { font-size: 1.25rem; color: #c41230; }\n  \u003C/style>\n  \n  \u003C!-- Preload des ressources critiques -->\n  \u003Clink rel=\"preload\" href=\"/fonts/ua-brand.woff2\" as=\"font\" type=\"font/woff2\" crossorigin />\n  \u003Clink rel=\"preload\" href=\"/images/product/12345-hero.webp\" as=\"image\" />\n  \n  \u003C!-- Bundle JS principal — defer, jamais async pour garantir l'ordre -->\n  \u003Cscript src=\"/js/main.bundle.js\" defer>\u003C/script>\n\u003C/head>\n\n\u003Cbody>\n  \u003C!-- Contenu SSR complet ici -->\n  \n  \u003C!-- Scripts tiers chargés après l'événement load -->\n  \u003Cscript>\n    window.addEventListener('load', () => {\n      // Analytics — attend que la page soit interactive\n      const gtm = document.createElement('script');\n      gtm.src = 'https://www.googletagmanager.com/gtm.js?id=GTM-XXXXX';\n      document.head.appendChild(gtm);\n      \n      // Chat widget — chargé uniquement après scroll ou délai\n      let chatLoaded = false;\n      const loadChat = () => {\n        if (chatLoaded) return;\n        chatLoaded = true;\n        const chat = document.createElement('script');\n        chat.src = 'https://widget.intercom.io/widget/xxxxx';\n        document.body.appendChild(chat);\n      };\n      \n      window.addEventListener('scroll', loadChat, { once: true });\n      setTimeout(loadChat, 5000); // Fallback si pas de scroll\n    });\n  \u003C/script>\n\u003C/body>\n```\n\n### L'impact mesurable sur le crawl\n\nUn 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.\n\nPourquoi 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.\n\n## Leçon 5 : les liens internes en JavaScript — le piège invisible\n\nC'est probablement le problème JavaScript SEO le plus sous-estimé. Googlebot suit les liens `\u003Ca href=\"...\">`. Il ne suit **pas** les navigations déclenchées par `onClick`, `window.location`, ou `router.push` sans balise `\u003Ca>` sous-jacente.\n\n### Le pattern SPA qui casse la découverte de pages\n\nDans une application React ou Vue classique, la navigation entre pages utilise souvent un composant `\u003CLink>` du framework (React Router, Vue Router, Next.js Link). Ces composants rendent bien un `\u003Ca href>` dans le DOM — en théorie. Mais certaines implémentations custom remplacent le `\u003Ca>` par un `\u003Cdiv>` ou un `\u003Cbutton>` avec un handler `onClick` qui appelle `router.push()`.\n\nHarrods utilise un pattern rigoureux : chaque élément de navigation cliquable dans le catalogue est un véritable `\u003Ca 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 `\u003Ca href>` est toujours présent dans le HTML initial pour Googlebot.\n\nVérifiez ce point sur votre site :\n\n```bash\n# Screaming Frog : crawl en mode \"JavaScript rendering\"\n# Config > Spider > Rendering > JavaScript\n# Comparer les liens découverts avec le crawl HTML-only\n\n# Alternative CLI avec puppeteer pour extraire les liens post-rendering\nnode -e \"\nconst puppeteer = require('puppeteer');\n(async () => {\n  const browser = await puppeteer.launch();\n  const page = await browser.newPage();\n  await page.goto('https://votre-ecommerce.com/category/shoes', { \n    waitUntil: 'networkidle0' \n  });\n  \n  // Extraire tous les liens \u003Ca> avec href\n  const links = await page.evaluate(() =>\n    Array.from(document.querySelectorAll('a[href]'))\n      .map(a => ({ text: a.textContent.trim(), href: a.href }))\n      .filter(l => l.href.includes('/product/'))\n  );\n  \n  console.log(JSON.stringify(links, null, 2));\n  console.log('Total product links found:', links.length);\n  await browser.close();\n})();\n\"\n```\n\n### Le cas des mega-menus JavaScript\n\nLes 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.\n\nLa 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é](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics) qu'il suit les liens dans le HTML même s'ils ne sont pas visibles à l'écran.\n\nCe point est particulièrement critique pour les sites qui dépendent d'une architecture de [SEO programmatique sémantique](/blog/a-blueprint-for-semantic-programmatic-seo) où la structure de liens internes est le principal vecteur de distribution de l'autorité.\n\n## La méthodologie d'audit JavaScript SEO en 5 étapes\n\nPlutôt qu'une simple checklist, voici la séquence d'audit que ces leçons impliquent pour votre propre e-commerce :\n\n### Étape 1 : comparer HTML brut vs DOM rendu\n\nPour 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 :\n\n- Le `\u003Ctitle>` est-il identique ?\n- Le `\u003Clink rel=\"canonical\">` est-il présent dans les deux ?\n- Le contenu textuel principal (H1, description produit) est-il dans le HTML brut ?\n- Le JSON-LD est-il complet dans le HTML brut ?\n\nSi une de ces réponses est \"non\", vous avez un problème de rendering critique.\n\n### Étape 2 : auditer le budget crawl consommé par les URLs parasites\n\nDans 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.\n\n### Étape 3 : tester le rendering Googlebot réel\n\nL'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).\n\n### Étape 4 : monitorer les régressions en continu\n\nUn déploiement front-end qui change le composant `\u003CHead>` 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.\n\nUn 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.\n\n### Étape 5 : mesurer l'impact SEO réel\n\nCorrélation n'est pas causalité, mais suivez ces métriques avant/après vos corrections JavaScript :\n\n- **Taux de pages indexées** : Search Console > Pages > comparaison sur 30 jours\n- **Crawl rate** : Search Console > Paramètres > Statistiques d'exploration\n- **Rich results éligibles** : rapport \"Améliorations\" dans Search Console\n- **LCP/INP** : rapport Core Web Vitals (données terrain CrUX)\n\n## Le facteur émergent : les bots IA et le JavaScript\n\nUn 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.\n\nCette réalité renforce l'argument du SSR. L'[activité de crawl d'OpenAI a triplé](/blog/openai-crawl-activity-tripled-since-gpt-5-data-shows-via-sejournal-mattgsouthern) récemment, et [Google encourage les développeurs à construire pour les agents IA](/blog/google-tells-developers-to-build-for-ai-agents-not-just-humans-via-sejournal-mattgsouthern). 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](/blog/4-signals-that-now-define-visibility-in-ai-search).\n\n---\n\nLes 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.","https://seogard.io/blog/5-javascript-seo-lessons-from-top-ecommerce-sites","Actualités SEO","2026-05-07T15:02:43.964Z","2026-05-07","Chewy, Harrods, Under Armour : comment les grands e-commerces gèrent rendering, navigation JS et structured data sans sacrifier le SEO.","\u003Cp>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.\u003C/p>\n\u003Ch2>Leçon 1 : le SSR hybride comme standard, pas comme option\u003C/h2>\n\u003Cp>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.\u003C/p>\n\u003Ch3>Le pattern hybride de Chewy\u003C/h3>\n\u003Cp>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).\u003C/p>\n\u003Cp>En inspectant le source HTML retourné par leur serveur (via \u003Ccode>curl\u003C/code> ou \u003Ccode>View Source\u003C/code>, 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.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Vérifier le HTML servi par le serveur (avant exécution JS)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -s\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"https://www.chewy.com/dp/12345\"\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -E\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '&#x3C;h1|&#x3C;meta name=\"description\"|itemprop=\"price\"'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Comparer avec le DOM rendu par le navigateur\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Dans Chrome DevTools > Console :\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># document.querySelector('h1').textContent\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Puis comparer avec le source brut\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>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).\u003C/p>\n\u003Ch3>Pourquoi le dynamic rendering est un piège à long terme\u003C/h3>\n\u003Cp>Certains sites optent pour le dynamic rendering — servir du HTML pré-rendu à Googlebot et du CSR aux utilisateurs. Google a \u003Ca href=\"https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering\">explicitement indiqué\u003C/a> 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.\u003C/p>\n\u003Cp>Under Armour illustre bien la bonne approche. Leur stack Next.js utilise \u003Ccode>getServerSideProps\u003C/code> pour les pages produit critiques, garantissant que le HTML complet est généré à chaque requête :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Pattern type Next.js pour une page produit e-commerce\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Le contenu SEO-critique est fetché côté serveur\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> const\u003C/span>\u003Cspan style=\"color:#B392F0\"> getServerSideProps\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> GetServerSideProps\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> async\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#FFAB70\">context\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#79B8FF\">slug\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> context.params;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  // Fetch depuis l'API catalogue — exécuté côté serveur uniquement\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> product\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> catalogApi.\u003C/span>\u003Cspan style=\"color:#B392F0\">getProduct\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(slug \u003C/span>\u003Cspan style=\"color:#F97583\">as\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">product) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { notFound: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> }; \u003C/span>\u003Cspan style=\"color:#6A737D\">// Retourne un vrai 404, pas une page vide\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  // Les avis sont chargés côté client — pas critiques pour l'indexation\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    props: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      product: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        name: product.name,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        description: product.description,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        price: product.price,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        availability: product.inStock \u003C/span>\u003Cspan style=\"color:#F97583\">?\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'InStock'\u003C/span>\u003Cspan style=\"color:#F97583\"> :\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'OutOfStock'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        images: product.images,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">        // Structured data pré-calculée côté serveur\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        jsonLd: \u003C/span>\u003Cspan style=\"color:#B392F0\">buildProductJsonLd\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(product),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  };\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">};\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>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.\u003C/p>\n\u003Ch2>Leçon 2 : la navigation facettée sans dilution de crawl budget\u003C/h2>\n\u003Cp>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.\u003C/p>\n\u003Ch3>Le pattern canonique + noindex sélectif\u003C/h3>\n\u003Cp>L'approche la plus robuste observée sur ces sites combine trois mécanismes :\u003C/p>\n\u003Col>\n\u003Cli>\u003Cstrong>Canonical vers la page de catégorie mère\u003C/strong> pour les combinaisons de filtres non-stratégiques\u003C/li>\n\u003Cli>\u003Cstrong>Pages indexables dédiées\u003C/strong> pour les combinaisons à fort volume de recherche (ex : \"manteau femme laine\")\u003C/li>\n\u003Cli>\u003Cstrong>Blocage par robots.txt\u003C/strong> des patterns d'URL à paramètres combinés au-delà de 2 filtres\u003C/li>\n\u003C/ol>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">&#x3C;!-- Page catégorie principale — indexable, canonique vers elle-même -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">link\u003C/span>\u003Cspan style=\"color:#B392F0\"> rel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"canonical\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> href\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"https://www.harrods.com/en-gb/shopping/women-coats\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">&#x3C;!-- Filtre unique stratégique — indexable avec son propre canonical -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">&#x3C;!-- URL : /en-gb/shopping/women-coats?material=wool -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">link\u003C/span>\u003Cspan style=\"color:#B392F0\"> rel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"canonical\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> href\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"https://www.harrods.com/en-gb/shopping/women-wool-coats\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">&#x3C;!-- Note : l'URL à paramètre redirige 301 vers l'URL propre -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">&#x3C;!-- Combinaison multi-filtres non stratégique — canonical vers le parent -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">&#x3C;!-- URL : /en-gb/shopping/women-coats?material=wool&#x26;color=red&#x26;size=m -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">link\u003C/span>\u003Cspan style=\"color:#B392F0\"> rel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"canonical\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> href\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"https://www.harrods.com/en-gb/shopping/women-coats\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">meta\u003C/span>\u003Cspan style=\"color:#B392F0\"> name\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"robots\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> content\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"noindex, follow\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>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.\u003C/p>\n\u003Cp>La solution : toujours servir les balises \u003Ccode>canonical\u003C/code> et \u003Ccode>meta robots\u003C/code> dans la réponse HTTP initiale, côté serveur. Jamais côté client.\u003C/p>\n\u003Ch3>Gérer les paramètres en JavaScript sans polluer le crawl\u003C/h3>\n\u003Cp>Un problème récurrent sur les e-commerces SPA : le routage côté client modifie l'URL (via \u003Ccode>history.pushState\u003C/code>) 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.\u003C/p>\n\u003Cp>Ce point rejoint directement les problématiques de \u003Ca href=\"/blog/why-tracking-parameters-in-internal-links-hurt-your-seo-and-how-to-fix-them\">tracking parameters dans les liens internes\u003C/a> qui polluent le crawl budget de manière similaire.\u003C/p>\n\u003Cp>La solution observée chez Under Armour : les interactions de filtrage côté client ne modifient \u003Cstrong>pas\u003C/strong> l'URL pour les combinaisons non indexables. Elles utilisent le \u003Ccode>state\u003C/code> du composant React sans toucher à \u003Ccode>window.location\u003C/code>. Seules les combinaisons stratégiques (qui ont une page SSR dédiée) génèrent un vrai lien \u003Ccode>&#x3C;a href>\u003C/code> vers une URL serveur.\u003C/p>\n\u003Ch2>Leçon 3 : les structured data injectées côté serveur, pas côté client\u003C/h2>\n\u003Cp>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.\u003C/p>\n\u003Ch3>Le timing du JSON-LD compte\u003C/h3>\n\u003Cp>Google peut techniquement parser du JSON-LD injecté par JavaScript. C'est \u003Ca href=\"https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data\">documenté\u003C/a>. Mais dans la pratique, les grands e-commerces ne prennent pas ce risque. Chewy, Harrods, Under Armour — tous trois injectent le JSON-LD \u003Ccode>Product\u003C/code> dans le HTML initial.\u003C/p>\n\u003Cp>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.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">&#x3C;!-- Pattern observé : JSON-LD injecté dans le &#x3C;head> côté serveur -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">head\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">script\u003C/span>\u003Cspan style=\"color:#B392F0\"> type\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"application/ld+json\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \"@context\": \"https://schema.org\",\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \"@type\": \"Product\",\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \"name\": \"Wilderness Trail Mix Dry Dog Food\",\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \"image\": \"https://img.chewy.com/product/12345.jpg\",\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \"description\": \"Grain-free formula with real salmon...\",\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \"sku\": \"CHW-12345\",\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \"brand\": {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      \"@type\": \"Brand\",\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      \"name\": \"Blue Buffalo\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \"offers\": {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      \"@type\": \"Offer\",\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      \"url\": \"https://www.chewy.com/dp/12345\",\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      \"priceCurrency\": \"USD\",\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      \"price\": \"52.99\",\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      \"availability\": \"https://schema.org/InStock\",\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      \"seller\": {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        \"@type\": \"Organization\",\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        \"name\": \"Chewy\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \"aggregateRating\": {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      \"@type\": \"AggregateRating\",\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      \"ratingValue\": \"4.7\",\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      \"reviewCount\": \"2841\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">script\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">head\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Ch3>L'erreur classique du JSON-LD dynamique\u003C/h3>\n\u003Cp>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 \u003Ccode>&#x3C;Head>\u003C/code> (via \u003Ccode>react-helmet\u003C/code> ou \u003Ccode>next/head\u003C/code>). Le problème : le prix affiché à l'utilisateur provient d'une API pricing appelée côté client \u003Cstrong>après\u003C/strong> le montage du composant. Le HTML servi par le serveur contient donc un JSON-LD avec un prix placeholder (ou pire, \u003Ccode>null\u003C/code>). Googlebot voit un structured data invalide et ne génère aucun rich result.\u003C/p>\n\u003Cp>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.\u003C/p>\n\u003Cp>Vérifiez la validité de vos structured data en production avec le \u003Ca href=\"https://search.google.com/test/rich-results\">Rich Results Test\u003C/a> de Google en mode \"URL\" (pas \"code\"), car il exécute le JavaScript et montre ce que Googlebot voit réellement.\u003C/p>\n\u003Ch2>Leçon 4 : le lazy loading intelligent — ce qu'on charge, ce qu'on ne charge pas\u003C/h2>\n\u003Cp>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.\u003C/p>\n\u003Ch3>La stratégie d'Under Armour : priorisation agressive\u003C/h3>\n\u003Cp>En auditant le waterfall réseau d'Under Armour avec Chrome DevTools (onglet Performance), on observe un pattern clair :\u003C/p>\n\u003Cul>\n\u003Cli>\u003Cstrong>Critique (chargé synchrone)\u003C/strong> : HTML SSR + CSS critique inline + polices système\u003C/li>\n\u003Cli>\u003Cstrong>Important (chargé en defer)\u003C/strong> : bundle JS principal (navigation, interactivité produit)\u003C/li>\n\u003Cli>\u003Cstrong>Non-critique (chargé après interaction)\u003C/strong> : scripts tiers (analytics, chat, social proof)\u003C/li>\n\u003C/ul>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">&#x3C;!-- Pattern de chargement priorisé -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">head\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  &#x3C;!-- CSS critique inliné — pas de requête bloquante -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">style\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    /* CSS critique pour le above-the-fold : header, hero produit, prix */\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    .product-hero\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#79B8FF\">display\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">grid\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; \u003C/span>\u003Cspan style=\"color:#79B8FF\">grid-template-columns\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#F97583\">fr\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 1\u003C/span>\u003Cspan style=\"color:#F97583\">fr\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    .product-title\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#79B8FF\">font-size\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">1.5\u003C/span>\u003Cspan style=\"color:#F97583\">rem\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; \u003C/span>\u003Cspan style=\"color:#79B8FF\">font-weight\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">700\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    .product-price\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#79B8FF\">font-size\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">1.25\u003C/span>\u003Cspan style=\"color:#F97583\">rem\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; \u003C/span>\u003Cspan style=\"color:#79B8FF\">color\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">#c41230\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">style\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  &#x3C;!-- Preload des ressources critiques -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">link\u003C/span>\u003Cspan style=\"color:#B392F0\"> rel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"preload\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> href\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"/fonts/ua-brand.woff2\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> as\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"font\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> type\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"font/woff2\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> crossorigin\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">link\u003C/span>\u003Cspan style=\"color:#B392F0\"> rel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"preload\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> href\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"/images/product/12345-hero.webp\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> as\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"image\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  &#x3C;!-- Bundle JS principal — defer, jamais async pour garantir l'ordre -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">script\u003C/span>\u003Cspan style=\"color:#B392F0\"> src\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"/js/main.bundle.js\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> defer\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">script\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">head\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">body\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  &#x3C;!-- Contenu SSR complet ici -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  &#x3C;!-- Scripts tiers chargés après l'événement load -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">script\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    window.\u003C/span>\u003Cspan style=\"color:#B392F0\">addEventListener\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'load'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, () \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">      // Analytics — attend que la page soit interactive\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> gtm\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> document.\u003C/span>\u003Cspan style=\"color:#B392F0\">createElement\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'script'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      gtm.src \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'https://www.googletagmanager.com/gtm.js?id=GTM-XXXXX'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      document.head.\u003C/span>\u003Cspan style=\"color:#B392F0\">appendChild\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(gtm);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">      // Chat widget — chargé uniquement après scroll ou délai\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      let\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> chatLoaded \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> false\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      const\u003C/span>\u003Cspan style=\"color:#B392F0\"> loadChat\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> () \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (chatLoaded) \u003C/span>\u003Cspan style=\"color:#F97583\">return\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        chatLoaded \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> true\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> chat\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> document.\u003C/span>\u003Cspan style=\"color:#B392F0\">createElement\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'script'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        chat.src \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'https://widget.intercom.io/widget/xxxxx'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        document.body.\u003C/span>\u003Cspan style=\"color:#B392F0\">appendChild\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(chat);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      };\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      window.\u003C/span>\u003Cspan style=\"color:#B392F0\">addEventListener\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'scroll'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, loadChat, { once: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">      setTimeout\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(loadChat, \u003C/span>\u003Cspan style=\"color:#79B8FF\">5000\u003C/span>\u003Cspan style=\"color:#E1E4E8\">); \u003C/span>\u003Cspan style=\"color:#6A737D\">// Fallback si pas de scroll\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">script\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">body\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Ch3>L'impact mesurable sur le crawl\u003C/h3>\n\u003Cp>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.\u003C/p>\n\u003Cp>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.\u003C/p>\n\u003Ch2>Leçon 5 : les liens internes en JavaScript — le piège invisible\u003C/h2>\n\u003Cp>C'est probablement le problème JavaScript SEO le plus sous-estimé. Googlebot suit les liens \u003Ccode>&#x3C;a href=\"...\">\u003C/code>. Il ne suit \u003Cstrong>pas\u003C/strong> les navigations déclenchées par \u003Ccode>onClick\u003C/code>, \u003Ccode>window.location\u003C/code>, ou \u003Ccode>router.push\u003C/code> sans balise \u003Ccode>&#x3C;a>\u003C/code> sous-jacente.\u003C/p>\n\u003Ch3>Le pattern SPA qui casse la découverte de pages\u003C/h3>\n\u003Cp>Dans une application React ou Vue classique, la navigation entre pages utilise souvent un composant \u003Ccode>&#x3C;Link>\u003C/code> du framework (React Router, Vue Router, Next.js Link). Ces composants rendent bien un \u003Ccode>&#x3C;a href>\u003C/code> dans le DOM — en théorie. Mais certaines implémentations custom remplacent le \u003Ccode>&#x3C;a>\u003C/code> par un \u003Ccode>&#x3C;div>\u003C/code> ou un \u003Ccode>&#x3C;button>\u003C/code> avec un handler \u003Ccode>onClick\u003C/code> qui appelle \u003Ccode>router.push()\u003C/code>.\u003C/p>\n\u003Cp>Harrods utilise un pattern rigoureux : chaque élément de navigation cliquable dans le catalogue est un véritable \u003Ccode>&#x3C;a href>\u003C/code> 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 \u003Ccode>&#x3C;a href>\u003C/code> est toujours présent dans le HTML initial pour Googlebot.\u003C/p>\n\u003Cp>Vérifiez ce point sur votre site :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Screaming Frog : crawl en mode \"JavaScript rendering\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Config > Spider > Rendering > JavaScript\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Comparer les liens découverts avec le crawl HTML-only\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Alternative CLI avec puppeteer pour extraire les liens post-rendering\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">node\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -e\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">const puppeteer = require('puppeteer');\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">(async () => {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  const browser = await puppeteer.launch();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  const page = await browser.newPage();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  await page.goto('https://votre-ecommerce.com/category/shoes', { \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    waitUntil: 'networkidle0' \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  // Extraire tous les liens &#x3C;a> avec href\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  const links = await page.evaluate(() =>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    Array.from(document.querySelectorAll('a[href]'))\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">      .map(a => ({ text: a.textContent.trim(), href: a.href }))\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">      .filter(l => l.href.includes('/product/'))\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  \u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  console.log(JSON.stringify(links, null, 2));\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  console.log('Total product links found:', links.length);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  await browser.close();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">})();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Ch3>Le cas des mega-menus JavaScript\u003C/h3>\n\u003Cp>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.\u003C/p>\n\u003Cp>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 \u003Ccode>display:none\u003C/code> jusqu'au hover), les liens sont dans le DOM et accessibles au crawl. Google a \u003Ca href=\"https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics\">confirmé\u003C/a> qu'il suit les liens dans le HTML même s'ils ne sont pas visibles à l'écran.\u003C/p>\n\u003Cp>Ce point est particulièrement critique pour les sites qui dépendent d'une architecture de \u003Ca href=\"/blog/a-blueprint-for-semantic-programmatic-seo\">SEO programmatique sémantique\u003C/a> où la structure de liens internes est le principal vecteur de distribution de l'autorité.\u003C/p>\n\u003Ch2>La méthodologie d'audit JavaScript SEO en 5 étapes\u003C/h2>\n\u003Cp>Plutôt qu'une simple checklist, voici la séquence d'audit que ces leçons impliquent pour votre propre e-commerce :\u003C/p>\n\u003Ch3>Étape 1 : comparer HTML brut vs DOM rendu\u003C/h3>\n\u003Cp>Pour chaque type de page template (accueil, catégorie, produit, recherche interne), récupérez le HTML brut (\u003Ccode>curl\u003C/code> ou \u003Ccode>wget\u003C/code>) et le DOM rendu (Puppeteer, ou l'onglet \"Rendered HTML\" de Screaming Frog). Comparez :\u003C/p>\n\u003Cul>\n\u003Cli>Le \u003Ccode>&#x3C;title>\u003C/code> est-il identique ?\u003C/li>\n\u003Cli>Le \u003Ccode>&#x3C;link rel=\"canonical\">\u003C/code> est-il présent dans les deux ?\u003C/li>\n\u003Cli>Le contenu textuel principal (H1, description produit) est-il dans le HTML brut ?\u003C/li>\n\u003Cli>Le JSON-LD est-il complet dans le HTML brut ?\u003C/li>\n\u003C/ul>\n\u003Cp>Si une de ces réponses est \"non\", vous avez un problème de rendering critique.\u003C/p>\n\u003Ch3>Étape 2 : auditer le budget crawl consommé par les URLs parasites\u003C/h3>\n\u003Cp>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 \u003Ccode>canonical\u003C/code>/\u003Ccode>noindex\u003C/code> ne sont pas efficaces — probablement parce qu'elles sont injectées côté client.\u003C/p>\n\u003Ch3>Étape 3 : tester le rendering Googlebot réel\u003C/h3>\n\u003Cp>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).\u003C/p>\n\u003Ch3>Étape 4 : monitorer les régressions en continu\u003C/h3>\n\u003Cp>Un déploiement front-end qui change le composant \u003Ccode>&#x3C;Head>\u003C/code> 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.\u003C/p>\n\u003Cp>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.\u003C/p>\n\u003Ch3>Étape 5 : mesurer l'impact SEO réel\u003C/h3>\n\u003Cp>Corrélation n'est pas causalité, mais suivez ces métriques avant/après vos corrections JavaScript :\u003C/p>\n\u003Cul>\n\u003Cli>\u003Cstrong>Taux de pages indexées\u003C/strong> : Search Console > Pages > comparaison sur 30 jours\u003C/li>\n\u003Cli>\u003Cstrong>Crawl rate\u003C/strong> : Search Console > Paramètres > Statistiques d'exploration\u003C/li>\n\u003Cli>\u003Cstrong>Rich results éligibles\u003C/strong> : rapport \"Améliorations\" dans Search Console\u003C/li>\n\u003Cli>\u003Cstrong>LCP/INP\u003C/strong> : rapport Core Web Vitals (données terrain CrUX)\u003C/li>\n\u003C/ul>\n\u003Ch2>Le facteur émergent : les bots IA et le JavaScript\u003C/h2>\n\u003Cp>Un angle que les analyses classiques de JavaScript SEO ignorent : les crawlers IA (GPTBot, ClaudeBot, PerplexityBot) ne font \u003Cstrong>pas\u003C/strong> de rendering JavaScript. Ils fonctionnent comme un \u003Ccode>curl\u003C/code> — 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.\u003C/p>\n\u003Cp>Cette réalité renforce l'argument du SSR. L'\u003Ca href=\"/blog/openai-crawl-activity-tripled-since-gpt-5-data-shows-via-sejournal-mattgsouthern\">activité de crawl d'OpenAI a triplé\u003C/a> récemment, et \u003Ca href=\"/blog/google-tells-developers-to-build-for-ai-agents-not-just-humans-via-sejournal-mattgsouthern\">Google encourage les développeurs à construire pour les agents IA\u003C/a>. 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 \u003Ca href=\"/blog/4-signals-that-now-define-visibility-in-ai-search\">réponses IA\u003C/a>.\u003C/p>\n\u003Chr>\n\u003Cp>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.\u003C/p>",null,12,[18,19,20,21,22],"javascript","seo technique","ecommerce","rendering","structured data","JavaScript SEO e-commerce : 5 leçons des sites à fort trafic","Thu May 07 2026 15:02:43 GMT+0000 (Coordinated Universal Time)",[26,41,56],{"_id":27,"slug":28,"__v":6,"author":7,"canonical":29,"category":10,"createdAt":30,"date":31,"description":32,"image":15,"imageAlt":15,"readingTime":16,"tags":33,"title":39,"updatedAt":40},"6a041412aa6b273b0c40f181","how-to-build-local-pages-that-win-in-ai-powered-search-via-sejournal-lorenbaker","https://seogard.io/blog/how-to-build-local-pages-that-win-in-ai-powered-search-via-sejournal-lorenbaker","2026-05-13T06:02:58.743Z","2026-05-13","Guide technique pour construire des pages locales qui performent dans les AI Overviews et AI Mode. Schema, SSR, contenu structuré.",[34,35,36,37,38],"local SEO","AI search","pages locales","schema markup","SSR","Pages locales pour l'AI Search : architecture technique","Wed May 13 2026 06:02:58 GMT+0000 (Coordinated Universal Time)",{"_id":42,"slug":43,"__v":6,"author":7,"canonical":44,"category":10,"createdAt":45,"date":46,"description":47,"image":15,"imageAlt":15,"readingTime":48,"tags":49,"title":54,"updatedAt":55},"6a02c291aa6b273b0c2a74f9","the-tech-seo-audit-for-the-ai-search-era-how-to-maximize-your-ai-visibility-via-sejournal-jetoctopus","https://seogard.io/blog/the-tech-seo-audit-for-the-ai-search-era-how-to-maximize-your-ai-visibility-via-sejournal-jetoctopus","2026-05-12T06:02:57.339Z","2026-05-12","Comment adapter votre audit technique SEO aux exigences des AI Overviews, du crawl par les LLMs et du grounding. Méthodes, code et scénarios concrets.",14,[50,51,52,53,22],"tech seo audit","ai search","ai visibility","crawl budget","Audit SEO technique pour l'ère AI Search : guide avancé","Tue May 12 2026 06:02:57 GMT+0000 (Coordinated Universal Time)",{"_id":57,"slug":58,"__v":6,"author":7,"canonical":59,"category":10,"createdAt":60,"date":46,"description":61,"image":15,"imageAlt":15,"readingTime":16,"tags":62,"title":67,"updatedAt":68},"6a02fac0aa6b273b0c58d096","the-consensus-gap-via-sejournal-kevin-indig","https://seogard.io/blog/the-consensus-gap-via-sejournal-kevin-indig","2026-05-12T10:02:40.519Z","Une marque peut dominer dans un dashboard AI agrégé et être absente de deux moteurs sur trois. Analyse technique du Consensus Gap et méthodes pour le détecter.",[63,35,64,65,66],"consensus gap","LLM visibility","GEO","multi-engine","The Consensus Gap : votre marque visible sur un LLM, invisible sur deux autres","Tue May 12 2026 10:02:40 GMT+0000 (Coordinated Universal Time)"]