SPA et page blanche sur Google : diagnostic et solutions

Un e-commerce de 12 000 fiches produits sous Angular perd 68 % de son trafic organique en trois semaines après une refonte front-end. Dans la Search Console, le rapport de couverture affiche des milliers de pages "Discovered – currently not indexed". Le test d'URL en direct révèle un <div id="app"></div> vide. Googlebot voit littéralement une page blanche. Ce scénario se produit chaque semaine sur des sites qui basculent vers une Single Page Application sans mesurer l'impact sur le rendering côté moteur.

Ce que Googlebot voit réellement quand il crawle une SPA

Googlebot utilise une version de Chromium headless (basée sur la dernière version stable de Chrome, mise à jour régulièrement — source Google) pour exécuter le JavaScript. Mais le processus se décompose en deux phases distinctes, et c'est là que tout se joue.

La file d'attente de rendering

Google sépare le crawl du rendering. Lors du premier passage, Googlebot récupère le HTML brut — exactement ce que curl renverrait. Si votre SPA sert un shell vide, c'est ce shell vide qui est indexé dans un premier temps. Le JavaScript est placé dans une file d'attente de rendering (render queue) qui sera traitée plus tard, parfois quelques secondes après, parfois plusieurs jours.

Le problème n'est pas que Googlebot "ne peut pas" exécuter JavaScript. Il le peut. Le problème est triple :

  1. Le délai entre crawl et rendering — pendant ce délai, la page est indexée avec son contenu HTML initial (souvent vide pour une SPA pure).
  2. Le budget de rendering — Google alloue des ressources limitées au rendering JS. Un site de 12 000 pages en SPA pure consomme beaucoup plus de ressources qu'un site qui sert du HTML statique.
  3. Les erreurs silencieuses — si le JS échoue pendant le rendering (API timeout, erreur réseau, dépendance bloquée), Google indexe le résultat partiel sans vous avertir.

Vérifier ce que Google voit

Le premier réflexe : l'outil "Inspection d'URL" dans la Search Console. Cliquez sur "Tester l'URL en direct", puis examinez le HTML rendu (pas le HTML brut). Comparez les deux.

Mais cet outil a une limite : il utilise un rendering frais, en temps réel, qui ne reflète pas forcément les conditions de la render queue en production. Un test plus fiable consiste à vérifier le cache Google ou à utiliser site: suivi de l'URL pour voir le snippet affiché — s'il est vide ou incohérent, le rendering a échoué.

En CLI, vous pouvez simuler un crawl sans JS avec curl pour voir le HTML initial :

# Récupérer le HTML brut tel que servi par le serveur (sans exécution JS)
curl -A "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" \
  -s https://www.votre-ecommerce.fr/produit/chaussure-running-x200 \
  | grep -A 5 '<div id="app"'

Si la sortie ressemble à ça, vous avez un problème :

<div id="app">
  <!-- contenu injecté par JavaScript -->
</div>

Aucun contenu, aucune balise <h1>, aucune meta description exploitable. C'est exactement ce que Googlebot reçoit lors de la phase de crawl initial.

Les cinq causes techniques d'une page blanche pour Googlebot

1. Ressources JavaScript bloquées par robots.txt

C'est la cause la plus idiote et la plus fréquente. Si vos fichiers JS ou CSS sont bloqués dans robots.txt, Googlebot ne peut pas les charger, donc ne peut pas exécuter le rendering. Vérifiez :

# robots.txt — configuration DANGEREUSE pour une SPA
User-agent: *
Disallow: /static/
Disallow: /assets/js/

Si vos bundles JS sont servis depuis /static/ ou /assets/js/, Googlebot ne les téléchargera jamais. Le test de robots.txt dans la Search Console permet de vérifier URL par URL. Screaming Frog, en mode "JavaScript rendering", signale aussi les ressources bloquées dans l'onglet "Blocked Resources".

2. Erreurs JavaScript qui cassent silencieusement le rendering

Googlebot exécute votre JS dans un environnement Chromium, mais sans interaction utilisateur. Tout code qui dépend d'un click, d'un scroll, ou d'un IntersectionObserver pour charger du contenu critique ne sera jamais déclenché.

Plus vicieux : les erreurs réseau. Si votre SPA fait un fetch() vers une API qui met plus de 5 secondes à répondre, ou qui retourne une erreur CORS dans le contexte de Googlebot, le contenu ne sera jamais injecté dans le DOM.

Ouvrez Chrome DevTools, onglet Console, en mode "throttling" réseau sur "Slow 3G", et désactivez le cache. Si votre page met plus de 5 secondes à afficher du contenu, Googlebot risque d'abandonner le rendering.

3. Dépendance à des APIs authentifiées ou géo-restreintes

