Aller au contenu principal
    IT INNOVE - Logo de l'agence de développement web et mobile
    Retour au blog
    SEO

    SPA et SEO: résoudre l'indexation des applications React

    Melvin Poutrel
    9 min de lecture

    Server-Side Rendering, pré-rendu et stratégies hybrides pour un SEO optimal.

    SPA et SEO: résoudre l'indexation des applications React - SEO | IT INNOVE
    SPA et SEO: résoudre l'indexation des applications React - SEO

    Les Single Page Applications offrent une UX fluide mais posent des défis SEO majeurs. Voici comment concilier performance côté client et visibilité moteurs de recherche.

    Le problème: JavaScript invisible pour les bots

    html
    <!-- Ce que voit Googlebot sur une SPA mal optimisée -->
    <html>
    <head>
      <title>Loading...</title>
    </head>
    <body>
      <div id="root"></div>
      <script src="/app.js"></script>
    </body>
    </html>

    Conséquences mesurées:

    • 0% de pages indexées sur Google
    • Aucun snippet enrichi
    • CTR organique proche de zéro

    Solutions architecturales

    1. Server-Side Rendering (SSR)

    Next.js App Router

    jsx
    // app/products/[slug]/page.tsx
    export async function generateMetadata({ params }) {
      const product = await fetchProduct(params.slug);
      
      return {
        title: `${product.name} - ${product.category}`,
        description: product.description.slice(0, 155),
        openGraph: {
          title: product.name,
          description: product.description,
          images: [product.imageUrl],
        },
        jsonLd: {
          '@context': 'https://schema.org',
          '@type': 'Product',
          name: product.name,
          description: product.description,
          offers: {
            '@type': 'Offer',
            price: product.price,
            priceCurrency: 'EUR'
          }
        }
      };
    }
    
    export default async function ProductPage({ params }) {
      const product = await fetchProduct(params.slug);
      
      return (
        <article>
          <h1>{product.name}</h1>
          <div itemScope itemType="http://schema.org/Product">
            <h2 itemProp="name">{product.name}</h2>
            <p itemProp="description">{product.description}</p>
            <span itemProp="price">{product.price}€</span>
          </div>
          
          {/* Composant client pour l'interactivité */}
          <AddToCartButton productId={product.id} />
        </article>
      );
    }

    2. Static Site Generation (SSG)

    jsx
    // Gatsby ou Next.js getStaticProps
    export async function getStaticPaths() {
      const products = await fetchAllProducts();
      
      return {
        paths: products.map(product => ({
          params: { slug: product.slug }
        })),
        fallback: 'blocking' // ISR pour nouveaux produits
      };
    }
    
    export async function getStaticProps({ params }) {
      const product = await fetchProduct(params.slug);
      
      return {
        props: { product },
        revalidate: 3600 // Re-build toutes les heures
      };
    }

    3. Prerendering avec React Snap

    bash
    npm install --save-dev react-snap
    json
    // package.json
    {
      "scripts": {
        "postbuild": "react-snap"
      },
      "reactSnap": {
        "puppeteerArgs": ["--no-sandbox"],
        "include": [
          "/",
          "/products",
          "/about",
          "/contact"
        ],
        "asyncScriptTags": true,
        "inlineCss": true
      }
    }

    4. Stratégie hybride: Critical Path SSR

    jsx
    // Route critique SEO = SSR
    // Route interactive = CSR
    const routes = [
      { path: '/', component: HomePage, ssr: true },
      { path: '/products/:slug', component: ProductPage, ssr: true },
      { path: '/dashboard', component: Dashboard, ssr: false }, // App-like
      { path: '/checkout', component: Checkout, ssr: false }   // Pas indexé
    ];
    
    // Configuration Vite/Webpack
    export default {
      build: {
        rollupOptions: {
          input: {
            // Bundle séparé pour les routes SSR
            'entry-ssr': './src/entry-ssr.jsx',
            'entry-client': './src/entry-client.jsx'
          }
        }
      }
    };

    Optimisations techniques avancées

    Structured Data automatisé

    jsx
    // Hook pour générer JSON-LD
    function useStructuredData(type, data) {
      useEffect(() => {
        const script = document.createElement('script');
        script.type = 'application/ld+json';
        
        const structuredData = {
          '@context': 'https://schema.org',
          '@type': type,
          ...data
        };
        
        script.textContent = JSON.stringify(structuredData);
        document.head.appendChild(script);
        
        return () => document.head.removeChild(script);
      }, [type, data]);
    }
    
    // Usage dans un composant
    function ArticlePage({ article }) {
      useStructuredData('Article', {
        headline: article.title,
        author: { '@type': 'Person', name: article.author },
        datePublished: article.date,
        image: article.imageUrl
      });
      
      return <article>...</article>;
    }

    Critical CSS extraction

    js
    // critical-css-plugin.js
    const critical = require('critical');
    
    export default function criticalCssPlugin() {
      return {
        name: 'critical-css',
        generateBundle(options, bundle) {
          // Extraire le CSS critique pour chaque route
          const routes = ['/products/', '/about/', '/contact/'];
          
          routes.forEach(async (route) => {
            const { css } = await critical.generate({
              base: 'dist/',
              src: route,
              width: 1300,
              height: 900,
              inline: true
            });
            
            // Injecter dans le HTML de la route
            bundle[`${route}/index.html`].code = 
              bundle[`${route}/index.html`].code.replace(
                '</head>', 
                `<style>${css}</style></head>`
              );
          });
        }
      };
    }

    Monitoring SEO technique

    Core Web Vitals en production

    js
    // web-vitals-reporter.js
    import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
    
    function sendToAnalytics(metric) {
      fetch('/api/analytics', {
        method: 'POST',
        body: JSON.stringify({
          page: window.location.pathname,
          metric: metric.name,
          value: metric.value,
          id: metric.id
        })
      });
    }
    
    // Mesurer tous les CWV
    getCLS(sendToAnalytics);
    getFID(sendToAnalytics);
    getFCP(sendToAnalytics);
    getLCP(sendToAnalytics);
    getTTFB(sendToAnalytics);

    Indexation monitoring

    js
    // scripts/check-indexation.js
    const puppeteer = require('puppeteer');
    
    async function checkIndexation(urls) {
      const browser = await puppeteer.launch();
      const results = [];
      
      for (const url of urls) {
        const page = await browser.newPage();
        
        // Simuler Googlebot
        await page.setUserAgent('Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)');
        await page.goto(url, { waitUntil: 'networkidle2' });
        
        // Vérifier le contenu rendu
        const title = await page.title();
        const h1 = await page.$eval('h1', el => el.textContent).catch(() => null);
        const metaDesc = await page.$eval('meta[name="description"]', el => el.content).catch(() => null);
        
        results.push({
          url,
          title: title !== 'Loading...' ? title : '❌ No title',
          h1: h1 || '❌ No H1',
          metaDescription: metaDesc || '❌ No meta description',
          isIndexable: title !== 'Loading...' && h1 && metaDesc
        });
        
        await page.close();
      }
      
      await browser.close();
      return results;
    }
    
    // Usage
    checkIndexation([
      'https://monsite.com/',
      'https://monsite.com/products/chaussures-sport',
      'https://monsite.com/about'
    ]).then(results => {
      console.table(results);
      
      const issues = results.filter(r => !r.isIndexable);
      if (issues.length > 0) {
        console.error(`⚠️  ${issues.length} pages non-indexables détectées`);
        process.exit(1);
      }
    });

    Cas d'usage par architecture

    E-commerce (10k+ produits)

    Solution: Next.js ISR + Edge caching

    • Pages produits: SSG avec revalidation
    • Catégories: SSR pour le SEO
    • Panier/compte: CSR pour la fluidité
    js
    // next.config.js
    module.exports = {
      experimental: {
        isrMemoryCacheSize: 0, // Disable default ISR cache
      },
      async rewrites() {
        return [
          {
            source: '/products/:slug',
            destination: '/api/products/:slug' // API route with caching
          }
        ];
      }
    };

    Blog/contenu (1k articles)

    Solution: Gatsby + Netlify

    • Build time: 2-3 minutes
    • SEO parfait out-of-the-box
    • CDN global inclus

    SaaS Dashboard

    Solution: Stratégie hybride

    • Landing pages: SSG (marketing)
    • Documentation: SSG (SEO)
    • App dashboard: CSR (UX)

    Métriques de succès

    Avant optimisation SEO

    • Pages indexées: 12/500 (2.4%)
    • Organic CTR: 0.8%
    • Avg. position: 47

    Après implémentation SSR

    • Pages indexées: 487/500 (97.4%)
    • Organic CTR: 4.2% (+425%)
    • Avg. position: 12 (-74%)
    • Trafic organique: +340% en 6 mois

    Checklist SEO technique SPA

    • [ ] HTML complet côté serveur
    • [ ] Temps de réponse < 200ms
    • [ ] Critical CSS inline
    • [ ] Meta tags dynamiques
    • [ ] Structured data JSON-LD
    • [ ] URLs propres et crawlables
    • [ ] Sitemap XML automatique
    • [ ] Robots.txt optimisé
    • [ ] Core Web Vitals < seuils Google
    • [ ] Monitoring indexation en continu
    La clé du succès
    choisir la bonne architecture selon vos contraintes business et techniques, puis optimiser méthodiquement chaque élément.

    Questions fréquentes