Meta tags dynamiques pour React SPA sur Vercel - Guide complet
Une SPA React sur Vercel sert un HTML vide aux robots de Facebook et LinkedIn : zéro balise Open Graph, zéro aperçu, zéro clic. Voici la solution avec les Edge Functions, du middleware à la génération d'images dynamiques, avec les trois erreurs qui m'ont coûté le plus de temps.
Quand j'ai lancé Housing AI, je pensais que la partie la plus difficile serait de scraper les annonces de logement. J'avais tort. Le vrai cauchemar, c'est quand mon app React déployée sur Vercel générait des previews sociales complètement vides sur LinkedIn et Facebook. Tu sais, ces petits carrés gris avec "No Preview Available" qui donnent l'impression que ton site date de 2005.
Le problème ? Les Single Page Applications (SPA) React chargent le contenu côté client. Quand les bots de Facebook ou LinkedIn crawlent ta page, ils ne voient que du HTML vide avec un div root. Résultat : zéro meta tag Open Graph, zéro preview sexy, zéro engagement.
Après avoir testé 6 solutions différentes et passé 3 nuits blanches, j'ai trouvé LA méthode qui fonctionne vraiment avec Vercel. Spoiler alert : les Vercel Edge Functions vont te sauver la vie.
Le problème des meta tags dans les SPA React
Laisse-moi t'expliquer pourquoi ton app React génère des previews sociales pourries.
Ton index.html ressemble probablement à ça :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mon App React</title>
<meta property="og:title" content="Mon App React" />
<meta property="og:description" content="Une description générique" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Le problème : ces meta tags sont statiques. Peu importe si l'utilisateur visite /article/comment-devenir-dev ou /profil/tokam-darius, les meta tags restent identiques.
Les bots sociaux ne font pas tourner JavaScript. Ils lisent le HTML brut, récupèrent les meta tags, et c'est fini. Si tes meta tags ne correspondent pas au contenu de la page, tu perds 80% de ton trafic social.
J'ai appris ça à mes dépens quand j'ai partagé Housing AI sur LinkedIn. Au lieu d'un beau preview avec screenshot et description, j'ai eu droit à un triste lien bleu. Zéro engagement, zéro clic.
Les solutions qui ne marchent PAS :
- React Helmet : Change les meta tags côté client, donc invisible pour les bots
- Prerender.io : Coûteux et ajoute de la latence
- Next.js SSR : Obligé de refactoriser toute ton app
Il faut une solution qui génère les meta tags côté serveur avant que les bots n'arrivent.
Les Vercel Edge Functions : la solution miracle
Vercel Edge Functions, c'est du JavaScript qui s'exécute côté serveur avant que ta page React ne charge. Perfect pour intercepter les bots sociaux et leur servir du HTML avec les bons meta tags.
Voici comment ça marche :
- Un bot Facebook hit
/article/mon-super-article - L'Edge Function intercepte la requête
- Elle génère les meta tags dynamiquement basés sur l'URL
- Elle renvoie le HTML modifié avec les bons meta tags
- Le bot voit les meta tags, indexe correctement
Création du middleware Vercel :
Crée un fichier middleware.js à la racine de ton projet :
import { NextResponse } from 'next/server'
export function middleware(request) {
const { pathname } = request.nextUrl
const userAgent = request.headers.get('user-agent') || ''
// Détecte les bots sociaux
const isBot = /facebookexternalhit|twitterbot|linkedinbot|slackbot|whatsapp/i.test(userAgent)
if (isBot) {
return generateMetaTags(pathname, request)
}
return NextResponse.next()
}
async function generateMetaTags(pathname, request) {
// Parse l'URL pour extraire les paramètres
const segments = pathname.split('/').filter(Boolean)
let title = "Housing AI - Trouvez votre logement au Québec"
let description = "Trouvez votre logement idéal au Québec grâce à l'intelligence artificielle"
let image = "https://housing-ai.vercel.app/og-default.png"
// Logique spécifique selon l'URL
if (segments[0] === 'logement' && segments[1]) {
const logementId = segments[1]
// Ici tu peux fetch les données depuis ton API
title = `Logement ${logementId} - Housing AI`
description = `Découvrez ce logement parfait pour vous`
image = `https://housing-ai.vercel.app/api/og?logement=${logementId}`
}
if (segments[0] === 'ville' && segments[1]) {
const ville = segments[1]
title = `Logements à ${ville} - Housing AI`
description = `Trouvez votre logement idéal à ${ville}`
}
const html = generateHTML(title, description, image, pathname)
return new NextResponse(html, {
headers: {
'content-type': 'text/html',
},
})
}
function generateHTML(title, description, image, pathname) {
return `
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Meta tags classiques -->
<title>${title}</title>
<meta name="description" content="${description}" />
<!-- Open Graph -->
<meta property="og:title" content="${title}" />
<meta property="og:description" content="${description}" />
<meta property="og:image" content="${image}" />
<meta property="og:url" content="https://housing-ai.vercel.app${pathname}" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Housing AI" />
<!-- Twitter Cards -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="${title}" />
<meta name="twitter:description" content="${description}" />
<meta name="twitter:image" content="${image}" />
<link rel="canonical" href="https://housing-ai.vercel.app${pathname}" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
`
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}
Configuration avancée avec base de données
Pour Housing AI, j'avais besoin de récupérer les vraies données des logements pour générer des meta tags précis. Voici comment intégrer une base de données :
async function fetchLogementData(logementId) {
try {
// Utilise ton API ou accède directement à la DB
const response = await fetch(`${process.env.API_URL}/api/logements/${logementId}`, {
headers: {
'Authorization': `Bearer ${process.env.API_TOKEN}`
}
})
if (!response.ok) return null
return await response.json()
} catch (error) {
console.error('Erreur fetch logement:', error)
return null
}
}
async function generateMetaTags(pathname, request) {
const segments = pathname.split('/').filter(Boolean)
// Defaults
let title = "Housing AI - Trouvez votre logement au Québec"
let description = "Trouvez votre logement idéal au Québec grâce à l'intelligence artificielle"
let image = "https://housing-ai.vercel.app/og-default.png"
if (segments[0] === 'logement' && segments[1]) {
const logementData = await fetchLogementData(segments[1])
if (logementData) {
title = `${logementData.titre} - ${logementData.prix}$/mois`
description = `${logementData.pieces} pièces à ${logementData.ville}. ${logementData.description.substring(0, 150)}...`
image = logementData.photos[0] || image
}
}
const html = generateHTML(title, description, image, pathname)
return new NextResponse(html, {
headers: {
'content-type': 'text/html',
},
})
}
Variables d'environnement sur Vercel :
Ajoute tes variables dans Vercel Dashboard → Settings → Environment Variables :
API_URL: L'URL de ton API backendAPI_TOKEN: Token d'authentificationDATABASE_URL: Si tu accèdes directement à la DB
Génération d'images Open Graph dynamiques
Les images, c'est ce qui fait cliquer. Une preview avec une image custom génère 3x plus de clics qu'un simple texte.
Vercel propose @vercel/og pour générer des images dynamiquement. Crée pages/api/og.js :
import { ImageResponse } from '@vercel/og'
export const config = {
runtime: 'edge',
}
export default async function handler(request) {
const { searchParams } = new URL(request.url)
const logementId = searchParams.get('logement')
const ville = searchParams.get('ville')
// Récupère les données si nécessaire
let titre = "Housing AI"
let prix = ""
let pieces = ""
if (logementId) {
const logementData = await fetchLogementData(logementId)
if (logementData) {
titre = logementData.titre
prix = `${logementData.prix}$/mois`
pieces = `${logementData.pieces} pièces`
}
}
return new ImageResponse(
(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'white',
background: 'linear-gradient(45deg, #1e3a8a, #3b82f6)',
}}
>
<div
style={{
marginLeft: 190,
marginRight: 190,
display: 'flex',
fontSize: 130,
fontFamily: 'Inter',
letterSpacing: '-0.05em',
fontStyle: 'normal',
color: 'white',
lineHeight: '120px',
whiteSpace: 'pre-wrap',
textAlign: 'center',
}}
>
{titre}
</div>
{prix && (
<div
style={{
fontSize: 60,
fontFamily: 'Inter',
color: '#fbbf24',
marginTop: 20,
}}
>
{prix}
</div>
)}
{pieces && (
<div
style={{
fontSize: 40,
fontFamily: 'Inter',
color: '#e5e7eb',
marginTop: 10,
}}
>
{pieces}
</div>
)}
</div>
),
{
width: 1200,
height: 630,
},
)
}
Maintenant tes meta tags peuvent pointer vers des images custom :
image = `https://housing-ai.vercel.app/api/og?logement=${logementId}`
Erreurs à éviter (j'ai tout testé)
Erreur #1 : Oublier le cache
Mon middleware faisait des appels DB à chaque crawl de bot. Résultat : 500ms de latence et des timeouts.
Solution : Cache agressif pour les bots
// Cache les données 1h pour les bots
const CACHE_TTL = 3600 // 1 heure
const cachedData = await getCachedLogement(logementId)
if (cachedData) {
// Utilise le cache
} else {
// Fetch et cache
const freshData = await fetchLogementData(logementId)
await setCachedLogement(logementId, freshData, CACHE_TTL)
}
Erreur #2 : Meta tags trop longs
Facebook coupe à 88 caractères pour le titre, LinkedIn à 95. J'ai eu des titres tronqués partout.
Solution :
function sanitizeTitle(title, maxLength = 88) {
if (title.length <= maxLength) return title
return title.substring(0, maxLength - 3) + '...'
}
function sanitizeDescription(desc, maxLength = 155) {
if (desc.length <= maxLength) return desc
return desc.substring(0, maxLength - 3) + '...'
}
Erreur #3 : Images qui ne chargent pas
Mes images générées renvoyaient parfois des 500. Les previews sociales affichaient des images cassées.
Solution : Fallback systématique
function getImageUrl(logementData, logementId) {
const defaultImage = "https://housing-ai.vercel.app/og-default.png"
try {
if (logementData?.photos?.[0]) {
return logementData.photos[0]
}
return `https://housing-ai.vercel.app/api/og?logement=${logementId}`
} catch (error) {
return defaultImage
}
}
Comme je l'explique dans mon article sur JWT entre Django et React, les détails qui semblent insignifiants peuvent te faire perdre des heures. Les meta tags, c'est pareil.
Test et debugging
1. Teste avec les outils officiels :
- Facebook Debugger : https://developers.facebook.com/tools/debug/
- LinkedIn Inspector : https://www.linkedin.com/post-inspector/
- Twitter Validator : https://cards-dev.twitter.com/validator
2. Simule les bots localement :
# Teste ton middleware avec un user-agent de bot
curl -H "User-Agent: facebookexternalhit/1.1" https://ton-app.vercel.app/logement/123
3. Logs Vercel Edge Functions :
export function middleware(request) {
console.log('Bot detected:', request.headers.get('user-agent'))
console.log('Path:', request.nextUrl.pathname)
// Tes logs apparaissent dans Vercel Functions
}
4. Validation HTML :
Assure-toi que ton HTML généré est valide. Les bots sont pointilleux.
Optimisation des performances
Quand j'ai déployé cette solution sur Housing AI, j'ai remarqué que les bots sociaux représentaient 15% du trafic mais 40% des appels à mon API. Il faut optimiser.
Cache stratégique :
// Cache en mémoire simple (pour dev)
const cache = new Map()
function getCachedData(key) {
const cached = cache.get(key)
if (!cached) return null
if (Date.now() > cached.expiry) {
cache.delete(key)
return null
}
return cached.data
}
function setCachedData(key, data, ttl = 3600) {
cache.set(key, {
data,
expiry: Date.now() + (ttl * 1000)
})
}
Fallback rapide :
async function fetchLogementDataWithTimeout(logementId, timeout = 2000) {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
try {
const response = await fetch(`/api/logements/${logementId}`, {
signal: controller.signal
})
clearTimeout(timeoutId)
return await response.json()
} catch (error) {
// En cas de timeout, renvoie des données génériques
return {
titre: 'Logement disponible',
prix: 'Prix à discuter',
ville: 'Québec'
}
}
}
Preload des données populaires :
Si certains logements sont souvent partagés, précharge leurs meta tags :
// Crée des meta tags statiques pour les 100 logements les plus vus
const POPULAR_LOGEMENTS = [123, 456, 789] // IDs populaires
if (POPULAR_LOGEMENTS.includes(parseInt(logementId))) {
// Utilise des données pré-calculées
return getPrecomputedMetaTags(logementId)
}
Cette approche m'a fait passer de 800ms à 150ms de réponse pour les bots. Essentiel quand Facebook timeout après 2 secondes.
Résultats concrets après implémentation
Depuis que j'ai implémenté cette solution sur Housing AI, les chiffres parlent :
Avant les meta tags dynamiques :
- Taux de clic social : 0.8%
- Shares LinkedIn : ~5 par semaine
- Engagement Facebook : Inexistant
- Trafic social : 180 visites/mois
Après implémentation :
- Taux de clic social : 4.2% (+425%)
- Shares LinkedIn : ~35 par semaine (+600%)
- Engagement Facebook : 15-20 interactions par post
- Trafic social : 850 visites/mois (+372%)
Le plus impressionnant ? Les articles de blog avec des meta tags customisés génèrent 3x plus de partages que ceux avec des meta tags génériques.
Monitoring en continu :
J'ai mis en place des alerts Vercel pour surveiller :
- Temps de réponse des Edge Functions > 500ms
- Erreurs 5xx sur
/api/og - Volume anormal de requêtes bots
// Tracking simple des performances
console.log(`Meta generation: ${Date.now() - startTime}ms for ${pathname}`)
D'ailleurs, cette approche me rappelle ce que je raconte dans mon guide pour créer un MVP rapidement : parfois, les détails techniques qu'on néglige (comme les meta tags) ont un impact business énorme.
La différence entre une preview sociale moche et une preview qui donne envie de cliquer, c'est facilement 5x plus de trafic. Pour Housing AI, ça représente 500+ utilisateurs additionnels par mois, juste grâce aux meta tags.
Mon conseil final : Ne sous-estime jamais l'impact des meta tags Open Graph. C'est 2h de dev pour des mois de trafic gratuit en plus.
Cette solution avec Vercel Edge Functions, elle fonctionne. Je l'utilise en prod depuis 8 mois sans problème. Les bots sont contents, les previews sont sexy, et mon trafic social explose.
Maintenant, va implémenter ça sur ton app React et regarde tes stats sociales décoller. Tu me remercieras dans 3 semaines quand tes partages LinkedIn généreront 10x plus de clics.
Tags
A lire aussi
Jongler avec 6 clients à la fois sans rien laisser tomber
Gérer six projets clients en solo sans échapper une démo ni brûler ses weekends tient à quatre habitudes simples : une revue du lundi, un canal unique par client, trois signaux d'alarme et un tableau de capacité honnête. Voici le système, et pourquoi il tue l'anxiété du dimanche soir.
React + TypeScript : mon stack par défaut, sans exception
Un écran blanc en prod à cause d'une réponse API qui a changé de forme : le genre de bug que TypeScript rend impossible à écrire. Pourquoi React et TypeScript sont devenus mon point de départ non négociable, avec la config exacte que je réutilise sur chaque projet.
Comment j'automatise mon workflow de développement web (et ce que je garde manuel)
Après deux ans en solo, j'ai appris que l'automatisation ne sauve personne du burnout si elle attaque les mauvaises tâches. Voici ce que j'automatise sans hésiter (tests, CI/CD, formatage), ce que je refuse de déléguer, et la grille de décision que j'applique à chaque tâche répétitive.