Googlebot crawle depuis des IPs Google (principalement US). Si votre API de contenu filtre par géolocalisation ou exige un token d'authentification que le serveur injecte côté client via un cookie de session, Googlebot ne recevra jamais les données.

4. Lazy loading agressif sur le contenu principal

Le lazy loading d'images avec loading="lazy" est parfaitement géré par Googlebot. Mais le lazy loading de blocs de contenu textuels via IntersectionObserver (pattern courant sur les pages "infinite scroll") pose un vrai problème : Googlebot ne scrolle pas. Le contenu sous la fold initiale ne sera jamais rendu.

5. Hydration errors en framework SSR mal configuré

Ce cas est plus subtil. Vous avez migré vers Next.js ou Nuxt en pensant régler le problème, mais le HTML rendu côté serveur et le HTML produit côté client divergent. React détecte un "hydration mismatch" et recrée le DOM entièrement côté client, ce qui peut temporairement produire un flash de contenu vide. Si Googlebot capture ce moment, c'est la page blanche.

Scénario réel : migration SPA vers SSR sur un catalogue de 15 000 pages

Prenons un cas concret. Un site e-commerce de matériel de sport — 15 000 fiches produits, 800 pages catégories — tourne sur une SPA React avec React Router. Le rendu est 100 % client-side. Le site est en ligne depuis 2 ans et génère 45 000 visites organiques par mois.

Le diagnostic initial

  • Search Console : 9 200 pages dans l'index sur 15 800 attendues. 4 100 pages en "Discovered – currently not indexed". 2 500 pages en "Crawled – currently not indexed".
  • Screaming Frog (mode JS rendering) : 35 % des pages retournent un <title> vide ou générique ("Mon Site | Chargement...").
  • Test curl : le HTML servi est un shell de 1,2 Ko identique pour toutes les URLs. Tout le contenu dépend de l'exécution de 2,8 Mo de bundles JS.
  • Web Vitals : LCP moyen à 6,2 secondes sur mobile — le contenu n'apparaît qu'après le chargement complet du JS + l'appel API catalogue.

La migration

L'équipe migre vers Next.js avec App Router et generateStaticParams pour les fiches produits. Les pages catégories utilisent le server-side rendering dynamique (SSR) pour gérer les filtres et la pagination.

La config clé dans next.config.js :

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Forcer le SSR pour les pages dynamiques, ISR pour les fiches produits
  experimental: {
    // Timeout de rendering côté serveur — crucial pour éviter les pages blanches
    // si l'API catalogue est lente
    serverComponentsExternalPackages: ['@catalogue/sdk'],
  },
  async headers() {
    return [
      {
        // Cache CDN pour les fiches produits (ISR)
        source: '/produit/:slug*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, s-maxage=3600, stale-while-revalidate=86400',
          },
        ],
      },
    ];
  },
};

module.exports = nextConfig;

Et la page produit en Server Component :

// app/produit/[slug]/page.tsx
import { Metadata } from 'next';
import { getProduct } from '@/lib/catalogue';
import { notFound } from 'next/navigation';

interface Props {
  params: { slug: string };
}

// Génération statique des 15 000 fiches produits au build
export async function generateStaticParams() {
  const products = await getAllProductSlugs();
  return products.map((slug) => ({ slug }));
}

// Meta tags générées côté serveur — toujours présentes dans le HTML initial
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const product = await getProduct(params.slug);
  if (!product) return {};

  return {
    title: `${product.name} — ${product.brand} | SportGear`,
    description: product.shortDescription.slice(0, 155),
    alternates: {
      canonical: `https://www.sportgear.fr/produit/${params.slug}`,
    },
  };
}

export default async function ProductPage({ params }: Props) {
  const product = await getProduct(params.slug);
  if (!product) notFound();

  return (
    <main>
      <h1>{product.name}</h1>
      <p className="brand">{product.brand}</p>
      <div className="description"
        dangerouslySetInnerHTML={{ __html: product.htmlDescription }}
      />
      {/* Le contenu critique est dans le HTML initial — pas de JS requis */}
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{
          __html: JSON.stringify({
            '@context': 'https://schema.org',
            '@type': 'Product',
            name: product.name,
            description: product.shortDescription,
            brand: { '@type': 'Brand', name: product.brand },
            offers: {
              '@type': 'Offer',
              price: product.price,
              priceCurrency: 'EUR',
              availability: product.inStock
                ? 'https://schema.org/InStock'
                : 'https://schema.org/OutOfStock',
            },
          }),
        }}
      />
    </main>
  );
}

Les résultats après 8 semaines

  • Pages indexées : de 9 200 à 14 800 (94 % du catalogue).
  • "Discovered – currently not indexed" : de 4 100 à 320.
  • Trafic organique : de 45 000 à 72 000 visites/mois (+60 %).
  • LCP mobile : de 6,2 s à 1,8 s.

