Lecture : 13 minutes · Niveau : intermédiaire · Mise à jour : avril 2026
Le combo Strapi + Next.js est devenu l’une des stacks JAMstack les plus populaires. Strapi gère le contenu et l’API, Next.js rend le frontend avec des performances de pointe. Ce guide montre comment intégrer les deux proprement, du fetch de données aux fonctionnalités avancées (ISR, preview mode, optimisation images).
Voir aussi → Strapi headless CMS pour PME : guide complet et React pour PME : guide frontend pro.
Sommaire
- Architecture du combo Strapi + Next.js
- Setup du projet Next.js
- Fetch de données : Server Components vs Client Components
- Génération de pages dynamiques
- ISR et revalidation
- Preview mode pour brouillons
- Optimisation images
- Rendu rich text et dynamic zones
- Authentification utilisateurs
- Déploiement Vercel
- FAQ
1. Architecture du combo Strapi + Next.js
┌──────────────┐
│ Strapi │ Backend headless
│ - PostgreSQL │ Content + API
│ - Médias S3 │
└──────┬───────┘
│ API REST/GraphQL
│
┌──────▼───────┐
│ Next.js │ Frontend
│ - SSG │ Rendu hybride
│ - ISR │ Build statique + revalidation
│ - SSR/RSC │ Pages dynamiques
└──────┬───────┘
│
│ HTTPS
│
Visiteurs
Hébergement typique
- Strapi : VPS, Render, Railway, ou Strapi Cloud
- PostgreSQL : managé (DB du PaaS) ou auto-géré sur le VPS
- Médias : S3-compatible (Backblaze B2, Wasabi, AWS S3)
- Next.js : Vercel (recommandé), Netlify, ou auto-hébergé
- DNS : Cloudflare devant pour cache et protection
Flux de données
- Build Next.js : fetch données depuis Strapi pour générer pages statiques
- À chaque requête utilisateur : Next.js sert la page pré-rendue (rapide)
- Pour pages dynamiques : Server Components fetchent en temps réel
- Modifications de contenu côté Strapi : trigger revalidation Next.js (ISR)
2. Setup du projet Next.js
npx create-next-app@latest mon-frontend --typescript --tailwind --app --import-alias="@/*"
cd mon-frontend
Variables d’environnement
.env.local :
NEXT_PUBLIC_STRAPI_URL=http://localhost:1337
STRAPI_API_TOKEN=<token-server-side-uniquement>
STRAPI_PREVIEW_SECRET=<random>
NEXT_PUBLIC_SITE_URL=http://localhost:3000
Helper de fetch Strapi
lib/strapi.ts :
import qs from 'qs';
const STRAPI_URL = process.env.NEXT_PUBLIC_STRAPI_URL!;
const STRAPI_TOKEN = process.env.STRAPI_API_TOKEN;
interface FetchOptions {
populate?: any;
filters?: any;
sort?: string | string[];
pagination?: { page?: number; pageSize?: number };
locale?: string;
next?: { revalidate?: number; tags?: string[] };
}
export async function strapiFetch<T = any>(
endpoint: string,
options: FetchOptions = {}
): Promise<T> {
const { next, ...query } = options;
const queryString = qs.stringify(query, { encodeValuesOnly: true });
const url = `${STRAPI_URL}/api/${endpoint}${queryString ? `?${queryString}` : ''}`;
const res = await fetch(url, {
headers: {
...(STRAPI_TOKEN && { Authorization: `Bearer ${STRAPI_TOKEN}` }),
},
next,
});
if (!res.ok) throw new Error(`Strapi fetch failed: ${res.status}`);
return res.json();
}
Helper centralisé : tous les fetchs vers Strapi passent par lui. Évite la duplication et standardise le caching.
3. Fetch de données : Server Components vs Client Components
Avec Next.js App Router, deux approches.
Server Components (recommandé par défaut)
Fetch côté serveur, pas de JavaScript envoyé au client pour le data-fetching :
// app/articles/page.tsx
import { strapiFetch } from '@/lib/strapi';
export default async function ArticlesPage() {
const { data } = await strapiFetch('articles', {
populate: ['coverImage', 'author'],
sort: 'publishedAt:desc',
pagination: { pageSize: 20 },
next: { revalidate: 60 }, // ISR : revalidation toutes les 60s
});
return (
<ul>
{data.map((article: any) => (
<li key={article.id}>
<h2>{article.attributes.title}</h2>
</li>
))}
</ul>
);
}
Avantages :
– Pas de JS côté client pour ça
– Tokens API jamais exposés (Server Components côté serveur)
– ISR natif via next.revalidate
Client Components (interactivité)
Pour des composants qui doivent réagir à des actions utilisateur (filtres en temps réel, formulaires) :
'use client';
import { useEffect, useState } from 'react';
export function SearchArticles() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
if (!query) return;
fetch(`/api/search?q=${query}`)
.then(r => r.json())
.then(setResults);
}, [query]);
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
<ul>{results.map((r: any) => <li key={r.id}>{r.attributes.title}</li>)}</ul>
</div>
);
}
Pour appeler Strapi côté client : passer par un endpoint API Next.js (route handler) qui ajoute le token. Ne jamais exposer le token Strapi dans le navigateur.
4. Génération de pages dynamiques
Page article par slug
// app/articles/[slug]/page.tsx
import { strapiFetch } from '@/lib/strapi';
import { notFound } from 'next/navigation';
export async function generateStaticParams() {
const { data } = await strapiFetch('articles', {
fields: ['slug'],
pagination: { pageSize: 100 },
});
return data.map((article: any) => ({ slug: article.attributes.slug }));
}
export default async function ArticlePage({ params }: { params: { slug: string } }) {
const { data } = await strapiFetch('articles', {
filters: { slug: { $eq: params.slug } },
populate: ['coverImage', 'author', 'categories'],
});
const article = data[0];
if (!article) notFound();
return (
<article>
<h1>{article.attributes.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.attributes.content }} />
</article>
);
}
generateStaticParams : Next.js construit toutes les pages au moment du build. Pour des sites avec des centaines de pages : pré-rendre les top 100 et ISR le reste.
Metadata SEO dynamiques
import { Metadata } from 'next';
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
const { data } = await strapiFetch('articles', {
filters: { slug: { $eq: params.slug } },
populate: ['seo', 'coverImage'],
});
const article = data[0];
if (!article) return {};
return {
title: article.attributes.seo?.meta_title || article.attributes.title,
description: article.attributes.seo?.meta_description || article.attributes.excerpt,
openGraph: {
title: article.attributes.title,
images: [article.attributes.coverImage?.data?.attributes?.url],
},
};
}
Génère les meta-tags depuis les données Strapi. SEO automatique.
5. ISR et revalidation
ISR (Incremental Static Regeneration) : pages servies statiquement, revalidées périodiquement ou à la demande.
Time-based revalidation
const data = await strapiFetch('articles', {
next: { revalidate: 60 }, // toutes les 60 secondes
});
Première requête après 60s déclenche un re-build en arrière-plan. Les utilisateurs suivants reçoivent la version fraîche.
On-demand revalidation
Encore mieux : déclencher une revalidation depuis Strapi quand un article est publié.
app/api/revalidate/route.ts :
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const secret = req.headers.get('x-secret');
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await req.json();
const { model, slug } = body;
if (model === 'article' && slug) {
revalidatePath(`/articles/${slug}`);
revalidatePath('/articles');
revalidateTag('articles');
}
return NextResponse.json({ revalidated: true });
}
Côté Strapi, créer un lifecycle hook qui notifie Next.js après publication :
// src/api/article/content-types/article/lifecycles.js
module.exports = {
async afterUpdate(event) {
const { result } = event;
if (result.publishedAt) {
await fetch(`${process.env.NEXTJS_URL}/api/revalidate`, {
method: 'POST',
headers: { 'x-secret': process.env.REVALIDATE_SECRET, 'content-type': 'application/json' },
body: JSON.stringify({ model: 'article', slug: result.slug }),
});
}
},
};
Bénéfice : contenu publié dans Strapi → page Next.js mise à jour en quelques secondes, sans rebuilder tout le site.
6. Preview mode pour brouillons
Permettre aux éditeurs de prévisualiser un brouillon avant publication.
Setup côté Next.js
app/api/preview/route.ts :
import { draftMode } from 'next/headers';
import { redirect } from 'next/navigation';
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const secret = searchParams.get('secret');
const slug = searchParams.get('slug');
if (secret !== process.env.STRAPI_PREVIEW_SECRET || !slug) {
return new Response('Invalid', { status: 401 });
}
draftMode().enable();
redirect(`/articles/${slug}`);
}
Côté Strapi
Configurer dans l’admin Strapi : Content Manager → Settings → Preview. Ajouter un bouton « Preview » sur les articles qui pointe vers https://votresite.com/api/preview?secret=XXX&slug=<slug>.
Adapter le fetch pour les drafts
import { draftMode } from 'next/headers';
const { isEnabled } = draftMode();
const data = await strapiFetch('articles', {
filters: { slug: { $eq: params.slug } },
publicationState: isEnabled ? 'preview' : 'live',
});
publicationState=preview côté Strapi inclut les brouillons (nécessite le système Draft & Publish activé).
7. Optimisation images
Next.js fournit le composant <Image> qui optimise automatiquement (formats modernes, lazy loading, responsive sizes).
Configuration
next.config.js :
module.exports = {
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'api.exemple.com', pathname: '/uploads/**' },
{ protocol: 'https', hostname: 's3.amazonaws.com', pathname: '/mon-bucket/**' },
],
},
};
Usage
import Image from 'next/image';
<Image
src={article.attributes.coverImage.data.attributes.url}
alt={article.attributes.coverImage.data.attributes.alternativeText || ''}
width={1200}
height={630}
priority
/>
Next.js optimise automatiquement : formats WebP/AVIF servis aux navigateurs supportant, dimensions multiples pour responsive, lazy loading, blur placeholder optionnel.
Avec Cloudinary
Si Strapi utilise Cloudinary pour le stockage :
import { CldImage } from 'next-cloudinary';
<CldImage src={publicId} alt="..." width={1200} height={630} />
Cloudinary applique des transformations à la volée (resize, crop, format).
8. Rendu rich text et dynamic zones
Markdown
Si Strapi stocke en Markdown :
npm install react-markdown remark-gfm
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{article.attributes.content}
</ReactMarkdown>
Blocks (Strapi v5)
Format JSON structuré, à rendre via parser custom :
import { BlocksRenderer } from '@strapi/blocks-react-renderer';
<BlocksRenderer content={article.attributes.content} />
Dynamic Zones
function PageRenderer({ blocks }: { blocks: any[] }) {
return (
<>
{blocks.map((block, i) => {
switch (block.__component) {
case 'blocks.hero':
return <Hero key={i} {...block} />;
case 'blocks.gallery':
return <Gallery key={i} {...block} />;
case 'blocks.cta':
return <CTA key={i} {...block} />;
case 'blocks.faq':
return <FAQ key={i} {...block} />;
default:
return null;
}
})}
</>
);
}
Chaque type de bloc défini dans Strapi a son composant React correspondant. Pattern modulaire et flexible.
9. Authentification utilisateurs
Pour des sections privées (espace client, contenus payants).
Login depuis Next.js
// app/api/auth/login/route.ts
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const { identifier, password } = await req.json();
const res = await fetch(`${process.env.NEXT_PUBLIC_STRAPI_URL}/api/auth/local`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ identifier, password }),
});
if (!res.ok) {
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
}
const data = await res.json();
cookies().set('strapi-jwt', data.jwt, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 jours
});
return NextResponse.json({ user: data.user });
}
Le JWT est stocké en cookie httpOnly côté Next.js, jamais exposé au JavaScript navigateur.
Pages protégées
// app/account/page.tsx
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
async function getMe() {
const jwt = cookies().get('strapi-jwt')?.value;
if (!jwt) return null;
const res = await fetch(`${process.env.NEXT_PUBLIC_STRAPI_URL}/api/users/me`, {
headers: { Authorization: `Bearer ${jwt}` },
});
if (!res.ok) return null;
return res.json();
}
export default async function AccountPage() {
const user = await getMe();
if (!user) redirect('/login');
return <div>Bonjour, {user.username}</div>;
}
10. Déploiement Vercel
Vercel est l’hébergeur officiel Next.js, optimisé pour la stack.
Setup
- Push le code sur GitHub
- Import sur vercel.com
- Configurer les variables d’environnement (
NEXT_PUBLIC_STRAPI_URL,STRAPI_API_TOKEN, etc.) - Deploy
Build automatique à chaque push, preview sur chaque PR.
Domaine custom
Configuration DNS (CNAME ou A record vers Vercel). SSL automatique.
Edge functions et middleware
Pour des optimisations avancées (rewrites par locale, A/B testing, géolocalisation), Vercel Edge Functions s’exécutent au plus près de l’utilisateur.
Alternatives à Vercel
- Netlify : équivalent, très bon support Next.js
- Cloudflare Pages : edge-native, gratuit pour usage modéré
- Railway, Render : déploiement Node.js classique
- Auto-hébergé :
next startderrière Caddy/Nginx
11. FAQ
App Router ou Pages Router ?
App Router (Next.js 13+) pour nouveaux projets. Plus moderne, Server Components, layouts imbriqués. Pages Router reste valide pour projets existants ou cas où la stabilité est prioritaire.
Server Components ou Client Components par défaut ?
Server Components par défaut. Switcher en 'use client' uniquement quand nécessaire (état local, événements DOM, hooks navigateur). Réduit drastiquement le JS envoyé au client.
Comment éviter les requêtes Strapi en double ?
fetch Next.js dédoublonne automatiquement les requêtes identiques dans une même page. Pour cas plus complexes : React cache() ou patterns custom.
ISR ou full SSG ?
SSG (export const dynamic = 'force-static') pour pages immuables. ISR pour pages qui peuvent évoluer (articles, catalogue produits). Aucune différence côté utilisateur, ISR plus pratique en pratique.
Performance Vercel vs auto-hébergé ?
Vercel est très optimisé (edge network mondial, CDN intégré, optimisations automatiques). Auto-hébergé peut égaler avec configuration soignée mais demande plus de travail. Pour PME : Vercel offre meilleur rapport effort/perf.
Coût Vercel pour PME ?
Tier gratuit (Hobby) couvre des projets personnels et MVP. Tier Pro (par équipe et par mois) pour usage commercial. Au-delà, tarification basée sur bande passante et builds. Anticiper si trafic élevé.
Strapi accessible publiquement ou derrière VPN ?
Pour le frontend Next.js : Strapi doit être accessible publiquement (HTTPS). Pour l’admin Strapi : peut être derrière VPN ou restriction IP. Configuration séparée possible (admin sur sous-domaine restreint, API publique).
Articles liés (cluster Strapi)
- 👉 Strapi headless CMS pour PME : guide complet (pillar)
- 👉 Strapi content types et API
- 👉 Strapi déploiement production
Article mis à jour le 25 avril 2026. Pour signaler une erreur ou suggérer une amélioration, écrivez-nous.