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

9 min de lecture
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 :

  1. Séparation des responsabilités - le frontend et le backend sont indépendants
  2. Performance - génération statique avec du contenu dynamique
  3. Expérience développeur - les deux outils sont pensés pour les développeurs
  4. Scalabilité - mise à l'échelle facile de chaque couche séparément
  5. 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 --typescript

Aprè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 :

  1. Va dans Content-Type Builder
  2. Clique sur Create new collection type
  3. Nomme ta collection (par exemple « Article »)
  4. 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 :

  1. Va dans Settings → Roles → Public
  2. Pour ton Content Type, coche les permissions adéquates (par ex. find et findOne)
  3. 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-graphql

Ensuite, 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-secret

2. 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/types

Ré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. 1. Quelles données Strapi renvoie-t-il par défaut via son API REST ?

  2. 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.

Articles utiles