[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fSzr3najdTTOxm6DVy-yNU7EwEYjJP0ADpEZN0DKrxeA":3,"$faKoJ0gb5VU2VSRGBq6jASlZrUPYIMseS0cIuIEXqmWs":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},"69d95741aa6b273b0cba1619","websocket-et-seo-contenu-temps-reel-et-indexation",0,"Equipe Seogard","Googlebot n'ouvre pas de connexion WebSocket. Jamais. Le WRS (Web Rendering Service) exécute du JavaScript, attend le rendu, prend un snapshot du DOM — mais il ne maintient aucune connexion persistante bidirectionnelle. Si votre contenu critique arrive exclusivement via `ws://` ou `wss://`, il n'existe tout simplement pas pour Google.\n\nCe n'est pas un edge case théorique. Les plateformes de paris sportifs, les sites de bourse, les marketplaces avec stock temps réel, les dashboards SaaS publics — tous s'appuient sur WebSocket pour pousser du contenu. Et beaucoup découvrent, après des mois en production, que des milliers de pages sont indexées avec un DOM vide ou des placeholders.\n\n## Pourquoi Googlebot ignore les WebSockets\n\nLe WRS de Google est basé sur une version headless de Chrome. Il exécute le JavaScript de la page, attend que le réseau se stabilise (plus de requêtes HTTP pendantes), puis capture le DOM final. La documentation officielle de Google sur le [rendu JavaScript](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics) confirme que le renderer suit un modèle request-response classique.\n\nUn WebSocket casse ce modèle sur trois points :\n\n**Pas de signal de complétion.** Une requête `fetch()` ou `XMLHttpRequest` a un début et une fin. Le WRS peut attendre la réponse. Une connexion WebSocket, par nature, reste ouverte indéfiniment. Le renderer n'a aucun moyen de savoir quand \"tout le contenu\" a été reçu.\n\n**Timeout agressif.** Le WRS applique un timeout sur le rendu — généralement autour de 5 secondes pour le JavaScript initial, parfois étendu mais jamais indéfini. Une connexion WebSocket qui ne pousse pas de données immédiatement rate cette fenêtre.\n\n**Pas de handshake WebSocket observé.** Les tests empiriques via les outils de debug de Search Console (URL Inspection > \"Voir la page explorée\") montrent systématiquement que les connexions `ws://` et `wss://` ne sont pas établies. Le DOM capturé ne contient que ce qui a été rendu par des mécanismes HTTP standard.\n\nConcrètement, si vous inspectez le HTML rendu d'une page dépendant de WebSocket dans Search Console, vous verrez quelque chose comme :\n\n```html\n\u003C!-- Ce que Googlebot voit -->\n\u003Cdiv id=\"live-prices\">\n  \u003Cdiv class=\"price-container\">\n    \u003C!-- Placeholder vide — le composant attend des données WS -->\n    \u003Cspan class=\"price-value\">\u003C/span>\n    \u003Cspan class=\"price-change\">\u003C/span>\n  \u003C/div>\n\u003C/div>\n\n\u003C!-- Ce que l'utilisateur voit après connexion WS -->\n\u003Cdiv id=\"live-prices\">\n  \u003Cdiv class=\"price-container\">\n    \u003Cspan class=\"price-value\">142.87 €\u003C/span>\n    \u003Cspan class=\"price-change positive\">+2.3%\u003C/span>\n  \u003C/div>\n\u003C/div>\n```\n\nGoogle indexe la première version. Pas la seconde. Et si votre title, vos descriptions produit ou vos données structurées dépendent de ces données WebSocket, votre page est fonctionnellement vide pour le moteur.\n\n## Architecture de fallback : servir du contenu sans WebSocket\n\nLa solution n'est pas d'abandonner les WebSockets. C'est de ne jamais en faire la source unique de contenu indexable. Le pattern fondamental : **HTTP pour l'état initial, WebSocket pour les mises à jour temps réel.**\n\n### Le pattern SSR + hydration WebSocket\n\nL'idée est simple : le serveur rend la page avec les données les plus récentes via SSR (ou SSG avec ISR), puis le client se connecte au WebSocket pour recevoir les mises à jour en temps réel.\n\nVoici une implémentation type avec Next.js App Router :\n\n```typescript\n// app/stocks/[ticker]/page.tsx\nimport { Suspense } from 'react';\nimport { StockPrice } from '@/components/StockPrice';\nimport { getLatestPrice } from '@/lib/stock-api';\n\n// Les données fraîches sont récupérées côté serveur via HTTP\nexport default async function StockPage({ params }: { params: { ticker: string } }) {\n  const { ticker } = params;\n  const initialPrice = await getLatestPrice(ticker);\n\n  return (\n    \u003Cmain>\n      \u003Ch1>{ticker} — Cours en temps réel\u003C/h1>\n      {/* Le composant reçoit les données initiales en props */}\n      {/* Côté client, il se connectera au WebSocket pour les updates */}\n      \u003CSuspense fallback={\u003CPriceSkeleton />}>\n        \u003CStockPrice ticker={ticker} initialData={initialPrice} />\n      \u003C/Suspense>\n      {/* Contenu statique toujours présent pour le SEO */}\n      \u003Csection>\n        \u003Ch2>Analyse technique {ticker}\u003C/h2>\n        \u003Cp>Dernier cours de clôture : {initialPrice.close} €...\u003C/p>\n      \u003C/section>\n    \u003C/main>\n  );\n}\n\n// Revalidation ISR toutes les 60 secondes\nexport const revalidate = 60;\n```\n\n```typescript\n// components/StockPrice.tsx\n'use client';\n\nimport { useState, useEffect, useRef } from 'react';\n\ninterface PriceData {\n  price: number;\n  change: number;\n  timestamp: string;\n}\n\nexport function StockPrice({ ticker, initialData }: { ticker: string; initialData: PriceData }) {\n  // L'état initial vient du SSR — Googlebot verra ces données\n  const [price, setPrice] = useState\u003CPriceData>(initialData);\n  const wsRef = useRef\u003CWebSocket | null>(null);\n\n  useEffect(() => {\n    // La connexion WebSocket ne s'ouvre que côté client\n    // Googlebot n'exécutera jamais ce code de manière fonctionnelle\n    const ws = new WebSocket(`wss://stream.example-trading.com/prices/${ticker}`);\n\n    ws.onmessage = (event) => {\n      const data = JSON.parse(event.data);\n      setPrice({\n        price: data.price,\n        change: data.change,\n        timestamp: data.timestamp,\n      });\n    };\n\n    ws.onerror = () => {\n      // Fallback : polling HTTP si le WebSocket échoue\n      startHttpPolling(ticker, setPrice);\n    };\n\n    wsRef.current = ws;\n    return () => ws.close();\n  }, [ticker]);\n\n  return (\n    \u003Cdiv className=\"stock-price\" itemScope itemType=\"https://schema.org/FinancialProduct\">\n      \u003Cspan itemProp=\"price\">{price.price} €\u003C/span>\n      \u003Cspan className={price.change >= 0 ? 'positive' : 'negative'}>\n        {price.change >= 0 ? '+' : ''}{price.change}%\n      \u003C/span>\n      \u003Ctime dateTime={price.timestamp}>\n        Mis à jour : {new Date(price.timestamp).toLocaleTimeString('fr-FR')}\n      \u003C/time>\n    \u003C/div>\n  );\n}\n```\n\nCe pattern garantit que Googlebot reçoit le HTML pré-rendu avec les données de prix au moment du crawl. L'utilisateur, lui, voit immédiatement ces mêmes données puis reçoit les updates en temps réel via WebSocket. Pas de flash de contenu vide, pas de placeholder pour le bot.\n\n### L'API REST comme source de vérité crawlable\n\nSi vous avez une architecture [API-first](/blog/api-first-et-seo-servir-du-contenu-crawlable-depuis-une-api), le WebSocket ne devrait être qu'une couche de notification au-dessus de votre API REST existante. Les données doivent toujours être accessibles en HTTP.\n\nStructurez votre API pour exposer un endpoint de snapshot :\n\n```\nGET /api/v1/prices/AAPL          → dernière valeur connue (HTTP, cacheable)\nWS  wss://stream.example.com/AAPL → flux temps réel (WebSocket, non crawlable)\n```\n\nLe SSR appelle le premier. Le client se connecte au second. Si le WebSocket tombe, le client peut toujours faire du polling sur le premier endpoint.\n\n## Cas concret : migration d'un site de paris sportifs\n\nUn site de paris en ligne avec 18 000 pages de cotes (matchs, ligues, événements) avait construit son frontend en React SPA. Les cotes étaient poussées exclusivement via WebSocket — chaque page de match se connectait à un flux pour recevoir les cotes en temps réel.\n\n**Le problème** : sur les 18 000 pages, Search Console rapportait 14 200 pages comme \"Explorée, actuellement non indexée\". Le test d'URL en direct montrait des pages avec les structures HTML mais aucune donnée de cote. Les titles dynamiques (qui incluaient les noms des équipes et la cote principale) n'étaient pas rendus.\n\n**Le diagnostic** : en utilisant l'onglet Network de Chrome DevTools avec la case \"Preserve log\" activée et en bloquant les connexions WebSocket via `chrome://flags` ou via les overrides réseau, l'équipe a pu reproduire exactement ce que Googlebot voyait : des pages structurées mais creuses.\n\n**La solution déployée en 3 phases** :\n\n1. **Ajout d'une couche API REST** — chaque match exposait un endpoint `/api/matches/{id}/odds` retournant les dernières cotes connues. Temps de développement : 2 semaines.\n\n2. **Migration vers Next.js avec SSR** — les pages de match étaient rendues côté serveur avec les données de l'API REST. Le WebSocket ne s'activait que côté client pour le streaming des cotes. Le [changement de framework](/blog/changer-de-framework-next-js-vers-nuxt-ou-l-inverse-sans-perte-seo) a été planifié avec un mapping 1:1 des URLs.\n\n3. **Mise en place d'ISR avec revalidation à 30 secondes** — les pages étaient regénérées en arrière-plan pour que même le cache statique reste raisonnablement frais.\n\n**Les résultats après 8 semaines** : le nombre de pages indexées est passé de 3 800 à 16 400. Le trafic organique sur les pages de match a augmenté de 340%. Les rich results (données structurées `SportsEvent`) ont commencé à apparaître, ce qui était impossible quand les données n'étaient pas dans le HTML initial.\n\nLe point critique : aucune fonctionnalité temps réel n'a été sacrifiée. L'utilisateur voyait toujours les cotes se mettre à jour en live. Seule la source de l'état initial a changé — de WebSocket vers HTTP.\n\n## Données structurées et contenu WebSocket\n\nLes données structurées (JSON-LD, microdata) posent un problème spécifique avec le contenu WebSocket. Google parse le JSON-LD présent dans le HTML rendu au moment du crawl. Si vos données structurées sont générées dynamiquement par du JavaScript qui dépend d'une connexion WebSocket, elles seront absentes.\n\nLa règle est absolue : **les données structurées doivent être dans le HTML initial ou générées par du JavaScript qui s'exécute sans dépendance réseau persistante.**\n\n```html\n\u003C!-- Injecté côté serveur dans le \u003Chead> -->\n\u003Cscript type=\"application/ld+json\">\n{\n  \"@context\": \"https://schema.org\",\n  \"@type\": \"Product\",\n  \"name\": \"Action LVMH\",\n  \"offers\": {\n    \"@type\": \"Offer\",\n    \"price\": \"742.30\",\n    \"priceCurrency\": \"EUR\",\n    \"availability\": \"https://schema.org/InStock\",\n    \"priceValidUntil\": \"2026-04-10\"\n  }\n}\n\u003C/script>\n```\n\nCe JSON-LD est rendu côté serveur avec la dernière valeur connue. Quand le WebSocket met à jour le prix côté client, vous pouvez aussi mettre à jour le JSON-LD dans le DOM — mais cette mise à jour ne sera visible que par les utilisateurs, pas par Googlebot.\n\nUn piège fréquent : les SPA qui insèrent le `\u003Cscript type=\"application/ld+json\">` via JavaScript après réception des données WebSocket. Le [rendering budget de Google](/blog/rendering-budget-de-google-combien-de-javascript-est-trop) est limité, et si l'insertion dépend d'un événement WebSocket qui n'arrive jamais côté WRS, les rich results disparaissent.\n\n## Tester ce que Googlebot voit réellement\n\nVous ne pouvez pas supposer que votre fallback fonctionne. Vous devez le vérifier systématiquement.\n\n### Search Console : URL Inspection\n\nL'outil le plus direct. Dans Google Search Console, utilisez \"Inspecter l'URL\" > \"Tester l'URL en direct\" > \"Afficher la page testée\". Vous verrez :\n\n- Le **HTML rendu** : c'est le DOM final après exécution JavaScript par le WRS. Cherchez vos données critiques (prix, descriptions, titles) dans ce HTML.\n- La **capture d'écran** : vérifiez visuellement que le contenu est présent.\n- Les **erreurs de console** : les échecs de connexion WebSocket apparaîtront ici. Vous verrez typiquement `WebSocket connection to 'wss://...' failed`.\n\n### Screaming Frog en mode JavaScript rendering\n\nConfigurez Screaming Frog avec le rendering JavaScript activé (Configuration > Spider > Rendering > JavaScript). Lancez un crawl sur un échantillon de vos pages temps réel. Comparez le \"Rendered HTML\" avec le \"Raw HTML\" :\n\n- Si le contenu critique n'est que dans le Rendered HTML, vous dépendez du JavaScript — acceptable si ce JS ne requiert pas de WebSocket.\n- Si le contenu critique n'est ni dans le Raw HTML ni dans le Rendered HTML, vous avez un problème de dépendance WebSocket.\n\n### Bloquer les WebSockets en local\n\nLa méthode la plus fiable pour simuler le comportement de Googlebot vis-à-vis des WebSockets :\n\n```javascript\n// Injectez ce script en premier dans votre page pour tester\n// (via Chrome DevTools Overrides ou un bookmarklet)\n(function() {\n  const OriginalWebSocket = window.WebSocket;\n  window.WebSocket = function(url, protocols) {\n    console.warn(`[SEO Test] WebSocket bloqué : ${url}`);\n    // Simule un WebSocket qui ne se connecte jamais\n    return {\n      readyState: 3, // CLOSED\n      send: () => {},\n      close: () => {},\n      addEventListener: () => {},\n      removeEventListener: () => {},\n      onopen: null,\n      onclose: null,\n      onerror: null,\n      onmessage: null,\n    };\n  };\n  window.WebSocket.CONNECTING = 0;\n  window.WebSocket.OPEN = 1;\n  window.WebSocket.CLOSING = 2;\n  window.WebSocket.CLOSED = 3;\n  console.warn('[SEO Test] WebSocket intercepté — simulation mode Googlebot');\n})();\n```\n\nRechargez votre page avec ce script injecté. Ce que vous voyez est une approximation de ce que Googlebot voit. Si le contenu essentiel est absent, votre fallback SSR ne fonctionne pas correctement.\n\n## Gestion du crawl budget pour les pages temps réel\n\nLes pages à contenu temps réel posent un problème spécifique de crawl budget. Si vous avez 18 000 pages de cotes sportives qui changent toutes les 30 secondes, Google ne les recrawlera pas à cette fréquence. Acceptez-le : le contenu indexé sera toujours un snapshot daté.\n\n### Signaler la fraîcheur sans gaspiller le crawl budget\n\nUtilisez `lastmod` dans votre sitemap de manière intelligente. Ne mettez pas à jour le `lastmod` à chaque micro-changement de prix — Google finira par ignorer votre sitemap s'il détecte que les dates changent constamment sans modification substantielle du contenu (cf. la [documentation Google sur les sitemaps](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap)).\n\n```xml\n\u003C!-- sitemap-matches.xml -->\n\u003Curlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n  \u003Curl>\n    \u003Cloc>https://paris-sportifs.example.com/match/psg-marseille-2026-04-12\u003C/loc>\n    \u003C!-- lastmod = dernière modification structurelle (nouveau commentaire, changement de statut) -->\n    \u003C!-- PAS la dernière mise à jour de cote -->\n    \u003Clastmod>2026-04-09T14:30:00+02:00\u003C/lastmod>\n    \u003Cchangefreq>daily\u003C/changefreq>\n  \u003C/url>\n\u003C/urlset>\n```\n\nPour [suivre l'impact de ces optimisations](/blog/mesurer-l-impact-seo-technique-quels-kpis-suivre), monitorez le ratio pages crawlées/pages indexées dans Search Console et via l'[API Search Console](/blog/search-console-api-automatiser-le-reporting-seo).\n\n### Headers de cache pour différencier bots et utilisateurs\n\nVotre CDN peut servir des stratégies de cache différentes aux crawlers et aux utilisateurs. Ce n'est pas du cloaking — c'est de l'optimisation de delivery légitime, tant que le contenu est le même.\n\n```nginx\n# Configuration Nginx — cache plus long pour les bots\n# Le contenu servi est identique, seule la durée de cache change\nmap $http_user_agent $cache_control {\n    ~*Googlebot     \"public, max-age=3600, s-maxage=3600\";\n    ~*bingbot       \"public, max-age=3600, s-maxage=3600\";\n    default         \"public, max-age=30, s-maxage=30\";\n}\n\nserver {\n    location /match/ {\n        add_header Cache-Control $cache_control;\n        # Le SSR sert toujours le HTML complet avec les dernières données HTTP\n        proxy_pass http://nextjs-backend;\n    }\n}\n```\n\nCette approche s'inscrit dans une logique d'[edge SEO](/blog/edge-seo-modifier-les-reponses-http-au-niveau-cdn) où le CDN joue un rôle actif dans l'optimisation du delivery pour les crawlers.\n\n## Server-Sent Events : une alternative plus SEO-friendly ?\n\nLes Server-Sent Events (SSE) utilisent une connexion HTTP standard (`Content-Type: text/event-stream`). Contrairement aux WebSockets, c'est du HTTP pur — unidirectionnel, du serveur vers le client.\n\nEn théorie, SSE est plus compatible avec le modèle de Googlebot puisque c'est une requête HTTP. En pratique, le WRS ne consomme pas non plus les flux SSE, car il n'attend pas indéfiniment une réponse HTTP qui ne se termine jamais.\n\nLe même pattern de fallback s'applique : SSR pour l'état initial, SSE pour les updates client. L'avantage de SSE sur WebSocket est la reconnexion automatique native et la compatibilité avec les proxies HTTP classiques. Mais pour le SEO, le résultat est identique : vous devez fournir le contenu initial via SSR.\n\nLe vrai avantage de SSE est architectural — il simplifie la stack quand vous n'avez pas besoin de communication bidirectionnelle. Moins de complexité = moins de chances d'introduire une régression où le fallback HTTP cesse de fonctionner et où le contenu disparaît silencieusement de l'index.\n\n## Monitoring : détecter les régressions avant Google\n\nLe scénario cauchemar : un déploiement modifie le composant React, le fallback SSR casse, et pendant 3 semaines vos pages de produit sont indexées sans prix ni description. Vous ne le découvrez qu'en voyant le trafic organique chuter.\n\n### Intégrer les checks dans le CI/CD\n\nAvant chaque déploiement, validez que le HTML rendu côté serveur contient les éléments critiques. Un test simple mais efficace :\n\n```typescript\n// tests/seo/websocket-fallback.test.ts\nimport { describe, it, expect } from 'vitest';\n\nconst CRITICAL_PAGES = [\n  '/match/psg-marseille-2026-04-12',\n  '/stock/LVMH',\n  '/product/sneakers-limited-edition-42',\n];\n\ndescribe('SSR Fallback — contenu sans WebSocket', () => {\n  for (const path of CRITICAL_PAGES) {\n    it(`${path} contient les données critiques dans le HTML SSR`, async () => {\n      // Requête HTTP simple — simule un crawler\n      const response = await fetch(`http://localhost:3000${path}`, {\n        headers: { 'User-Agent': 'SEO-Test-Bot/1.0' },\n      });\n      const html = await response.text();\n\n      // Le prix/la cote doit être dans le HTML initial\n      expect(html).toMatch(/\\d+[\\.,]\\d{2}\\s*€/);\n\n      // Le JSON-LD doit être présent\n      expect(html).toContain('application/ld+json');\n\n      // Pas de placeholder vide\n      expect(html).not.toContain('Loading...');\n      expect(html).not.toContain('Chargement en cours');\n\n      // Le title ne doit pas être générique\n      const titleMatch = html.match(/\u003Ctitle>(.*?)\u003C\\/title>/);\n      expect(titleMatch).toBeTruthy();\n      expect(titleMatch![1]).not.toBe('Mon Site');\n      expect(titleMatch![1].length).toBeGreaterThan(20);\n    });\n  }\n});\n```\n\nCe type de test s'intègre directement dans votre [pipeline CI/CD](/blog/automatiser-les-checks-seo-dans-le-ci-cd). Un test qui échoue bloque le déploiement — mieux vaut retarder une release de 2 heures que perdre 3 semaines d'indexation.\n\n### Monitoring continu en production\n\nLes tests CI/CD couvrent le pré-déploiement. En production, vous avez besoin d'un monitoring continu qui vérifie que le contenu SSR reste intact. Un outil de monitoring comme Seogard détecte automatiquement les régressions de contenu — une meta description qui disparaît, un prix qui n'est plus rendu côté serveur, un JSON-LD manquant — et alerte avant que l'impact organique ne se matérialise.\n\nLa combinaison des deux couches — tests CI/CD pour prévenir les régressions au déploiement, monitoring continu pour attraper les problèmes en production (API down, timeout serveur, changement de comportement côté CDN) — est ce qui différencie les équipes qui maintiennent leurs positions organiques de celles qui jouent à la roulette russe à chaque sprint.\n\n## Réconcilier temps réel et crawlabilité\n\nLe WebSocket est un outil de delivery, pas une source de vérité SEO. Chaque donnée que vous voulez indexer doit exister en HTTP. Le SSR fournit le snapshot crawlable, le WebSocket fournit la fraîcheur pour l'utilisateur. Ce sont deux couches complémentaires, pas interchangeables.\n\nLa discipline technique qui en découle — maintenir une API REST miroir, tester le rendu sans WebSocket, monitorer les régressions de contenu en continu — est le prix à payer pour combiner interactivité temps réel et visibilité organique. Ce prix est modeste comparé à celui de découvrir, après un trimestre, que la moitié de vos pages n'étaient pas indexées.","https://seogard.io/blog/websocket-et-seo-contenu-temps-reel-et-indexation","Avancé","2026-04-10T20:02:09.399Z","2026-04-10","Comment gérer le contenu WebSocket pour qu'il soit crawlable et indexable. SSR, fallback HTTP, hydration : guide technique complet.","\u003Cp>Googlebot n'ouvre pas de connexion WebSocket. Jamais. Le WRS (Web Rendering Service) exécute du JavaScript, attend le rendu, prend un snapshot du DOM — mais il ne maintient aucune connexion persistante bidirectionnelle. Si votre contenu critique arrive exclusivement via \u003Ccode>ws://\u003C/code> ou \u003Ccode>wss://\u003C/code>, il n'existe tout simplement pas pour Google.\u003C/p>\n\u003Cp>Ce n'est pas un edge case théorique. Les plateformes de paris sportifs, les sites de bourse, les marketplaces avec stock temps réel, les dashboards SaaS publics — tous s'appuient sur WebSocket pour pousser du contenu. Et beaucoup découvrent, après des mois en production, que des milliers de pages sont indexées avec un DOM vide ou des placeholders.\u003C/p>\n\u003Ch2>Pourquoi Googlebot ignore les WebSockets\u003C/h2>\n\u003Cp>Le WRS de Google est basé sur une version headless de Chrome. Il exécute le JavaScript de la page, attend que le réseau se stabilise (plus de requêtes HTTP pendantes), puis capture le DOM final. La documentation officielle de Google sur le \u003Ca href=\"https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics\">rendu JavaScript\u003C/a> confirme que le renderer suit un modèle request-response classique.\u003C/p>\n\u003Cp>Un WebSocket casse ce modèle sur trois points :\u003C/p>\n\u003Cp>\u003Cstrong>Pas de signal de complétion.\u003C/strong> Une requête \u003Ccode>fetch()\u003C/code> ou \u003Ccode>XMLHttpRequest\u003C/code> a un début et une fin. Le WRS peut attendre la réponse. Une connexion WebSocket, par nature, reste ouverte indéfiniment. Le renderer n'a aucun moyen de savoir quand \"tout le contenu\" a été reçu.\u003C/p>\n\u003Cp>\u003Cstrong>Timeout agressif.\u003C/strong> Le WRS applique un timeout sur le rendu — généralement autour de 5 secondes pour le JavaScript initial, parfois étendu mais jamais indéfini. Une connexion WebSocket qui ne pousse pas de données immédiatement rate cette fenêtre.\u003C/p>\n\u003Cp>\u003Cstrong>Pas de handshake WebSocket observé.\u003C/strong> Les tests empiriques via les outils de debug de Search Console (URL Inspection > \"Voir la page explorée\") montrent systématiquement que les connexions \u003Ccode>ws://\u003C/code> et \u003Ccode>wss://\u003C/code> ne sont pas établies. Le DOM capturé ne contient que ce qui a été rendu par des mécanismes HTTP standard.\u003C/p>\n\u003Cp>Concrètement, si vous inspectez le HTML rendu d'une page dépendant de WebSocket dans Search Console, vous verrez quelque chose comme :\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;!-- Ce que Googlebot voit -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">div\u003C/span>\u003Cspan style=\"color:#B392F0\"> id\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"live-prices\"\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\">div\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"price-container\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    &#x3C;!-- Placeholder vide — le composant attend des données WS -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">span\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"price-value\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">span\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\">span\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"price-change\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">span\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\">div\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\">div\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;!-- Ce que l'utilisateur voit après connexion WS -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">div\u003C/span>\u003Cspan style=\"color:#B392F0\"> id\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"live-prices\"\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\">div\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"price-container\"\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\">span\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"price-value\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>142.87 €&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">span\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\">span\u003C/span>\u003Cspan style=\"color:#B392F0\"> class\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"price-change positive\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>+2.3%&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">span\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\">div\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\">div\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Google indexe la première version. Pas la seconde. Et si votre title, vos descriptions produit ou vos données structurées dépendent de ces données WebSocket, votre page est fonctionnellement vide pour le moteur.\u003C/p>\n\u003Ch2>Architecture de fallback : servir du contenu sans WebSocket\u003C/h2>\n\u003Cp>La solution n'est pas d'abandonner les WebSockets. C'est de ne jamais en faire la source unique de contenu indexable. Le pattern fondamental : \u003Cstrong>HTTP pour l'état initial, WebSocket pour les mises à jour temps réel.\u003C/strong>\u003C/p>\n\u003Ch3>Le pattern SSR + hydration WebSocket\u003C/h3>\n\u003Cp>L'idée est simple : le serveur rend la page avec les données les plus récentes via SSR (ou SSG avec ISR), puis le client se connecte au WebSocket pour recevoir les mises à jour en temps réel.\u003C/p>\n\u003Cp>Voici une implémentation type avec Next.js App Router :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// app/stocks/[ticker]/page.tsx\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { Suspense } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'react'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { StockPrice } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '@/components/StockPrice'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { getLatestPrice } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '@/lib/stock-api'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Les données fraîches sont récupérées côté serveur via HTTP\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> default\u003C/span>\u003Cspan style=\"color:#F97583\"> async\u003C/span>\u003Cspan style=\"color:#F97583\"> function\u003C/span>\u003Cspan style=\"color:#B392F0\"> StockPage\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({ \u003C/span>\u003Cspan style=\"color:#FFAB70\">params\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> }\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#FFAB70\">params\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#FFAB70\">ticker\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\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\">ticker\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> } \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> params;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> initialPrice\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> getLatestPrice\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(ticker);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#B392F0\">main\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#B392F0\">h1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>{ticker} — Cours en temps réel\u003C/span>\u003Cspan style=\"color:#F97583\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">h1\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      {\u003C/span>\u003Cspan style=\"color:#6A737D\">/* Le composant reçoit les données initiales en props */\u003C/span>\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      {\u003C/span>\u003Cspan style=\"color:#6A737D\">/* Côté client, il se connectera au WebSocket pour les updates */\u003C/span>\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#E1E4E8\">Suspense fallback\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{&#x3C;PriceSkeleton />}\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        &#x3C;\u003C/span>\u003Cspan style=\"color:#E1E4E8\">StockPrice ticker\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{ticker} initialData\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{initialPrice} \u003C/span>\u003Cspan style=\"color:#F97583\">/>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      &#x3C;/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">Suspense\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      {\u003C/span>\u003Cspan style=\"color:#6A737D\">/* Contenu statique toujours présent pour le SEO */\u003C/span>\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#B392F0\">section\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        &#x3C;\u003C/span>\u003Cspan style=\"color:#B392F0\">h2\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Analyse technique {\u003C/span>\u003Cspan style=\"color:#FFAB70\">ticker\u003C/span>\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003Cspan style=\"color:#F97583\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">h2\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        &#x3C;\u003C/span>\u003Cspan style=\"color:#B392F0\">p\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>Dernier cours de \u003C/span>\u003Cspan style=\"color:#FFAB70\">clôture\u003C/span>\u003Cspan style=\"color:#F97583\"> :\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003Cspan style=\"color:#B392F0\">initialPrice\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#FFAB70\">close\u003C/span>\u003Cspan style=\"color:#E1E4E8\">} €...&#x3C;/\u003C/span>\u003Cspan style=\"color:#B392F0\">p\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      &#x3C;/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">section\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    &#x3C;/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">main\u003C/span>\u003Cspan style=\"color:#F97583\">>\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\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Revalidation ISR toutes les 60 secondes\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:#79B8FF\"> revalidate\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 60\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// components/StockPrice.tsx\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">'use client'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { useState, useEffect, useRef } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'react'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">interface\u003C/span>\u003Cspan style=\"color:#B392F0\"> PriceData\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  price\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> number\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  change\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> number\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFAB70\">  timestamp\u003C/span>\u003Cspan style=\"color:#F97583\">:\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\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> function\u003C/span>\u003Cspan style=\"color:#B392F0\"> StockPrice\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({ \u003C/span>\u003Cspan style=\"color:#FFAB70\">ticker\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">initialData\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> }\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#FFAB70\">ticker\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; \u003C/span>\u003Cspan style=\"color:#FFAB70\">initialData\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> PriceData\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> }) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  // L'état initial vient du SSR — Googlebot verra ces données\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\">price\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">setPrice\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#B392F0\"> useState\u003C/span>\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#B392F0\">PriceData\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>(initialData);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> wsRef\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> useRef\u003C/span>\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#B392F0\">WebSocket\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#79B8FF\"> null\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>(\u003C/span>\u003Cspan style=\"color:#79B8FF\">null\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  useEffect\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\">    // La connexion WebSocket ne s'ouvre que côté client\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Googlebot n'exécutera jamais ce code de manière fonctionnelle\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> ws\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> new\u003C/span>\u003Cspan style=\"color:#B392F0\"> WebSocket\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`wss://stream.example-trading.com/prices/${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">ticker\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    ws.\u003C/span>\u003Cspan style=\"color:#B392F0\">onmessage\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#FFAB70\">event\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:#79B8FF\"> data\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#79B8FF\"> JSON\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#B392F0\">parse\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(event.data);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">      setPrice\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        price: data.price,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        change: data.change,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        timestamp: data.timestamp,\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\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    ws.\u003C/span>\u003Cspan style=\"color:#B392F0\">onerror\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:#6A737D\">      // Fallback : polling HTTP si le WebSocket échoue\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">      startHttpPolling\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(ticker, setPrice);\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:#E1E4E8\">    wsRef.current \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ws;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> () \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ws.\u003C/span>\u003Cspan style=\"color:#B392F0\">close\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  }, [ticker]);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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:#F97583\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#E1E4E8\">div className\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"stock-price\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> itemScope itemType\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"https://schema.org/FinancialProduct\"\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#E1E4E8\">span itemProp\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"price\"\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{price.price} €\u003C/span>\u003Cspan style=\"color:#F97583\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">span\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#E1E4E8\">span className\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{price.change >= \u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ? 'positive' : \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'negative'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        {\u003C/span>\u003Cspan style=\"color:#FFAB70\">price\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#FFAB70\">change\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> >\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0\u003C/span>\u003Cspan style=\"color:#F97583\"> ?\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '+'\u003C/span>\u003Cspan style=\"color:#F97583\"> :\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> ''\u003C/span>\u003Cspan style=\"color:#E1E4E8\">}{price.change}\u003C/span>\u003Cspan style=\"color:#F97583\">%\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      &#x3C;/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">span\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#E1E4E8\">time dateTime\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{price.timestamp}\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        Mis à \u003C/span>\u003Cspan style=\"color:#FFAB70\">jour\u003C/span>\u003Cspan style=\"color:#F97583\"> :\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003Cspan style=\"color:#B392F0\">new\u003C/span>\u003Cspan style=\"color:#B392F0\"> Date\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(price.timestamp).\u003C/span>\u003Cspan style=\"color:#B392F0\">toLocaleTimeString\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'fr-FR'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      &#x3C;/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">time\u003C/span>\u003Cspan style=\"color:#F97583\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    &#x3C;/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">div\u003C/span>\u003Cspan style=\"color:#F97583\">>\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>Ce pattern garantit que Googlebot reçoit le HTML pré-rendu avec les données de prix au moment du crawl. L'utilisateur, lui, voit immédiatement ces mêmes données puis reçoit les updates en temps réel via WebSocket. Pas de flash de contenu vide, pas de placeholder pour le bot.\u003C/p>\n\u003Ch3>L'API REST comme source de vérité crawlable\u003C/h3>\n\u003Cp>Si vous avez une architecture \u003Ca href=\"/blog/api-first-et-seo-servir-du-contenu-crawlable-depuis-une-api\">API-first\u003C/a>, le WebSocket ne devrait être qu'une couche de notification au-dessus de votre API REST existante. Les données doivent toujours être accessibles en HTTP.\u003C/p>\n\u003Cp>Structurez votre API pour exposer un endpoint de snapshot :\u003C/p>\n\u003Cpre>\u003Ccode>GET /api/v1/prices/AAPL          → dernière valeur connue (HTTP, cacheable)\nWS  wss://stream.example.com/AAPL → flux temps réel (WebSocket, non crawlable)\n\u003C/code>\u003C/pre>\n\u003Cp>Le SSR appelle le premier. Le client se connecte au second. Si le WebSocket tombe, le client peut toujours faire du polling sur le premier endpoint.\u003C/p>\n\u003Ch2>Cas concret : migration d'un site de paris sportifs\u003C/h2>\n\u003Cp>Un site de paris en ligne avec 18 000 pages de cotes (matchs, ligues, événements) avait construit son frontend en React SPA. Les cotes étaient poussées exclusivement via WebSocket — chaque page de match se connectait à un flux pour recevoir les cotes en temps réel.\u003C/p>\n\u003Cp>\u003Cstrong>Le problème\u003C/strong> : sur les 18 000 pages, Search Console rapportait 14 200 pages comme \"Explorée, actuellement non indexée\". Le test d'URL en direct montrait des pages avec les structures HTML mais aucune donnée de cote. Les titles dynamiques (qui incluaient les noms des équipes et la cote principale) n'étaient pas rendus.\u003C/p>\n\u003Cp>\u003Cstrong>Le diagnostic\u003C/strong> : en utilisant l'onglet Network de Chrome DevTools avec la case \"Preserve log\" activée et en bloquant les connexions WebSocket via \u003Ccode>chrome://flags\u003C/code> ou via les overrides réseau, l'équipe a pu reproduire exactement ce que Googlebot voyait : des pages structurées mais creuses.\u003C/p>\n\u003Cp>\u003Cstrong>La solution déployée en 3 phases\u003C/strong> :\u003C/p>\n\u003Col>\n\u003Cli>\n\u003Cp>\u003Cstrong>Ajout d'une couche API REST\u003C/strong> — chaque match exposait un endpoint \u003Ccode>/api/matches/{id}/odds\u003C/code> retournant les dernières cotes connues. Temps de développement : 2 semaines.\u003C/p>\n\u003C/li>\n\u003Cli>\n\u003Cp>\u003Cstrong>Migration vers Next.js avec SSR\u003C/strong> — les pages de match étaient rendues côté serveur avec les données de l'API REST. Le WebSocket ne s'activait que côté client pour le streaming des cotes. Le \u003Ca href=\"/blog/changer-de-framework-next-js-vers-nuxt-ou-l-inverse-sans-perte-seo\">changement de framework\u003C/a> a été planifié avec un mapping 1:1 des URLs.\u003C/p>\n\u003C/li>\n\u003Cli>\n\u003Cp>\u003Cstrong>Mise en place d'ISR avec revalidation à 30 secondes\u003C/strong> — les pages étaient regénérées en arrière-plan pour que même le cache statique reste raisonnablement frais.\u003C/p>\n\u003C/li>\n\u003C/ol>\n\u003Cp>\u003Cstrong>Les résultats après 8 semaines\u003C/strong> : le nombre de pages indexées est passé de 3 800 à 16 400. Le trafic organique sur les pages de match a augmenté de 340%. Les rich results (données structurées \u003Ccode>SportsEvent\u003C/code>) ont commencé à apparaître, ce qui était impossible quand les données n'étaient pas dans le HTML initial.\u003C/p>\n\u003Cp>Le point critique : aucune fonctionnalité temps réel n'a été sacrifiée. L'utilisateur voyait toujours les cotes se mettre à jour en live. Seule la source de l'état initial a changé — de WebSocket vers HTTP.\u003C/p>\n\u003Ch2>Données structurées et contenu WebSocket\u003C/h2>\n\u003Cp>Les données structurées (JSON-LD, microdata) posent un problème spécifique avec le contenu WebSocket. Google parse le JSON-LD présent dans le HTML rendu au moment du crawl. Si vos données structurées sont générées dynamiquement par du JavaScript qui dépend d'une connexion WebSocket, elles seront absentes.\u003C/p>\n\u003Cp>La règle est absolue : \u003Cstrong>les données structurées doivent être dans le HTML initial ou générées par du JavaScript qui s'exécute sans dépendance réseau persistante.\u003C/strong>\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;!-- Injecté côté serveur dans le &#x3C;head> -->\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\": \"Action LVMH\",\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\">    \"price\": \"742.30\",\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    \"priceCurrency\": \"EUR\",\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\">    \"priceValidUntil\": \"2026-04-10\"\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>\u003C/code>\u003C/pre>\n\u003Cp>Ce JSON-LD est rendu côté serveur avec la dernière valeur connue. Quand le WebSocket met à jour le prix côté client, vous pouvez aussi mettre à jour le JSON-LD dans le DOM — mais cette mise à jour ne sera visible que par les utilisateurs, pas par Googlebot.\u003C/p>\n\u003Cp>Un piège fréquent : les SPA qui insèrent le \u003Ccode>&#x3C;script type=\"application/ld+json\">\u003C/code> via JavaScript après réception des données WebSocket. Le \u003Ca href=\"/blog/rendering-budget-de-google-combien-de-javascript-est-trop\">rendering budget de Google\u003C/a> est limité, et si l'insertion dépend d'un événement WebSocket qui n'arrive jamais côté WRS, les rich results disparaissent.\u003C/p>\n\u003Ch2>Tester ce que Googlebot voit réellement\u003C/h2>\n\u003Cp>Vous ne pouvez pas supposer que votre fallback fonctionne. Vous devez le vérifier systématiquement.\u003C/p>\n\u003Ch3>Search Console : URL Inspection\u003C/h3>\n\u003Cp>L'outil le plus direct. Dans Google Search Console, utilisez \"Inspecter l'URL\" > \"Tester l'URL en direct\" > \"Afficher la page testée\". Vous verrez :\u003C/p>\n\u003Cul>\n\u003Cli>Le \u003Cstrong>HTML rendu\u003C/strong> : c'est le DOM final après exécution JavaScript par le WRS. Cherchez vos données critiques (prix, descriptions, titles) dans ce HTML.\u003C/li>\n\u003Cli>La \u003Cstrong>capture d'écran\u003C/strong> : vérifiez visuellement que le contenu est présent.\u003C/li>\n\u003Cli>Les \u003Cstrong>erreurs de console\u003C/strong> : les échecs de connexion WebSocket apparaîtront ici. Vous verrez typiquement \u003Ccode>WebSocket connection to 'wss://...' failed\u003C/code>.\u003C/li>\n\u003C/ul>\n\u003Ch3>Screaming Frog en mode JavaScript rendering\u003C/h3>\n\u003Cp>Configurez Screaming Frog avec le rendering JavaScript activé (Configuration > Spider > Rendering > JavaScript). Lancez un crawl sur un échantillon de vos pages temps réel. Comparez le \"Rendered HTML\" avec le \"Raw HTML\" :\u003C/p>\n\u003Cul>\n\u003Cli>Si le contenu critique n'est que dans le Rendered HTML, vous dépendez du JavaScript — acceptable si ce JS ne requiert pas de WebSocket.\u003C/li>\n\u003Cli>Si le contenu critique n'est ni dans le Raw HTML ni dans le Rendered HTML, vous avez un problème de dépendance WebSocket.\u003C/li>\n\u003C/ul>\n\u003Ch3>Bloquer les WebSockets en local\u003C/h3>\n\u003Cp>La méthode la plus fiable pour simuler le comportement de Googlebot vis-à-vis des WebSockets :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Injectez ce script en premier dans votre page pour tester\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// (via Chrome DevTools Overrides ou un bookmarklet)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#F97583\">function\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\"> OriginalWebSocket\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> window.WebSocket;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  window.\u003C/span>\u003Cspan style=\"color:#B392F0\">WebSocket\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> function\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">url\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">protocols\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    console.\u003C/span>\u003Cspan style=\"color:#B392F0\">warn\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`[SEO Test] WebSocket bloqué : ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">url\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Simule un WebSocket qui ne se connecte jamais\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\">      readyState: \u003C/span>\u003Cspan style=\"color:#79B8FF\">3\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#6A737D\">// CLOSED\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">      send\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:#B392F0\">      close\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:#B392F0\">      addEventListener\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:#B392F0\">      removeEventListener\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:#E1E4E8\">      onopen: \u003C/span>\u003Cspan style=\"color:#79B8FF\">null\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      onclose: \u003C/span>\u003Cspan style=\"color:#79B8FF\">null\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      onerror: \u003C/span>\u003Cspan style=\"color:#79B8FF\">null\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      onmessage: \u003C/span>\u003Cspan style=\"color:#79B8FF\">null\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\">  };\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  window.WebSocket.\u003C/span>\u003Cspan style=\"color:#79B8FF\">CONNECTING\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  window.WebSocket.\u003C/span>\u003Cspan style=\"color:#79B8FF\">OPEN\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  window.WebSocket.\u003C/span>\u003Cspan style=\"color:#79B8FF\">CLOSING\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 2\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  window.WebSocket.\u003C/span>\u003Cspan style=\"color:#79B8FF\">CLOSED\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 3\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  console.\u003C/span>\u003Cspan style=\"color:#B392F0\">warn\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'[SEO Test] WebSocket intercepté — simulation mode Googlebot'\u003C/span>\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>Rechargez votre page avec ce script injecté. Ce que vous voyez est une approximation de ce que Googlebot voit. Si le contenu essentiel est absent, votre fallback SSR ne fonctionne pas correctement.\u003C/p>\n\u003Ch2>Gestion du crawl budget pour les pages temps réel\u003C/h2>\n\u003Cp>Les pages à contenu temps réel posent un problème spécifique de crawl budget. Si vous avez 18 000 pages de cotes sportives qui changent toutes les 30 secondes, Google ne les recrawlera pas à cette fréquence. Acceptez-le : le contenu indexé sera toujours un snapshot daté.\u003C/p>\n\u003Ch3>Signaler la fraîcheur sans gaspiller le crawl budget\u003C/h3>\n\u003Cp>Utilisez \u003Ccode>lastmod\u003C/code> dans votre sitemap de manière intelligente. Ne mettez pas à jour le \u003Ccode>lastmod\u003C/code> à chaque micro-changement de prix — Google finira par ignorer votre sitemap s'il détecte que les dates changent constamment sans modification substantielle du contenu (cf. la \u003Ca href=\"https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap\">documentation Google sur les sitemaps\u003C/a>).\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;!-- sitemap-matches.xml -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">urlset\u003C/span>\u003Cspan style=\"color:#B392F0\"> xmlns\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"http://www.sitemaps.org/schemas/sitemap/0.9\"\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\">url\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\">loc\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>https://paris-sportifs.example.com/match/psg-marseille-2026-04-12&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">loc\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    &#x3C;!-- lastmod = dernière modification structurelle (nouveau commentaire, changement de statut) -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    &#x3C;!-- PAS la dernière mise à jour de cote -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">lastmod\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>2026-04-09T14:30:00+02:00&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">lastmod\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\">changefreq\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>daily&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">changefreq\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\">url\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\">urlset\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Pour \u003Ca href=\"/blog/mesurer-l-impact-seo-technique-quels-kpis-suivre\">suivre l'impact de ces optimisations\u003C/a>, monitorez le ratio pages crawlées/pages indexées dans Search Console et via l'\u003Ca href=\"/blog/search-console-api-automatiser-le-reporting-seo\">API Search Console\u003C/a>.\u003C/p>\n\u003Ch3>Headers de cache pour différencier bots et utilisateurs\u003C/h3>\n\u003Cp>Votre CDN peut servir des stratégies de cache différentes aux crawlers et aux utilisateurs. Ce n'est pas du cloaking — c'est de l'optimisation de delivery légitime, tant que le contenu est le même.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Configuration Nginx — cache plus long pour les bots\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Le contenu servi est identique, seule la durée de cache change\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">map\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> $\u003C/span>\u003Cspan style=\"color:#FFAB70\">http_user_agent\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> $cache_control {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    ~*\u003C/span>\u003Cspan style=\"color:#E1E4E8\">Googlebot     \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"public, max-age=3600, s-maxage=3600\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    ~*\u003C/span>\u003Cspan style=\"color:#E1E4E8\">bingbot       \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"public, max-age=3600, s-maxage=3600\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    default\u003C/span>\u003Cspan style=\"color:#9ECBFF\">         \"public, max-age=30, s-maxage=30\"\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\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">server\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    location\u003C/span>\u003Cspan style=\"color:#B392F0\"> /match/ \u003C/span>\u003Cspan style=\"color:#E1E4E8\">{\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        add_header \u003C/span>\u003Cspan style=\"color:#E1E4E8\">Cache-Control $cache_control;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">        # Le SSR sert toujours le HTML complet avec les dernières données HTTP\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        proxy_pass \u003C/span>\u003Cspan style=\"color:#E1E4E8\">http://nextjs-backend;\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>Cette approche s'inscrit dans une logique d'\u003Ca href=\"/blog/edge-seo-modifier-les-reponses-http-au-niveau-cdn\">edge SEO\u003C/a> où le CDN joue un rôle actif dans l'optimisation du delivery pour les crawlers.\u003C/p>\n\u003Ch2>Server-Sent Events : une alternative plus SEO-friendly ?\u003C/h2>\n\u003Cp>Les Server-Sent Events (SSE) utilisent une connexion HTTP standard (\u003Ccode>Content-Type: text/event-stream\u003C/code>). Contrairement aux WebSockets, c'est du HTTP pur — unidirectionnel, du serveur vers le client.\u003C/p>\n\u003Cp>En théorie, SSE est plus compatible avec le modèle de Googlebot puisque c'est une requête HTTP. En pratique, le WRS ne consomme pas non plus les flux SSE, car il n'attend pas indéfiniment une réponse HTTP qui ne se termine jamais.\u003C/p>\n\u003Cp>Le même pattern de fallback s'applique : SSR pour l'état initial, SSE pour les updates client. L'avantage de SSE sur WebSocket est la reconnexion automatique native et la compatibilité avec les proxies HTTP classiques. Mais pour le SEO, le résultat est identique : vous devez fournir le contenu initial via SSR.\u003C/p>\n\u003Cp>Le vrai avantage de SSE est architectural — il simplifie la stack quand vous n'avez pas besoin de communication bidirectionnelle. Moins de complexité = moins de chances d'introduire une régression où le fallback HTTP cesse de fonctionner et où le contenu disparaît silencieusement de l'index.\u003C/p>\n\u003Ch2>Monitoring : détecter les régressions avant Google\u003C/h2>\n\u003Cp>Le scénario cauchemar : un déploiement modifie le composant React, le fallback SSR casse, et pendant 3 semaines vos pages de produit sont indexées sans prix ni description. Vous ne le découvrez qu'en voyant le trafic organique chuter.\u003C/p>\n\u003Ch3>Intégrer les checks dans le CI/CD\u003C/h3>\n\u003Cp>Avant chaque déploiement, validez que le HTML rendu côté serveur contient les éléments critiques. Un test simple mais efficace :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// tests/seo/websocket-fallback.test.ts\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { describe, it, expect } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'vitest'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> CRITICAL_PAGES\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  '/match/psg-marseille-2026-04-12'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  '/stock/LVMH'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  '/product/sneakers-limited-edition-42'\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\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">describe\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'SSR Fallback — contenu sans WebSocket'\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\">  for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> path\u003C/span>\u003Cspan style=\"color:#F97583\"> of\u003C/span>\u003Cspan style=\"color:#79B8FF\"> CRITICAL_PAGES\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    it\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">path\u003C/span>\u003Cspan style=\"color:#9ECBFF\">} contient les données critiques dans le HTML SSR`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#F97583\">async\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\">      // Requête HTTP simple — simule un crawler\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> response\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`http://localhost:3000${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">path\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        headers: { \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'User-Agent'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'SEO-Test-Bot/1.0'\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\">      const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> html\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> response.\u003C/span>\u003Cspan style=\"color:#B392F0\">text\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">      // Le prix/la cote doit être dans le HTML initial\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">      expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(html).\u003C/span>\u003Cspan style=\"color:#B392F0\">toMatch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\d\u003C/span>\u003Cspan style=\"color:#F97583\">+\u003C/span>\u003Cspan style=\"color:#79B8FF\">[\u003C/span>\u003Cspan style=\"color:#85E89D;font-weight:bold\">\\.\u003C/span>\u003Cspan style=\"color:#79B8FF\">,]\\d\u003C/span>\u003Cspan style=\"color:#F97583\">{2}\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\s\u003C/span>\u003Cspan style=\"color:#F97583\">*\u003C/span>\u003Cspan style=\"color:#DBEDFF\">€\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">      // Le JSON-LD doit être présent\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">      expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(html).\u003C/span>\u003Cspan style=\"color:#B392F0\">toContain\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\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">      // Pas de placeholder vide\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">      expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(html).not.\u003C/span>\u003Cspan style=\"color:#B392F0\">toContain\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Loading...'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">      expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(html).not.\u003C/span>\u003Cspan style=\"color:#B392F0\">toContain\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Chargement en cours'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">      // Le title ne doit pas être générique\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> titleMatch\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> html.\u003C/span>\u003Cspan style=\"color:#B392F0\">match\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\u003C/span>\u003Cspan style=\"color:#DBEDFF\">&#x3C;title>(\u003C/span>\u003Cspan style=\"color:#79B8FF\">.\u003C/span>\u003Cspan style=\"color:#F97583\">*?\u003C/span>\u003Cspan style=\"color:#DBEDFF\">)&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D;font-weight:bold\">\\/\u003C/span>\u003Cspan style=\"color:#DBEDFF\">title>\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">      expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(titleMatch).\u003C/span>\u003Cspan style=\"color:#B392F0\">toBeTruthy\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">      expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(titleMatch\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">[\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]).not.\u003C/span>\u003Cspan style=\"color:#B392F0\">toBe\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Mon Site'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">      expect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(titleMatch\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">[\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].\u003C/span>\u003Cspan style=\"color:#79B8FF\">length\u003C/span>\u003Cspan style=\"color:#E1E4E8\">).\u003C/span>\u003Cspan style=\"color:#B392F0\">toBeGreaterThan\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#79B8FF\">20\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\">  }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">});\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Ce type de test s'intègre directement dans votre \u003Ca href=\"/blog/automatiser-les-checks-seo-dans-le-ci-cd\">pipeline CI/CD\u003C/a>. Un test qui échoue bloque le déploiement — mieux vaut retarder une release de 2 heures que perdre 3 semaines d'indexation.\u003C/p>\n\u003Ch3>Monitoring continu en production\u003C/h3>\n\u003Cp>Les tests CI/CD couvrent le pré-déploiement. En production, vous avez besoin d'un monitoring continu qui vérifie que le contenu SSR reste intact. Un outil de monitoring comme Seogard détecte automatiquement les régressions de contenu — une meta description qui disparaît, un prix qui n'est plus rendu côté serveur, un JSON-LD manquant — et alerte avant que l'impact organique ne se matérialise.\u003C/p>\n\u003Cp>La combinaison des deux couches — tests CI/CD pour prévenir les régressions au déploiement, monitoring continu pour attraper les problèmes en production (API down, timeout serveur, changement de comportement côté CDN) — est ce qui différencie les équipes qui maintiennent leurs positions organiques de celles qui jouent à la roulette russe à chaque sprint.\u003C/p>\n\u003Ch2>Réconcilier temps réel et crawlabilité\u003C/h2>\n\u003Cp>Le WebSocket est un outil de delivery, pas une source de vérité SEO. Chaque donnée que vous voulez indexer doit exister en HTTP. Le SSR fournit le snapshot crawlable, le WebSocket fournit la fraîcheur pour l'utilisateur. Ce sont deux couches complémentaires, pas interchangeables.\u003C/p>\n\u003Cp>La discipline technique qui en découle — maintenir une API REST miroir, tester le rendu sans WebSocket, monitorer les régressions de contenu en continu — est le prix à payer pour combiner interactivité temps réel et visibilité organique. Ce prix est modeste comparé à celui de découvrir, après un trimestre, que la moitié de vos pages n'étaient pas indexées.\u003C/p>",null,12,[18,19,20,21,22],"websocket","temps-réel","seo","rendering","indexation","WebSocket et SEO : rendre le temps réel indexable","Fri Apr 10 2026 20:02:09 GMT+0000 (Coordinated Universal Time)",[26,40],{"_id":27,"slug":28,"__v":6,"author":7,"canonical":29,"category":10,"createdAt":30,"date":12,"description":31,"image":15,"imageAlt":15,"readingTime":16,"tags":32,"title":38,"updatedAt":39},"69d8e6d7aa6b273b0c603186","rendering-budget-de-google-combien-de-javascript-est-trop","https://seogard.io/blog/rendering-budget-de-google-combien-de-javascript-est-trop","2026-04-10T12:02:31.965Z","Analyse technique des limites de rendering JavaScript de Googlebot : seuils, mesures concrètes et stratégies pour garder vos pages indexables.",[33,34,35,36,37],"rendering-budget","javascript","googlebot","limites","SEO technique","Rendering budget Google : combien de JS est trop pour Googlebot","Fri Apr 10 2026 12:02:31 GMT+0000 (Coordinated Universal Time)",{"_id":41,"slug":42,"__v":6,"author":7,"canonical":43,"category":10,"createdAt":44,"date":12,"description":45,"image":15,"imageAlt":15,"readingTime":16,"tags":46,"title":50,"updatedAt":51},"69d91f09aa6b273b0c8d2160","service-workers-et-seo-cache-offline-vs-crawlabilite","https://seogard.io/blog/service-workers-et-seo-cache-offline-vs-crawlabilite","2026-04-10T16:02:17.242Z","Comment les service workers impactent l'indexation Google. Stratégies de cache, pièges de crawlabilité et configurations pour concilier PWA et SEO.",[47,48,49,20,34],"service-worker","pwa","cache","Service Workers et SEO : cache offline vs crawlabilité","Fri Apr 10 2026 16:02:17 GMT+0000 (Coordinated Universal Time)"]