Aller au contenu

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

a computer screen with a program running on it
Photo de James Wiseman sur Unsplash

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 :

  1. Authentifier la requête — seuls les éditeurs connectés peuvent récupérer les brouillons.
  2. Contourner le cache ISR — la requête d’aperçu doit toujours interroger Strapi pour obtenir le dernier brouillon.
  3. 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.
  4. 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

ExigenceVersion
Strapi5.x (installé avec le plugin GraphQL)
Node.js18 – 24
Next.js15.x
Base de donnéesPostgreSQL, MySQL ou SQLite
Secret JWTConfiguré 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 :

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 :

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

bash
npm i @apollo/client graphql

Créez un client singleton qui utilise le documentId de Strapi comme identifiant de cache :

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

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 :

  1. Lit le paramètre de requête jwt fourni par l’interface éditeur.
  2. Appelle l’endpoint interne /admin/jwt/verify de Strapi pour s’assurer que le token est toujours valide.
  3. Définit deux cookies : next-preview (active le mode aperçu de Next.js) et strapi-jwt (utilisé par le client pour authentifier les requêtes GraphQL).
  4. Appelle revalidateTag('preview') afin de nettoyer les pages ISR précédemment générées avec le tag preview.
  5. 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 :

code
https://your‑next‑site.com/api/preview?jwt=<YOUR_JWT>&to=/articles/123

Vous pouvez obtenir le JWT via la mutation GraphQL login :

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

tsx
// 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 Authorization n’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 :

ts
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
});

a computer screen with a lot of data on it
Photo de Lukas sur Unsplash

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.

tsx
// 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é

ÉtapePourquoi c’est important
Vérifier le JWT avec l’endpoint /admin/jwt/verify de StrapiEmpêche les attaques de relecture et assure que le token n’a pas été révoqué.
Stocker le JWT dans un cookie httpOnlyEmpêche le JavaScript côté client de lire le token, limitant les fuites XSS.
Utiliser sameSite: 'lax' et une courte maxAgeRéduit la fenêtre d’attaque CSRF et de vol de token.
Utiliser fetchPolicy: 'network-only' pour les brouillonsGarantit qu’une version publiée en cache n’est jamais servie à un éditeur.
Taguer les fetch d’aperçu avec 'preview' et appeler revalidateTagAssure la purge immédiate du cache après toute modification de brouillon.
Garder la route Edge sans étatAucun 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é.

json
{
  "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

  1. Test unitaire de la vérification du JWT — moquez l’endpoint /admin/jwt/verify et assurez‑vous que la route renvoie 401 pour les tokens invalides.
  2. 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.
  3. 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

code
# 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
Mots-clésNext.jsStrapiGraphQLPreview ModeISREdge Functions

Sources

  1. Strapi 5 REST API documentation
  2. Strapi 5 GraphQL plugin documentation
  3. Next.js 15 caching and revalidation guide
  4. Next.js 15 preview mode guide
  5. Next.js Edge API routes documentation
  6. Strapi content‑history feature documentation

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.

Tous les articles