Connecter Next.js à WordPress
Le front rencontre le back. C'est ici que ça devient concret.
6.1Variables d'environnement
Première étape : configurer l'URL de ton WordPress dans un fichier .env.local :
# .env.local (à la racine de ton projet Next.js)
WP_API_URL=http://mon-cms-headless.local/wp-json
# En production, ce sera :
# WP_API_URL=https://cms.monsite.fr/wp-json6.2Typer les réponses WordPress
Avant de fetcher, on définit les types TypeScript pour les données WordPress :
// lib/types/wordpress.ts
export interface WPPost {
id: number
date: string
slug: string
title: { rendered: string }
content: { rendered: string }
excerpt: { rendered: string }
featured_media: number
categories: number[]
_embedded?: {
'wp:featuredmedia'?: Array<{
source_url: string
alt_text: string
}>
}
}
export interface WPProjet {
id: number
title: string
slug: string
excerpt: string
image: string | null
url_site: string | null
repo_github: string | null
client: string
date_livraison: string
stack: string[]
}
export interface WPPage {
id: number
slug: string
title: { rendered: string }
content: { rendered: string }
}6.3Fonction fetch générique
Crée une fonction utilitaire pour appeler l'API WordPress :
// lib/wordpress.ts
const API_URL = process.env.WP_API_URL
if (!API_URL) {
throw new Error('WP_API_URL non configurée dans .env.local')
}
export async function fetchWP<T>(
endpoint: string,
options?: RequestInit
): Promise<T> {
const url = `${API_URL}${endpoint}`
const res = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
next: { revalidate: 60 }, // ISR : revalide toutes les 60 secondes
})
if (!res.ok) {
throw new Error(`WordPress API error: ${res.status} on ${endpoint}`)
}
return res.json()
}
// Fonctions spécialisées
export async function getPosts(perPage = 10) {
return fetchWP<WPPost[]>(
`/wp/v2/posts?per_page=${perPage}&_embed`
)
}
export async function getPostBySlug(slug: string) {
const posts = await fetchWP<WPPost[]>(
`/wp/v2/posts?slug=${encodeURIComponent(slug)}&_embed`
)
return posts[0] ?? null
}
export async function getProjets() {
return fetchWP<WPProjet[]>('/monsite/v1/projets')
}
export async function getProjetBySlug(slug: string) {
return fetchWP<WPProjet>(
`/monsite/v1/projets/${encodeURIComponent(slug)}`
)
}6.4Afficher les données dans un Server Component
La magie de Next.js : tes composants sont des Server Components par défaut. Tu peux fetcher directement dans le composant :
// app/projets/page.tsx
import { getProjets } from '@/lib/wordpress'
export default async function ProjetsPage() {
const projets = await getProjets()
return (
<main className="max-w-6xl mx-auto px-4 py-12">
<h1 className="text-4xl font-bold mb-8">Nos projets</h1>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{projets.map((projet) => (
<article
key={projet.id}
className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden"
>
{projet.image && (
<img
src={projet.image}
alt={projet.title}
className="w-full h-48 object-cover"
/>
)}
<div className="p-5">
<h2 className="text-lg font-bold mb-2">{projet.title}</h2>
<p className="text-slate-600 text-sm mb-3">{projet.excerpt}</p>
<div className="flex flex-wrap gap-2">
{projet.stack.map((tech) => (
<span
key={tech}
className="px-2 py-1 bg-blue-50 text-blue-600 text-xs
rounded-full font-medium"
>
{tech}
</span>
))}
</div>
</div>
</article>
))}
</div>
</main>
)
}Pas de useEffect, pas de useState, pas de loading spinner. Le contenu est récupéré côté serveur et envoyé en HTML au navigateur. SEO parfait, chargement instantané.
6.5Page dynamique avec slug
// app/projets/[slug]/page.tsx
import { getProjetBySlug, getProjets } from '@/lib/wordpress'
import { notFound } from 'next/navigation'
// Génère les pages statiques au build
export async function generateStaticParams() {
const projets = await getProjets()
return projets.map((p) => ({ slug: p.slug }))
}
export default async function ProjetPage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const projet = await getProjetBySlug(slug)
if (!projet) return notFound()
return (
<main className="max-w-4xl mx-auto px-4 py-12">
<h1 className="text-4xl font-bold mb-4">{projet.title}</h1>
<p className="text-slate-500 mb-8">
Client : {projet.client} - Livré le {projet.date_livraison}
</p>
{projet.image && (
<img
src={projet.image}
alt={projet.title}
className="w-full rounded-xl mb-8"
/>
)}
<div
className="prose max-w-none"
dangerouslySetInnerHTML={{ __html: projet.content }}
/>
</main>
)
}