Le gain ne vient pas uniquement du rendering. Le HTML initial contient désormais les balises title, description, h1, le structured data — tout ce dont Google a besoin dès le premier passage de crawl, sans attendre la render queue.

Pour un contexte plus large sur les différences entre SSR et CSR, consultez notre analyse détaillée de l'impact réel sur le SEO.

Solutions techniques selon votre stack

Il n'existe pas une solution unique. Le choix dépend de votre stack actuelle, du nombre de pages, de la fréquence de mise à jour du contenu, et de vos contraintes d'infrastructure.

SSR dynamique (Next.js, Nuxt, Angular Universal)

Quand l'utiliser : pages avec contenu qui change fréquemment (stock, prix, contenu utilisateur), pages avec paramètres dynamiques (filtres, recherche).

Trade-off : chaque requête exécute le rendering côté serveur. Coût en infrastructure proportionnel au trafic. Si votre API catalogue met 500 ms à répondre, votre TTFB sera au minimum de 500 ms + overhead rendering.

Edge case : si votre serveur SSR tombe, toutes vos pages retournent une erreur 500. Un site en SPA pure avec un CDN devant continuait à servir le shell HTML (certes vide pour Google, mais fonctionnel pour les utilisateurs). En SSR, une panne backend = une panne totale. Prévoyez un fallback avec stale-while-revalidate ou une couche de cache agressive.

Static Site Generation (SSG) / Incremental Static Regeneration (ISR)

Quand l'utiliser : catalogues avec mises à jour peu fréquentes, blogs, documentation. ISR est idéal quand vous avez 15 000+ pages mais que seulement 5 % changent par jour.

Trade-off : le build initial peut être très long. 15 000 pages en SSG pur avec Next.js et une API à 200 ms par requête = ~50 minutes de build. ISR résout ce problème en générant les pages à la demande, mais la première visite après expiration du cache subira le coût du rendering.

Prerendering via service dédié (Prerender.io, Rendertron)

Quand l'utiliser : quand la migration vers SSR est impossible à court terme (dette technique trop importante, SPA legacy sur un framework sans solution SSR mature).

Le principe : un middleware détecte les user-agents des bots et leur sert une version pré-rendue de la page, tandis que les utilisateurs reçoivent la SPA classique.

Configuration Nginx typique :

# /etc/nginx/conf.d/prerender.conf
map $http_user_agent $is_bot {
    default 0;
    "~*googlebot"       1;
    "~*bingbot"         1;
    "~*baiduspider"     1;
    "~*yandexbot"       1;
    "~*linkedinbot"     1;
    "~*facebookexternalhit" 1;
}

