Aperçu temps réel sécurisé avec Next.js 15, Strapi 5 GraphQL et Edge
Suivez le guide pour créer un mode aperçu avec Next.js 15 : récupérez les brouillons depuis Strapi 5, puis déclenchez l’invalidation du cache Edge grâce au JWT.
Par l’équipe Revisionary · · 6 min de lecture
Réponse directe :
Créez une route API Edge qui vérifie un JWT Strapi, définit le cookie d’aperçu de Next.js et invalide immédiatement les pages en cache ISR marquées « preview ». Ensuite, récupérez le contenu brouillon dans vos composants de page avec fetch(..., { next: { revalidate: 0, tags: ['preview'] } }) ou avec Apollo Client configuré pour utiliser le documentId de Strapi comme clé de cache. Cette combinaison garantit que seuls les utilisateurs authentifiés voient le dernier brouillon, contourne le cache statique et purge les pages obsolètes en temps réel.
1. Pourquoi un flux d’aperçu personnalisé est nécessaire
Strapi 5 expose à la fois les versions publiées et les brouillons d’un document. L’API REST vous permet de requêter ?status=draft, tandis que le plugin GraphQL ajoute un champ documentId qui identifie la même entité à travers les versions. Dans une configuration ISR typique de Next.js, les pages sont générées statiquement et mises en cache pendant un intervalle configurable. Lorsqu’un éditeur publie ou met à jour un brouillon, la page mise en cache continue à servir l’ancien contenu jusqu’à la prochaine revalidation. Un mode aperçu sécurisé doit donc :
- Authentifier la requête — seuls les éditeurs connectés peuvent récupérer les brouillons.
- Contourner le cache ISR — la requête d’aperçu doit toujours interroger Strapi pour obtenir le dernier brouillon.
- Invalidation du cache ISR — après une modification de brouillon, purger les pages en cache d’aperçu afin que les requêtes suivantes voient la mise à jour immédiatement.
- Exécution au edge — faible latence et aucune dépendance à un processus Node côté serveur.
Next.js 15 propose nativement la prise en charge des cookies d’aperçu, des routes API Edge et de l’invalidation de cache basée sur des tags, ce qui en fait la plateforme idéale pour ce workflow.
2. Prérequis
| Exigence | Version |
|---|---|
| Strapi | 5.x (installé avec le plugin GraphQL) |
| Node.js | 18 – 24 |
| Next.js | 15.x |
| Base de données | PostgreSQL, MySQL ou SQLite |
| Secret JWT | Configuré dans Strapi (JWT_SECRET) |
Assurez‑vous de disposer d’un compte administrateur Strapi capable de générer des tokens JWT via la mutation login (voir la documentation GraphQL). Activez également le plugin Revisionary si vous souhaitez disposer d’une vue complète de l’historique du contenu, bien que ce ne soit pas obligatoire pour l’aperçu.
3. Configurer GraphQL Strapi pour l’accès aux brouillons
3.1 Activer le plugin GraphQL
Ajoutez le plugin dans config/plugins.ts :
export default {
graphql: {
endpoint: '/graphql',
shadowCRUD: true, // générer automatiquement les résolveurs CRUD
apolloServer: {
// la valeur par défaut est « bounded » ; nous la conservons
},
},
};3.2 Autoriser les requêtes de brouillon
Strapi résout automatiquement l’argument status sur les requêtes générées. Vous pouvez demander les brouillons ainsi :
query GetArticleDraft($id: ID!) {
article(id: $id, status: "draft") {
documentId
title
content
updatedAt
}
}Le champ documentId est essentiel pour la normalisation du cache d’Apollo Client (voir la section suivante).
4. Configurer Apollo Client dans Next.js
Installez le client :
npm i @apollo/client graphqlCréez un client singleton qui utilise le documentId de Strapi comme identifiant de cache :
// lib/apolloClient.ts
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
const httpLink = new HttpLink({
uri: process.env.NEXT_PUBLIC_STRAPI_GRAPHQL_ENDPOINT,
headers: {
// Le JWT sera ajouté par requête dans la route d’aperçu
},
});
export const apolloClient = new ApolloClient({
link: httpLink,
cache: new InMemoryCache({
typePolicies: {
// Utiliser le documentId de Strapi pour l’identité entre brouillon et publié
Query: {
fields: {
article: {
read(_, { readField }) {
return readField('documentId');
},
},
},
},
Article: {
keyFields: ['documentId'],
},
},
}),
});Lorsque le cookie d’aperçu est présent, la route Edge ajoutera l’en‑tête JWT, ainsi le client n’a pas besoin de le définir manuellement.
5. Créer la route API d’aperçu Edge
Les routes Edge de Next.js 15 s’exécutent au CDN, ont un temps de démarrage quasi nul et peuvent définir des cookies qui seront envoyés lors des requêtes de page suivantes.
Créez le fichier app/api/preview/route.ts :
// app/api/preview/route.ts
import { NextResponse } from 'next/server';
import { revalidateTag } from 'next/cache';
export const runtime = 'edge'; // garantit l’exécution au edge
// Helper : vérifier le JWT auprès de la clé publique de Strapi (ou via /admin)
async function verifyJwt(token: string): Promise<boolean> {
const res = await fetch(`${process.env.NEXT_PUBLIC_STRAPI_URL}/admin/jwt/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
});
return res.ok;
}
export async function GET(request: Request) {
const url = new URL(request.url);
const token = url.searchParams.get('jwt');
const redirectTo = url.searchParams.get('to') ?? '/';
if (!token) {
return new NextResponse('Missing JWT', { status: 400 });
}
const isValid = await verifyJwt(token);
if (!isValid) {
return new NextResponse('Invalid JWT', { status: 401 });
}
// Définir le cookie d’aperçu (nom par défaut __next_preview_data)
const response = NextResponse.redirect(redirectTo);
response.cookies.set('next-preview', '1', {
httpOnly: true,
path: '/',
maxAge: 60 * 60, // 1 heure
sameSite: 'lax',
});
// Stocker le JWT dans un cookie séparé pour les fetch côté client
response.cookies.set('strapi-jwt', token, {
httpOnly: true,
path: '/',
maxAge: 60 * 60,
sameSite: 'lax',
});
// Purger immédiatement les pages ISR marquées « preview »
revalidateTag('preview');
return response;
}Ce que fait la route :
- Lit le paramètre de requête
jwtfourni par l’interface éditeur. - Appelle l’endpoint interne
/admin/jwt/verifyde Strapi pour s’assurer que le token est toujours valide. - Définit deux cookies :
next-preview(active le mode aperçu de Next.js) etstrapi-jwt(utilisé par le client pour authentifier les requêtes GraphQL). - Appelle
revalidateTag('preview')afin de nettoyer les pages ISR précédemment générées avec le tagpreview. - Redirige l’éditeur vers la page qu’il était en train de prévisualiser.
6. Déclencher la route d’aperçu depuis l’interface Strapi
Ajoutez un petit bouton dans la barre d’outils admin de Strapi (ou un webhook personnalisé) qui ouvre l’URL d’aperçu :
https://your‑next‑site.com/api/preview?jwt=<YOUR_JWT>&to=/articles/123Vous pouvez obtenir le JWT via la mutation GraphQL login :
mutation Login($identifier: String!, $password: String!) {
login(input: { identifier: $identifier, password: $password }) {
jwt
}
}Stockez le token temporairement dans le navigateur (par ex. sessionStorage) et construisez l’URL dynamiquement.
7. Récupérer le contenu brouillon dans un composant de page Next.js
Dans la page qui rend un article, utilisez les options next du fetch pour contourner l’ISR et taguer la requête pour l’invalidation d’aperçu.
// app/articles/[id]/page.tsx
import { gql } from '@apollo/client';
import { apolloClient } from '@/lib/apolloClient';
const ARTICLE_DRAFT = gql`
query Article($id: ID!) {
article(id: $id, status: "draft") {
documentId
title
content
}
}
`;
export default async function ArticlePage({ params }: { params: { id: string } }) {
// Détecter le mode aperçu via le cookie (Next.js définit automatiquement le drapeau `preview`)
const isPreview = !!process.env.NEXT_PUBLIC_PREVIEW_MODE && !!(await import('next/headers')).cookies().get('next-preview');
const { data } = await apolloClient.query({
query: ARTICLE_DRAFT,
variables: { id: params.id },
fetchPolicy: 'network-only', // toujours interroger Strapi pour les brouillons
context: {
// Attacher le JWT du cookie en mode aperçu
headers: isPreview
? { Authorization: `Bearer ${await import('next/headers').cookies().get('strapi-jwt')?.value}` }
: {},
},
});
const article = data.article;
return (
<article>
<h1>{article.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.content }} />
</article>
);
}Points clés :
fetchPolicy: 'network-only'force Apollo à contourner son cache, garantissant le dernier brouillon.- L’en‑tête
Authorizationn’est ajouté que lorsque le cookie d’aperçu est présent. - Le composant s’exécute au edge (par défaut avec le routeur d’app de Next.js 15), ce qui minimise la latence.
Si vous préférez l’API native fetch plutôt qu’Apollo, vous pouvez écrire :
const res = await fetch(`${process.env.NEXT_PUBLIC_STRAPI_GRAPHQL_ENDPOINT}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(isPreview && { Authorization: `Bearer ${jwt}` }),
},
body: JSON.stringify({ query: ARTICLE_DRAFT, variables: { id: params.id } }),
next: { revalidate: 0, tags: ['preview'] }, // contourner l’ISR et taguer pour l’invalidation
});8. ISR compatible edge pour le trafic non‑aperçu
Pour les visiteurs classiques, vous conservez l’ISR afin de garder le site rapide. Ajoutez simplement une valeur revalidate globale et évitez le tag preview.
// app/articles/[id]/page.tsx (suite)
export const revalidate = 60; // ISR d’1 minute pour le contenu publiéLorsque la page s’exécute en mode aperçu, les options next { revalidate: 0, tags: ['preview'] } écrasent la valeur statique, assurant que le brouillon reste toujours frais.
9. Checklist sécurité
| Étape | Pourquoi c’est important |
|---|---|
Vérifier le JWT avec l’endpoint /admin/jwt/verify de Strapi | Empêche les attaques de relecture et assure que le token n’a pas été révoqué. |
| Stocker le JWT dans un cookie httpOnly | Empêche le JavaScript côté client de lire le token, limitant les fuites XSS. |
Utiliser sameSite: 'lax' et une courte maxAge | Réduit la fenêtre d’attaque CSRF et de vol de token. |
Utiliser fetchPolicy: 'network-only' pour les brouillons | Garantit qu’une version publiée en cache n’est jamais servie à un éditeur. |
Taguer les fetch d’aperçu avec 'preview' et appeler revalidateTag | Assure la purge immédiate du cache après toute modification de brouillon. |
| Garder la route Edge sans état | Aucun stockage côté serveur, la route s’adapte automatiquement à la mise à l’échelle. |
10. Option : se brancher aux événements de changement de contenu Strapi
Si vous voulez que le cache d’aperçu se vide automatiquement lorsqu’un éditeur enregistre un brouillon, ajoutez un webhook dans Strapi qui appelle la même route Edge (POST) avec le JWT et le slug concerné.
{
"event": "entry.save",
"url": "https://your‑next‑site.com/api/preview?jwt=<JWT>&to=/articles/{{entry.id}}",
"method": "GET"
}Le webhook se déclenche à chaque sauvegarde, la route Edge re‑valide le token puis exécute revalidateTag('preview') pour nettoyer la page obsolète.
11. Tester le flux d’aperçu
- Test unitaire de la vérification du JWT — moquez l’endpoint
/admin/jwt/verifyet assurez‑vous que la route renvoie 401 pour les tokens invalides. - Test end‑to‑end avec Playwright — connectez‑vous à Strapi, récupérez un JWT, naviguez vers l’URL d’aperçu et vérifiez que la page affichée contient le contenu du brouillon tandis que l’URL publique montre toujours la version publiée.
- Test d’invalidation du cache — après modification du brouillon, déclenchez le webhook ou rafraîchissez manuellement l’URL d’aperçu et confirmez que la page se met à jour sans rechargement complet.
12. Où le plugin Revisionary intervient
Si votre projet nécessite un historique complet de chaque modification de brouillon, activez le plugin Revisionary. Il stocke l’historique champ par champ, permet de comparer les versions et expose une API (/content-history) consultable depuis le front‑end si vous souhaitez afficher une bannière « dernière modification » en mode aperçu. Le plugin est un module payant, installé sous forme de zip et activé via le gestionnaire de plugins de Strapi. Consultez la page /docs/reference/content-api pour plus de détails sur l’exposition de l’historique via l’API.
13. Récapitulatif du code complet
# config/plugins.ts (Strapi)
export default {
graphql: { endpoint: '/graphql', shadowCRUD: true },
};
# lib/apolloClient.ts (Next.js)
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
export const apolloClient = new ApolloClient({
link: new HttpLink({ uri: process.env.NEXT_PUBLIC_STRAPI_GRAPHQL_ENDPOINT }),
cache: new InMemoryCache({ typePolicies: { Article: { keyFields: ['documentId'] } } }),
});
# app/api/preview/route.ts (Edge API)
export const runtime = 'edge';
export async function GET(req: Request) { /* voir section 5 */ }
# app/articles/[id]/page.tsx (Composant page)
export const revalidate = 60;
export default async function ArticlePage({ params }) { /* voir section 7 */ }14. Conclusion
En combinant l’accès aux brouillons GraphQL de Strapi 5, l’authentification JWT et les routes API Edge de Next.js 15 avec l’invalidation de cache basée sur les tags, vous obtenez un mode aperçu sécurisé en temps réel qui ne fuit jamais le contenu non publié et reflète instantanément les dernières modifications. Le pattern est indépendant du framework : les mêmes concepts fonctionnent avec des endpoints REST ou d’autres CMS headless, mais le documentId de Strapi et la vérification JWT intégrée facilitent grandement l’intégration.
Lectures complémentaires
- Documentation du plugin GraphQL Strapi –
/docs/reference/content-api - Guide de mise en cache de Next.js 15 –
/docs/getting-started(lien interne fictif) - Présentation du plugin Revisionary –
/docs/everyday-use/panel
Sources
L’équipe Revisionary
Les développeurs de Revisionary, un plugin d’historique des versions pour Strapi 5. Nous écrivons sur Strapi, le développement web, l’IA et la sécurité des applications.
Cet article a été rédigé avec l'aide d'une IA.