Skip to Content

Chapitre 9.5 — WordPress headless avec Next.js

⏱️ TL;DR — En headless, WordPress devient un back-office de contenu et Next.js (App Router) rend le front. On fetch l’API WordPress côté serveur (Server Components), on génère les pages en SSG avec ISR (revalidate) ou à la demande, et on n’expose aucun secret au client. On construit HeadlessShelf : un front Next.js sur le catalogue wpr-books (Parties 7-8), avec liste et pages de détail, et revalidation à la publication via un webhook. C’est le projet vitrine de votre profil (Partie 13).

🎯 Objectifs

  • Structurer un front Next.js App Router sur WordPress.
  • Fetcher l’API côté serveur avec cache/ISR.
  • Générer les routes dynamiques (liste + détail).
  • Revalider à la publication (webhook / on-demand).

1. L’architecture headless

  • WordPress gère le contenu (édition, meta, médias) — les rédacteurs gardent leur back-office familier.
  • Next.js rend le site (perf, SEO, DX moderne, composants React).
  • La revalidation (webhook) garde le front à jour après publication.

2. Le client WordPress (côté serveur)

Un petit module qui centralise les appels, jamais exposé au client :

// lib/wp.ts (exécuté côté serveur uniquement) const WP = process.env.WP_API_URL!; // ex. https://cms.exemple.com/wp-json/wp/v2 export async function getLivres(): Promise<Livre[]> { const res = await fetch(`${WP}/livre?_fields=id,slug,title,meta.wpr_prix&per_page=100`, { next: { revalidate: 3600, tags: ['livres'] }, // ISR : re-généré au max 1×/h, invalidable par tag }); if (!res.ok) throw new Error(`WP ${res.status}`); return res.json(); } export async function getLivre(slug: string): Promise<Livre | null> { const res = await fetch(`${WP}/livre?slug=${encodeURIComponent(slug)}&_fields=id,slug,title,content,meta`, { next: { revalidate: 3600, tags: [`livre:${slug}`] }, }); const arr = await res.json(); return arr[0] ?? null; }
  • Le fetch de Next.js cache la réponse et applique ISR via next.revalidate et des tags (revalidateTag).
  • On limite les champs (_fields, ch. 9.1) pour des payloads maigres.
  • Aucun secret ici (lecture publique). Pour du privé (brouillons), l’auth serveur va dans le fetch (ch. 9.2 & 9.6).

3. La liste (Server Component)

// app/livres/page.tsx import { getLivres } from '@/lib/wp'; import Link from 'next/link'; export default async function LivresPage() { const livres = await getLivres(); return ( <ul> {livres.map((l) => ( <li key={l.id}> <Link href={`/livres/${l.slug}`}>{l.title.rendered}</Link> {l.meta?.wpr_prix ? <span> — {l.meta.wpr_prix} €</span> : null} </li> ))} </ul> ); }

Un Server Component classique : await le fetch, map sur les données. Rien d’exotique — c’est votre quotidien Next.js, la source étant WordPress.


4. La page de détail + génération statique

// app/livres/[slug]/page.tsx import { getLivres, getLivre } from '@/lib/wp'; import { notFound } from 'next/navigation'; // Pré-générer les pages au build (SSG) export async function generateStaticParams() { const livres = await getLivres(); return livres.map((l) => ({ slug: l.slug })); } export default async function LivrePage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const livre = await getLivre(slug); if (!livre) notFound(); return ( <article> <h1>{livre.title.rendered}</h1> {/* content.rendered est du HTML WordPress → sanitize avant dangerouslySetInnerHTML */} <div dangerouslySetInnerHTML={{ __html: livre.content.rendered }} /> </article> ); }
  • generateStaticParams pré-génère les pages des livres au build (SSG) ; les nouvelles apparaissent via ISR/revalidation.
  • content.rendered est du HTML WordPress : en headless, assainissez-le (ex. isomorphic-dompurify) avant dangerouslySetInnerHTML — le HTML vient d’un CMS, traité comme contenu à filtrer.

⚠️ Piège n°1 — le HTML brut du CMS. Injecter content.rendered sans le nettoyer expose à du XSS si un compte éditeur est compromis ou si des shortcodes/embeds injectent du script. Filtrez (DOMPurify) côté rendu. Le pont WordPress→React ne dispense pas des réflexes de sécurité (ch. 7.7).

