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

    Équipe IT INNOVE
    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

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

    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