Cours Next.js · Module 9: Intégrations et fonctions avancées
Next.js et Strapi - Une combinaison puissante entre le frontend et un CMS headless
Dans cette leçon9
Dans la Métropole Quantum, où les technologies frontend et backend fusionnent en un tout harmonieux, tu vas découvrir l'une des combinaisons les plus puissantes du développement web moderne - Next.js avec Strapi. Ce duo te permet de construire des applications scalables et performantes avec un contrôle total sur le contenu.
Qu'est-ce que Strapi ?
Strapi est un CMS headless (système de gestion de contenu) open-source qui fournit une API RESTful ou GraphQL pour gérer le contenu. Contrairement aux CMS traditionnels comme WordPress, Strapi n'impose aucune structure frontend - c'est toi qui décides comment afficher les données.
Caractéristiques clés de Strapi :
- 100% JavaScript - écrit en Node.js
- Personnalisable - contrôle total sur la structure des données
- Auto-hébergé ou Cloud - choisis ton mode d'hébergement
- API RESTful & GraphQL - accès flexible aux données
- Panneau d'administration - interface de gestion intuitive
- Contrôle d'accès basé sur les rôles - gestion avancée des permissions
Pourquoi Next.js + Strapi ?
Combiner Next.js avec Strapi crée une stack complète pour construire des applications web modernes :
- Séparation des responsabilités - le frontend et le backend sont indépendants
- Performance - génération statique avec du contenu dynamique
- Expérience développeur - les deux outils sont pensés pour les développeurs
- Scalabilité - mise à l'échelle facile de chaque couche séparément
- Support TypeScript - support complet du typage
Installation et configuration de Strapi
Étape 1 : Créer un nouveau projet Strapi
1# Crée un nouveau projet Strapi
2npx create-strapi-app@latest my-strapi-backend --quickstart
3
4# Ou avec TypeScript
5npx create-strapi-app@latest my-strapi-backend --typescriptAprès l'installation, Strapi démarre automatiquement sur http://localhost:1337. Ta première visite te redirigera vers le panneau de création du compte administrateur.
Étape 2 : Configuration des Content Types
Dans le panneau d'administration de Strapi, tu peux créer des structures de données (Content Types) grâce au Content-Type Builder intuitif :
- Va dans Content-Type Builder
- Clique sur Create new collection type
- Nomme ta collection (par exemple « Article »)
- Ajoute les champs :
title(Text)content(Rich text)slug(UID)featuredImage(Media)author(Relation to Users)publishedAt(Datetime)
Étape 3 : Configuration des permissions de l'API
Par défaut, Strapi bloque l'accès public à l'API. Pour activer la lecture des données :
- Va dans Settings → Roles → Public
- Pour ton Content Type, coche les permissions adéquates (par ex.
findetfindOne) - Enregistre les modifications
Intégration de Next.js avec Strapi
Étape 1 : Préparation du projet Next.js
1# Dans un nouveau terminal, crée un projet Next.js
2npx create-next-app@latest my-nextjs-frontend --typescript --tailwind --app
3
4cd my-nextjs-frontendÉtape 2 : Configuration des variables d'environnement
Crée le fichier .env.local :
1NEXT_PUBLIC_STRAPI_API_URL=http://localhost:1337
2STRAPI_API_TOKEN=your-api-token-hereÉtape 3 : Création du client API
Crée des fonctions utilitaires pour communiquer avec Strapi :
1// lib/strapi.ts
2interface StrapiResponse<T> {
3 data: T;
4 meta: {
5 pagination?: {
6 page: number;
7 pageSize: number;
8 pageCount: number;
9 total: number;
10 };
11 };
12}
13
14const strapiUrl = process.env.NEXT_PUBLIC_STRAPI_API_URL || 'http://localhost:1337';
15
16export async function fetchAPI<T>(
17 path: string,
18 urlParamsObject: Record<string, any> = {},
19 options: RequestInit = {}
20): Promise<T> {
21 // Fusionne les options par défaut et celles de l'utilisateur
22 const mergedOptions: RequestInit = {
23 headers: {
24 "Content-Type": "application/json",
25 ...(process.env.STRAPI_API_TOKEN && {
26 Authorization: `Bearer ${process.env.STRAPI_API_TOKEN}`,
27 }),
28 },
29 ...options,
30 };
31
32 // Construit l'URL de la requête
33 const queryString = new URLSearchParams(urlParamsObject).toString();
34 const requestUrl = `${strapiUrl}${path}${queryString ? `?${queryString}` : ""}`;
35
36 // Déclenche l'appel API
37 const response = await fetch(requestUrl, mergedOptions);
38 const data = await response.json();
39
40 if (!response.ok) {
41 throw new Error(`API call failed: ${response.status}`);
42 }
43
44 return data;
45}
46
47// Utilitaire pour obtenir l'URL d'un média
48export function getStrapiMedia(url: string | null) {
49 if (url == null) return null;
50 if (url.startsWith("http") || url.startsWith("//")) return url;
51 return `${strapiUrl}${url}`;
52}Récupérer des données depuis Strapi dans Next.js
Static Site Generation (SSG)
Utilise generateStaticParams et des fonctions asynchrones dans les composants :
1// app/articles/page.tsx
2import { fetchAPI } from '@/lib/strapi';
3
4interface Article {
5 id: number;
6 attributes: {
7 title: string;
8 slug: string;
9 content: string;
10 publishedAt: string;
11 featuredImage: {
12 data: {
13 attributes: {
14 url: string;
15 alternativeText: string;
16 };
17 };
18 };
19 };
20}
21
22async function getArticles() {
23 const articlesRes = await fetchAPI<StrapiResponse<Article[]>>('/api/articles', {
24 populate: {
25 featuredImage: {
26 fields: ['url', 'alternativeText']
27 },
28 author: {
29 fields: ['name', 'email']
30 }
31 },
32 sort: ['publishedAt:desc'],
33 pagination: {
34 pageSize: 10,
35 },
36 });
37
38 return articlesRes.data;
39}
40
41export default async function ArticlesPage() {
42 const articles = await getArticles();
43
44 return (
45 <div className="container mx-auto px-4 py-8">
46 <h1 className="text-4xl font-bold mb-8">Articles</h1>
47 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
48 {articles.map((article) => (
49 <article key={article.id} className="border rounded-lg overflow-hidden shadow-lg">
50 {article.attributes.featuredImage?.data && (
51 <img
52 src={getStrapiMedia(article.attributes.featuredImage.data.attributes.url)}
53 alt={article.attributes.featuredImage.data.attributes.alternativeText}
54 className="w-full h-48 object-cover"
55 />
56 )}
57 <div className="p-4">
58 <h2 className="text-xl font-semibold mb-2">
59 {article.attributes.title}
60 </h2>
61 <p className="text-gray-600">
62 {new Date(article.attributes.publishedAt).toLocaleDateString('pl-PL')}
63 </p>
64 <a
65 href={`/articles/${article.attributes.slug}`}
66 className="text-blue-500 hover:underline mt-2 inline-block"
67 >
68 Lire la suite →
69 </a>
70 </div>
71 </article>
72 ))}
73 </div>
74 </div>
75 );
76}Routes dynamiques
Pour les articles individuels, utilise des routes dynamiques :
1// app/articles/[slug]/page.tsx
2import { fetchAPI } from '@/lib/strapi';
3import { notFound } from 'next/navigation';
4import ReactMarkdown from 'react-markdown';
5
6interface ArticlePageProps {
7 params: Promise<{
8 slug: string;
9 }>;
10}
11
12async function getArticleBySlug(slug: string) {
13 const articlesRes = await fetchAPI<StrapiResponse<Article[]>>('/api/articles', {
14 filters: {
15 slug: {
16 $eq: slug,
17 },
18 },
19 populate: {
20 featuredImage: {
21 fields: ['url', 'alternativeText']
22 },
23 author: {
24 populate: {
25 avatar: {
26 fields: ['url']
27 }
28 }
29 }
30 },
31 });
32
33 if (!articlesRes.data || articlesRes.data.length === 0) {
34 return null;
35 }
36
37 return articlesRes.data[0];
38}
39
40export async function generateStaticParams() {
41 const articlesRes = await fetchAPI<StrapiResponse<Article[]>>('/api/articles', {
42 fields: ['slug'],
43 pagination: {
44 pageSize: 100,
45 },
46 });
47
48 return articlesRes.data.map((article) => ({
49 slug: article.attributes.slug,
50 }));
51}
52
53export default async function ArticlePage({ params }: ArticlePageProps) {
54 const article = await getArticleBySlug((await params).slug);
55
56 if (!article) {
57 notFound();
58 }
59
60 return (
61 <article className="container mx-auto px-4 py-8 max-w-4xl">
62 {article.attributes.featuredImage?.data && (
63 <img
64 src={getStrapiMedia(article.attributes.featuredImage.data.attributes.url)}
65 alt={article.attributes.featuredImage.data.attributes.alternativeText}
66 className="w-full h-96 object-cover rounded-lg mb-8"
67 />
68 )}
69
70 <h1 className="text-4xl font-bold mb-4">{article.attributes.title}</h1>
71
72 <div className="flex items-center gap-4 mb-8 text-gray-600">
73 <span>Auteur : {article.attributes.author?.data?.attributes.name}</span>
74 <span>•</span>
75 <time>
76 {new Date(article.attributes.publishedAt).toLocaleDateString('pl-PL')}
77 </time>
78 </div>
79
80 <div className="prose prose-lg max-w-none">
81 <ReactMarkdown>{article.attributes.content}</ReactMarkdown>
82 </div>
83 </article>
84 );
85}Fonctionnalités avancées
Mises à jour en temps réel avec les webhooks
Strapi peut envoyer des webhooks quand le contenu change. Configure un endpoint dans Next.js :
1// app/api/revalidate/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { revalidatePath, revalidateTag } from 'next/cache';
4
5export async function POST(request: NextRequest) {
6 const body = await request.json();
7 const secret = request.headers.get('x-strapi-signature');
8
9 // Vérifie la signature du webhook
10 if (secret !== process.env.STRAPI_WEBHOOK_SECRET) {
11 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
12 }
13
14 try {
15 // Revalide les chemins appropriés selon l'événement
16 switch (body.event) {
17 case 'entry.create':
18 case 'entry.update':
19 if (body.model === 'article') {
20 revalidatePath('/articles');
21 revalidatePath(`/articles/${body.entry.slug}`);
22 }
23 break;
24 case 'entry.delete':
25 if (body.model === 'article') {
26 revalidatePath('/articles');
27 }
28 break;
29 }
30
31 return NextResponse.json({ revalidated: true });
32 } catch (error) {
33 return NextResponse.json({ error: 'Error revalidating' }, { status: 500 });
34 }
35}Intégration GraphQL
Strapi propose aussi une API GraphQL. Installe le plugin :
1# Dans le dossier Strapi
2npm install @strapi/plugin-graphqlEnsuite, utilise GraphQL dans Next.js :
1// lib/graphql.ts
2import { GraphQLClient } from 'graphql-request';
3
4const client = new GraphQLClient(`${process.env.NEXT_PUBLIC_STRAPI_API_URL}/graphql`, {
5 headers: {
6 authorization: process.env.STRAPI_API_TOKEN
7 ? `Bearer ${process.env.STRAPI_API_TOKEN}`
8 : '',
9 },
10});
11
12export async function queryStrapi<T>(query: string, variables?: any): Promise<T> {
13 return client.request<T>(query, variables);
14}Optimisation des images
Intègre Strapi avec l'optimisation d'images de Next.js :
1// components/StrapiImage.tsx
2import Image from 'next/image';
3import { getStrapiMedia } from '@/lib/strapi';
4
5interface StrapiImageProps {
6 src: string;
7 alt: string;
8 width: number;
9 height: number;
10 className?: string;
11}
12
13export default function StrapiImage({ src, alt, ...props }: StrapiImageProps) {
14 const imageUrl = getStrapiMedia(src);
15
16 if (!imageUrl) {
17 return null;
18 }
19
20 return (
21 <Image
22 src={imageUrl}
23 alt={alt}
24 {...props}
25 loader={({ src, width, quality }) => {
26 const params = new URLSearchParams();
27 if (width) params.set('width', width.toString());
28 if (quality) params.set('quality', quality.toString());
29 return `${src}?${params.toString()}`;
30 }}
31 />
32 );
33}Mode aperçu
Implémente un aperçu pour le contenu non publié :
1// app/api/preview/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { draftMode } from 'next/headers';
4
5export async function GET(request: NextRequest) {
6 const secret = request.nextUrl.searchParams.get('secret');
7 const slug = request.nextUrl.searchParams.get('slug');
8
9 if (secret !== process.env.STRAPI_PREVIEW_SECRET || !slug) {
10 return NextResponse.json({ message: 'Invalid token' }, { status: 401 });
11 }
12
13 // Active le Draft Mode
14 (await draftMode()).enable();
15
16 // Redirige vers la page d'aperçu
17 return NextResponse.redirect(new URL(`/articles/${slug}`, request.url));
18}
19
20// app/api/exit-preview/route.ts
21export async function GET() {
22 (await draftMode()).disable();
23 return NextResponse.redirect('/');
24}Déploiement et bonnes pratiques
1. Variables d'environnement
Utilise toujours des variables d'environnement pour les données sensibles :
1# .env.production
2NEXT_PUBLIC_STRAPI_API_URL=https://your-strapi-instance.com
3STRAPI_API_TOKEN=your-production-token
4STRAPI_WEBHOOK_SECRET=your-webhook-secret
5STRAPI_PREVIEW_SECRET=your-preview-secret2. Stratégie de cache
Exploite le cache de Next.js :
1// Cache pendant 60 secondes
2export const revalidate = 60;
3
4// Ou utilise des cache tags
5fetch(url, { next: { tags: ['articles'] } });3. Gestion des erreurs
Implémente une gestion des erreurs robuste :
1export async function getArticles() {
2 try {
3 const data = await fetchAPI<StrapiResponse<Article[]>>('/api/articles');
4 return data;
5 } catch (error) {
6 console.error('Error fetching articles:', error);
7 return { data: [], meta: {} };
8 }
9}4. Types TypeScript
Génère des types à partir du schéma Strapi :
1# Installe le générateur de types
2npm install -D @strapi/typesRésumé
Combiner Next.js avec Strapi crée une stack puissante pour construire des applications web modernes. Strapi fournit une API flexible et un système de gestion de contenu intuitif, tandis que Next.js offre un frontend performant avec SSG/SSR et des optimisations.
Cette combinaison est idéale pour :
- Les blogs et les portails d'actualités
- L'e-commerce
- Les portfolios et les sites corporatifs
- Les applications SaaS
- Les plateformes éducatives
Dans les modules suivants, nous explorerons des sujets plus avancés comme le multi-tenancy, l'internationalisation et les plugins personnalisés dans Strapi.
Code de cette leçon : App.tsx
1import React, { useState } from 'react';
2
3interface Article {
4 id: number;
5 title: string;
6 slug: string;
7 content: string;
8 author: string;
9 publishedAt: string;
10 category: string;
11}
12
13const mockArticles: Article[] = [
14 { id: 1, title: 'Introduction au CMS Headless', slug: 'intro-headless-cms', content: 'Un CMS headless sépare le backend du frontend, offrant un contrôle total sur la présentation du contenu...', author: 'Admin', publishedAt: '2025-01-15', category: 'CMS' },
15 { id: 2, title: 'Strapi et Next.js - Pour commencer', slug: 'strapi-nextjs-start', content: 'Intégrer Strapi avec Next.js permet de construire des applications performantes avec du contenu dynamique...', author: 'Developer', publishedAt: '2025-02-10', category: 'Tutorial' },
16 { id: 3, title: 'REST vs GraphQL dans Strapi', slug: 'rest-vs-graphql', content: 'Strapi propose à la fois une API REST et GraphQL. Chaque approche a ses avantages...', author: 'Admin', publishedAt: '2025-03-05', category: 'API' },
17 { id: 4, title: 'Typer les réponses Strapi en TypeScript', slug: 'strapi-typescript', content: "Avec TypeScript, on peut typer les réponses de l'API Strapi pour une meilleure DX...", author: 'Developer', publishedAt: '2025-03-20', category: 'TypeScript' },
18];
19
20const StrapiDemo = () => {
21 const [articles] = useState<Article[]>(mockArticles);
22 const [selected, setSelected] = useState<Article | null>(null);
23 const [endpoint, setEndpoint] = useState('/api/articles');
24 const [response, setResponse] = useState<string>('');
25
26 const simulateFetch = () => {
27 const isDetail = endpoint.includes('/api/articles/');
28 const slug = endpoint.split('/api/articles/')[1];
29
30 if (isDetail && slug) {
31 const found = articles.find(a => a.slug === slug);
32 if (found) {
33 setResponse(JSON.stringify({ data: { id: found.id, attributes: { title: found.title, content: found.content, slug: found.slug, author: found.author, publishedAt: found.publishedAt } }, meta: {} }, null, 2));
34 setSelected(found);
35 } else {
36 setResponse(JSON.stringify({ error: { status: 404, message: 'Not found' } }, null, 2));
37 }
38 } else {
39 setResponse(JSON.stringify({ data: articles.map(a => ({ id: a.id, attributes: { title: a.title, slug: a.slug, category: a.category, publishedAt: a.publishedAt } })), meta: { pagination: { page: 1, pageSize: 25, total: articles.length } } }, null, 2));
40 setSelected(null);
41 }
42 };
43
44 return (
45 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#fff', fontFamily: 'sans-serif' }}>
46 <h1 style={{ color: '#64ffda', textAlign: 'center' }}>Next.js + Strapi CMS</h1>
47 <p style={{ textAlign: 'center', color: '#888', marginBottom: '20px' }}>Simulation de l'API d'un CMS headless</p>
48
49 <div style={{ maxWidth: '800px', margin: '0 auto' }}>
50 <div style={{ background: 'rgba(255,255,255,0.05)', padding: '16px', borderRadius: '10px', marginBottom: '16px', border: '1px solid #333' }}>
51 <h3 style={{ color: '#7c4dff', marginBottom: '10px' }}>API Explorer</h3>
52 <div style={{ display: 'flex', gap: '8px', marginBottom: '10px' }}>
53 <input value={endpoint} onChange={e => setEndpoint(e.target.value)}
54 style={{ flex: 1, padding: '10px', background: '#16213e', border: '1px solid #444', borderRadius: '6px', color: '#fff', fontFamily: 'monospace' }} />
55 <button onClick={simulateFetch} style={{ padding: '10px 20px', background: '#7c4dff', border: 'none', borderRadius: '6px', color: '#fff', cursor: 'pointer' }}>
56 GET
57 </button>
58 </div>
59 <div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
60 <button onClick={() => setEndpoint('/api/articles')} style={{ padding: '4px 10px', background: 'rgba(100,255,218,0.1)', border: '1px solid #64ffda', borderRadius: '4px', color: '#64ffda', cursor: 'pointer', fontSize: '12px' }}>/api/articles</button>
61 {articles.map(a => (
62 <button key={a.id} onClick={() => setEndpoint('/api/articles/' + a.slug)} style={{ padding: '4px 10px', background: 'rgba(124,77,255,0.1)', border: '1px solid #7c4dff', borderRadius: '4px', color: '#7c4dff', cursor: 'pointer', fontSize: '12px' }}>
63 /articles/{a.slug}
64 </button>
65 ))}
66 </div>
67 </div>
68
69 {response && (
70 <div style={{ background: '#000', padding: '16px', borderRadius: '10px', marginBottom: '16px' }}>
71 <h4 style={{ color: '#4caf50', marginBottom: '8px' }}>Réponse :</h4>
72 <pre style={{ color: '#0f0', fontSize: '12px', overflow: 'auto', maxHeight: '250px', whiteSpace: 'pre-wrap' }}>{response}</pre>
73 </div>
74 )}
75
76 {selected && (
77 <div style={{ background: 'rgba(255,255,255,0.05)', padding: '20px', borderRadius: '10px', border: '1px solid #333' }}>
78 <span style={{ color: '#7c4dff', fontSize: '12px' }}>{selected.category}</span>
79 <h2 style={{ color: '#64ffda', margin: '8px 0' }}>{selected.title}</h2>
80 <p style={{ color: '#aaa', fontSize: '13px' }}>Par {selected.author} | {selected.publishedAt}</p>
81 <p style={{ color: '#ccc', marginTop: '12px', lineHeight: '1.6' }}>{selected.content}</p>
82 </div>
83 )}
84 </div>
85 </div>
86 );
87};
88
89export default StrapiDemo;Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quelles données Strapi renvoie-t-il par défaut via son API REST ?
2. Laquelle des caractéristiques suivantes décrit le MIEUX un Headless CMS ?
Tâches pratiques dans le jeu
- Clique dans l'ordre
Remets dans l'ordre les étapes de l'intégration de Next.js avec Strapi.
- Éditeur de code
Crée un composant qui récupère des données depuis le headless CMS Strapi dans Next.js
- Éditeur de code
Crée un content type Article avec les champs title, content et slug.