server {
    listen 443 ssl;
    server_name www.sportgear.fr;

    location / {
        if ($is_bot = 1) {
            # Proxy vers le service de prerendering
            rewrite .* /render/https://$host$request_uri break;
            proxy_pass https://votre-prerender-service.example.com;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        # Utilisateurs normaux : SPA classique
        try_files $uri $uri/ /index.html;
    }
}

Trade-off critique : Google a explicitement déclaré que le cloaking (servir un contenu différent aux bots et aux utilisateurs) est une violation de ses guidelines. Mais le prerendering est toléré tant que le contenu servi au bot est identique au contenu rendu pour l'utilisateur. La nuance est importante : si votre SPA et votre version pré-rendue montrent le même contenu, vous êtes dans les clous. Si vous en profitez pour injecter du keyword stuffing dans la version bot, vous risquez une pénalité manuelle.

Référence : Google's documentation on dynamic rendering.

Note importante : Google a indiqué que le dynamic rendering est une solution temporaire ("workaround"), pas une solution à long terme. Depuis la mise à jour de la documentation en 2024, Google recommande explicitement le SSR ou le SSG plutôt que le dynamic rendering.

Hybrid rendering (React Server Components, Astro Islands)

L'approche la plus moderne. Astro et les React Server Components de Next.js App Router permettent de choisir le rendering par composant — pas par page. Le shell de navigation, les headers, le contenu textuel sont rendus côté serveur. Les composants interactifs (panier, filtres dynamiques, carrousels) sont hydratés côté client.

C'est la direction vers laquelle l'écosystème converge. Le contenu critique pour le SEO est toujours dans le HTML initial. L'interactivité est chargée progressivement.

Auditer le rendering JS à grande échelle

Screaming Frog en mode JavaScript

Screaming Frog (version desktop, licence payante) permet de crawler en activant le rendering JavaScript. La comparaison entre le crawl "HTML only" et le crawl "JavaScript rendering" est extrêmement révélatrice :

  • Comparez les colonnes Title 1 et H1-1 entre les deux modes.
  • Filtrez les pages où le title est vide en mode HTML mais présent en mode JS — ce sont vos pages à risque.
  • Vérifiez les "Rendered Page Resources" pour identifier les ressources bloquées ou en timeout.

Configuration recommandée : onglet Configuration > Spider > Rendering > JavaScript. Augmentez le "AJAX Timeout" à 10 secondes minimum si votre SPA dépend d'appels API lents.

Search Console : le rapport de couverture comme signal d'alerte

Les statuts à surveiller :

  • "Discovered – currently not indexed" en masse sur des pages qui devraient être indexées = Google n'arrive pas à les traiter, souvent parce que le rendering échoue ou que la render queue est saturée.
  • "Crawled – currently not indexed" = Google a crawlé la page mais a jugé le contenu insuffisant pour l'indexer. Sur une SPA, ça signifie souvent que le rendering a produit un contenu vide ou quasi-vide.

Monitoring continu

Le problème avec le rendering JS, c'est sa fragilité. Un déploiement front qui casse un import, un changement d'URL d'API, un certificat SSL expiré sur le CDN des assets — n'importe lequel de ces événements peut rendre vos pages blanches pour Googlebot sans que vos utilisateurs (qui ont le JS en cache) ne remarquent quoi que ce soit.

C'est exactement le type de régression qu'un outil de monitoring SEO comme SEOGard détecte en moins de 24h : une meta title qui disparaît, un H1 qui devient vide, un changement de status code — avant que l'impact sur le trafic ne devienne visible dans Analytics.

Checklist de survie pour les SPAs en production

Quelques vérifications à intégrer dans votre pipeline CI/CD :

#!/bin/bash
# Script de vérification post-deploy — à intégrer dans votre CI

TARGET_URL="https://www.sportgear.fr/produit/chaussure-running-x200"

# 1. Vérifier que le HTML initial contient du contenu (pas juste le shell)
TITLE=$(curl -s "$TARGET_URL" | grep -oP '(?<=<title>).*(?=</title>)')
if [ -z "$TITLE" ] || [ "$TITLE" = "Loading..." ] || [ "$TITLE" = "SportGear" ]; then
  echo "ERREUR: Title vide ou générique détecté: '$TITLE'"
  exit 1
fi

# 2. Vérifier la présence d'un H1
H1=$(curl -s "$TARGET_URL" | grep -oP '(?<=<h1[^>]*>).*(?=</h1>)')
if [ -z "$H1" ]; then
  echo "ERREUR: Aucun H1 trouvé dans le HTML initial"
  exit 1
fi

# 3. Vérifier que les ressources JS critiques ne sont pas bloquées par robots.txt
ROBOTS=$(curl -s "https://www.sportgear.fr/robots.txt")
if echo "$ROBOTS" | grep -q "Disallow: /_next/"; then
  echo "ERREUR: Les assets Next.js sont bloqués dans robots.txt"
  exit 1
fi

echo "OK: Title='$TITLE', H1='$H1'"

Ce script ne remplace pas un audit complet, mais il attrape les régressions les plus courantes immédiatement après un déploiement.

Au-delà du CI/CD, gardez en tête ces principes :

  • Tout contenu critique pour le SEO doit être dans le HTML initial. Si curl ne le voit pas, considérez que Google ne le verra peut-être pas non plus — ou pas avant des jours.
  • Testez avec les outils de Google, pas seulement vos outils. Le test d'URL en direct de la Search Console est le seul moyen de voir exactement ce que le renderer de Google produit.
  • Mesurez le delta entre pages découvertes et pages indexées. Un écart qui se creuse est le signe le plus fiable d'un problème de rendering à grande échelle.
  • Ne comptez pas sur le JavaScript rendering de Google comme filet de sécurité. Il fonctionne, mais avec un délai, un coût en crawl budget, et une fragilité que vous ne contrôlez pas.

Le rendering JavaScript pour le SEO est un problème résolu techniquement — SSR, SSG, ISR, hybrid rendering, les solutions existent et sont matures. Ce qui reste difficile, c'est de détecter quand ça casse. Un site qui servait du HTML complet hier peut servir des pages blanches demain après un déploiement anodin. La différence entre les équipes qui perdent 60 % de leur trafic et celles qui corrigent en 24h, c'est le monitoring.

Articles connexes

Rendering21 février 2026

Tester le rendering Google : outils et méthodes avancées

Méthodes concrètes pour valider ce que Google voit sur vos pages : Inspect URL, Puppeteer headless, diff DOM et monitoring continu du rendering SEO.

Rendering21 février 2026

Dynamic rendering : solution temporaire ou piège SEO

Avantages, limites et alternatives au dynamic rendering. Pourquoi cette approche recommandée par Google devient un risque technique à long terme.

Rendering20 février 2026

Prerendering pour le SEO : quand l'utiliser et comment l'implémenter

Guide technique du prerendering pour le SEO : cas d'usage concrets, implémentation avec Next.js, Nuxt et Prerender.io, et pièges à éviter sur les SPA.