Meta tags dynamiques pour React SPA sur Vercel - Guide complet
Productivite & Outils

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.

6 février 2026
10 min de lecture
Par Tokam Darius

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.

Développeur devant un écran affichant un aperçu social vide

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 :

html
<!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 :

  1. Un bot Facebook hit /article/mon-super-article
  2. L'Edge Function intercepte la requête
  3. Elle génère les meta tags dynamiquement basés sur l'URL
  4. Elle renvoie le HTML modifié avec les bons meta tags
  5. Le bot voit les meta tags, indexe correctement

Création du middleware Vercel :

Crée un fichier middleware.js à la racine de ton projet :

javascript
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).*)',
  ],
}

Salle de serveurs illustrant le calcul en périphérie

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 :

javascript
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 backend
  • API_TOKEN : Token d'authentification
  • DATABASE_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 :

javascript
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 :

javascript
image = `https://housing-ai.vercel.app/api/og?logement=${logementId}`

Cartes d'aperçu social affichées sur un téléphone

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

javascript
// 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 :

javascript
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

javascript
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 :

bash
# 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 :

javascript
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 :

javascript
// 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 :

javascript
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 :

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

Tableau de bord de statistiques de performance et d'engagement

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
javascript
// 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

Django
Erreurs
MVP
SEO
Trafic
Validation

A lire aussi

T

Tokam Darius

Développeur web à Jonquière, au Saguenay. Je conçois et je code des applications web sur mesure en Django, React et TypeScript.