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-snapjson
// 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.