⚠️ Piège n°2 — les liens et médias. Le HTML de WordPress contient des URLs absolues vers le CMS (cms.exemple.com/wp-content/uploads/…) et des liens internes pointant vers WordPress. En headless, il faut souvent réécrire ces URLs (médias servis depuis le CMS ou un CDN, liens internes vers vos routes Next). À prévoir dans le pipeline de rendu.


5. Revalider à la publication (on-demand ISR)

Pour que le front se mette à jour quand un rédacteur publie, WordPress appelle un webhook Next.js qui invalide le cache :

// app/api/revalidate/route.ts import { revalidateTag } from 'next/cache'; import { NextRequest, NextResponse } from 'next/server'; export async function POST(req: NextRequest) { const secret = req.nextUrl.searchParams.get('secret'); if (secret !== process.env.REVALIDATE_SECRET) { // secret partagé (env serveur) return NextResponse.json({ ok: false }, { status: 401 }); } revalidateTag('livres'); // invalide la liste ; on peut cibler `livre:${slug}` return NextResponse.json({ revalidated: true }); }

Côté WordPress, on déclenche l’appel sur save_post_livre (ch. 5.2) :

add_action( 'save_post_livre', function ( $post_id, $post ) { if ( wp_is_post_revision( $post_id ) || 'publish' !== $post->post_status ) { return; } wp_remote_post( 'https://app.exemple.com/api/revalidate?secret=' . MON_SECRET, array( 'blocking' => false, // non bloquant ) ); }, 10, 2 );

→ à chaque publication, le front régénère les pages concernées. C’est l’ISR à la demande : statique et frais.

💡 Pour un dev React — Tout ce chapitre est du Next.js standard : Server Components qui await fetch, generateStaticParams pour le SSG, next.revalidate/revalidateTag pour l’ISR, un route handler pour le webhook. La seule « nouveauté » est que la source de données est WordPress (que vous avez modelé en Parties 7-8). Vous appliquez vos compétences Next.js telles quelles — c’est précisément pourquoi ce profil est rare et recherché (Partie 13).


6. HeadlessShelf : ce qu’on a construit

  • Un front Next.js (liste /livres + détail /livres/[slug]) sur le catalogue wpr-books.
  • SSG + ISR avec revalidation à la publication (webhook).
  • Payloads optimisés (_fields), HTML assaini, secrets côté serveur.
  • Les rédacteurs éditent dans WordPress ; les visiteurs voient un front moderne et rapide.

C’est le pont complet WordPress ↔ Next.js — la démonstration vivante de votre valeur.


✏️ Exercices

  1. Pourquoi fetcher l’API WordPress dans un Server Component plutôt que côté client ?
  2. Comment faire en sorte qu’une page de livre se mette à jour immédiatement après publication, sans rebuild complet ?
  3. Quel risque en injectant content.rendered directement, et comment le gérer ?

✅ Solution

  1. Pour garder les éventuels secrets côté serveur (ch. 9.2), bénéficier du cache/ISR de Next, éviter le CORS (pas d’appel navigateur → CMS), et livrer du HTML statique (SEO + perf). Le client ne parle qu’à Next.
  2. Un webhook de revalidation on-demand : WordPress appelle /api/revalidate (avec un secret) sur save_post, qui fait revalidateTag('livres')/livre:${slug} → seules les pages concernées sont régénérées, sans rebuild complet.
  3. Un risque XSS (le HTML vient du CMS ; un éditeur compromis ou un embed malveillant pourrait injecter du script). On le gère en assainissant le HTML (ex. DOMPurify) avant dangerouslySetInnerHTML, et en réécrivant les URLs médias/liens.

🧠 Quiz de révision

1. Qui gère le contenu, qui gère le rendu en headless ?

WordPress gère le contenu (back-office) ; Next.js gère le rendu du front.

2. Comment obtenir des pages statiques mais à jour ?

SSG + ISR : generateStaticParams + next.revalidate/revalidateTag, avec revalidation on-demand à la publication.

3. Où fetch-t-on l’API et pourquoi ?

Côté serveur (Server Components / route handlers) : secrets protégés, cache/ISR, pas de CORS.

4. Comment WordPress notifie-t-il Next.js d’une publication ?

Par un webhook (wp_remote_post sur save_post) vers un route handler qui revalidateTag.

5. Que faire du content.rendered avant affichage ?

L’assainir (DOMPurify) avant dangerouslySetInnerHTML, et réécrire les URLs médias/liens.


Chapitre suivant : Previews & brouillons en headless.