Introduction : L’Ère du WordPress Découplé
WordPress headless, ou WordPress découplé, représente l’évolution majeure de l’écosystème WordPress en 2025. Cette architecture sépare le backend (WordPress comme CMS) du frontend (application JavaScript moderne), offrant des performances exceptionnelles, une expérience développeur supérieure, et une flexibilité architecturale totale.
Table of Contents
Avec l’adoption massive de frameworks comme Next.js, React, Vue.js et l’essor de GraphQL via WPGraphQL, WordPress s’affirme comme un CMS headless de premier plan, capable de servir des applications web, mobiles, IoT et même des expériences AR/VR.
Architecture Headless : Vue d’Ensemble
┌─────────────────────────────────────────────────────────┐
│ Frontend Layer │
├─────────────────────────────────────────────────────────┤
│ Next.js / React / Vue.js / Nuxt / Gatsby │
│ • Server-Side Rendering (SSR) │
│ • Static Site Generation (SSG) │
│ • Incremental Static Regeneration (ISR) │
│ • Client-Side Rendering (CSR) │
└────────────────┬────────────────────────────────────────┘
│
│ HTTP/HTTPS
│ REST API / GraphQL
│
┌────────────────▼────────────────────────────────────────┐
│ API Layer │
├─────────────────────────────────────────────────────────┤
│ WordPress REST API (wp-json/wp/v2) │
│ WPGraphQL (GraphQL endpoint) │
│ Custom Endpoints │
│ JWT Authentication │
│ Rate Limiting & Caching │
└────────────────┬────────────────────────────────────────┘
│
┌────────────────▼────────────────────────────────────────┐
│ WordPress Backend │
├─────────────────────────────────────────────────────────┤
│ Content Management (Gutenberg) │
│ Custom Post Types & Taxonomies │
│ ACF / Custom Fields │
│ Media Library │
│ User Management │
│ Plugins & Extensions │
└─────────────────────────────────────────────────────────┘
Extension de l’API REST WordPress
Endpoints REST Personnalisés
Gestionnaire d'endpoints REST personnalisés
/
class CustomEndpointsManager {
private string $namespace = 'monapp/v1';
public function register(): void {
addaction('restapiinit', [$this, 'registerRoutes']);
}
public function registerRoutes(): void {
// Endpoint de recherche avancée
registerrestroute($this->namespace, '/search', [
'methods' => 'GET',
'callback' => [$this, 'advancedSearch'],
'permissioncallback' => 'returntrue',
'args' => [
'query' => [
'required' => true,
'type' => 'string',
'sanitizecallback' => 'sanitizetextfield',
],
'posttype' => [
'default' => 'post',
'type' => 'string',
'enum' => ['post', 'page', 'product'],
],
'perpage' => [
'default' => 10,
'type' => 'integer',
'minimum' => 1,
'maximum' => 100,
],
'page' => [
'default' => 1,
'type' => 'integer',
'minimum' => 1,
],
],
]);
// Endpoint de posts populaires
registerrestroute($this->namespace, '/posts/popular', [
'methods' => 'GET',
'callback' => [$this, 'getPopularPosts'],
'permissioncallback' => 'returntrue',
'args' => [
'limit' => [
'default' => 10,
'type' => 'integer',
],
'period' => [
'default' => 'week',
'type' => 'string',
'enum' => ['day', 'week', 'month', 'year'],
],
],
]);
// Endpoint de posts liés
registerrestroute($this->namespace, '/posts/(?Pd+)/related', [
'methods' => 'GET',
'callback' => [$this, 'getRelatedPosts'],
'permissioncallback' => 'returntrue',
'args' => [
'id' => [
'required' => true,
'type' => 'integer',
'validatecallback' => function($param) {
return isnumeric($param) && getpost($param) !== null;
},
],
'limit' => [
'default' => 5,
'type' => 'integer',
],
],
]);
// Endpoint de formulaire de contact
registerrestroute($this->namespace, '/contact', [
'methods' => 'POST',
'callback' => [$this, 'submitContactForm'],
'permissioncallback' => 'returntrue',
'args' => [
'name' => [
'required' => true,
'type' => 'string',
'sanitizecallback' => 'sanitizetextfield',
],
'email' => [
'required' => true,
'type' => 'string',
'sanitizecallback' => 'sanitizeemail',
'validatecallback' => function($param) {
return isemail($param);
},
],
'message' => [
'required' => true,
'type' => 'string',
'sanitizecallback' => 'sanitizetextareafield',
],
],
]);
}
/
Recherche avancée avec Elasticsearch ou Algolia
/
public function advancedSearch(WPRESTRequest $request): WPRESTResponse {
$query = $request->getparam('query');
$postType = $request->getparam('posttype');
$perPage = $request->getparam('perpage');
$page = $request->getparam('page');
// Recherche avec ElasticSearch (si disponible)
if (classexists('ElasticPressElasticsearch')) {
$args = [
's' => $query,
'posttype' => $postType,
'postsperpage' => $perPage,
'paged' => $page,
'epintegrate' => true,
];
} else {
// Recherche WordPress standard
$args = [
's' => $query,
'posttype' => $postType,
'postsperpage' => $perPage,
'paged' => $page,
];
}
$wpQuery = new WPQuery($args);
$posts = arraymap(function($post) {
return $this->preparePostForResponse($post);
}, $wpQuery->posts);
$response = new WPRESTResponse([
'results' => $posts,
'total' => $wpQuery->foundposts,
'pages' => $wpQuery->maxnumpages,
'currentpage' => $page,
]);
$response->header('X-WP-Total', $wpQuery->foundposts);
$response->header('X-WP-TotalPages', $wpQuery->maxnumpages);
return $response;
}
/
Posts populaires basés sur les vues
/
public function getPopularPosts(WPRESTRequest $request): WPRESTResponse {
$limit = $request->getparam('limit');
$period = $request->getparam('period');
$dateQuery = $this->getDateQueryForPeriod($period);
$args = [
'posttype' => 'post',
'postsperpage' => $limit,
'metakey' => 'postviews',
'orderby' => 'metavaluenum',
'order' => 'DESC',
'datequery' => $dateQuery,
];
$query = new WPQuery($args);
$posts = arraymap(
fn($post) => $this->preparePostForResponse($post),
$query->posts
);
return new WPRESTResponse([
'posts' => $posts,
'period' => $period,
]);
}
/
Posts liés basés sur les taxonomies
/
public function getRelatedPosts(WPRESTRequest $request): WPRESTResponse {
$postId = $request->getparam('id');
$limit = $request->getparam('limit');
$post = getpost($postId);
// Récupérer les tags/catégories
$tags = wpgetposttags($postId, ['fields' => 'ids']);
$categories = wpgetpostcategories($postId, ['fields' => 'ids']);
$args = [
'posttype' => $post->posttype,
'postsperpage' => $limit,
'postnotin' => [$postId],
'taxquery' => [
'relation' => 'OR',
],
];
if (!empty($tags)) {
$args['taxquery'][] = [
'taxonomy' => 'posttag',
'field' => 'termid',
'terms' => $tags,
];
}
if (!empty($categories)) {
$args['taxquery'][] = [
'taxonomy' => 'category',
'field' => 'termid',
'terms' => $categories,
];
}
$query = new WPQuery($args);
$posts = arraymap(
fn($post) => $this->preparePostForResponse($post),
$query->posts
);
return new WPRESTResponse([
'relatedposts' => $posts,
]);
}
/
Soumission de formulaire de contact
/
public function submitContactForm(WPRESTRequest $request): WPRESTResponse {
$name = $request->getparam('name');
$email = $request->getparam('email');
$message = $request->getparam('message');
// Rate limiting
$rateLimiter = app(MonAppSecurityRateLimitRateLimiter::class);
$key = $rateLimiter->keyForIpAndAction('contactform');
if ($rateLimiter->tooManyAttempts($key)) {
return new WPRESTResponse([
'success' => false,
'message' => 'Too many submissions. Please try again later.',
], 429);
}
$rateLimiter->hit($key, 5); // 5 minutes
// Stocker la soumission
$postId = wpinsertpost([
'posttype' => 'contactsubmission',
'posttitle' => sprintf('Contact from %s', $name),
'poststatus' => 'private',
'metainput' => [
'contactname' => $name,
'contactemail' => $email,
'contactmessage' => $message,
'contactip' => $SERVER['REMOTEADDR'],
'contactuseragent' => $SERVER['HTTPUSERAGENT'],
],
]);
if (iswperror($postId)) {
return new WPRESTResponse([
'success' => false,
'message' => 'Failed to submit form.',
], 500);
}
// Envoyer l'email
$adminEmail = getoption('adminemail');
$subject = sprintf('[%s] New contact form submission', getbloginfo('name'));
$body = sprintf(
"Name: %snEmail: %snnMessage:n%s",
$name,
$email,
$message
);
wpmail($adminEmail, $subject, $body);
return new WPRESTResponse([
'success' => true,
'message' => 'Thank you for your message. We will get back to you soon.',
]);
}
/
Prépare un post pour la réponse API
/
private function preparePostForResponse(WPPost $post): array {
$thumbnailId = getpostthumbnailid($post->ID);
return [
'id' => $post->ID,
'title' => getthetitle($post->ID),
'slug' => $post->postname,
'excerpt' => gettheexcerpt($post->ID),
'content' => applyfilters('thecontent', $post->postcontent),
'author' => [
'id' => $post->postauthor,
'name' => gettheauthormeta('displayname', $post->postauthor),
'avatar' => getavatarurl($post->postauthor),
],
'date' => $post->postdate,
'modified' => $post->postmodified,
'featuredimage' => $thumbnailId ? [
'id' => $thumbnailId,
'url' => wpgetattachmenturl($thumbnailId),
'alt' => getpostmeta($thumbnailId, 'wpattachmentimagealt', true),
'sizes' => $this->getImageSizes($thumbnailId),
] : null,
'categories' => $this->getTermsForPost($post->ID, 'category'),
'tags' => $this->getTermsForPost($post->ID, 'posttag'),
'link' => getpermalink($post->ID),
];
}
private function getImageSizes(int $attachmentId): array {
$sizes = ['thumbnail', 'medium', 'mediumlarge', 'large', 'full'];
$imageSizes = [];
foreach ($sizes as $size) {
$image = wpgetattachmentimagesrc($attachmentId, $size);
if ($image) {
$imageSizes[$size] = [
'url' => $image[0],
'width' => $image[1],
'height' => $image[2],
];
}
}
return $imageSizes;
}
private function getTermsForPost(int $postId, string $taxonomy): array {
$terms = gettheterms($postId, $taxonomy);
if (!$terms || iswperror($terms)) {
return [];
}
return arraymap(function($term) {
return [
'id' => $term->termid,
'name' => $term->name,
'slug' => $term->slug,
];
}, $terms);
}
private function getDateQueryForPeriod(string $period): array {
$intervals = [
'day' => '1 day ago',
'week' => '1 week ago',
'month' => '1 month ago',
'year' => '1 year ago',
];
return [
[
'after' => $intervals[$period] ?? '1 week ago',
],
];
}
}
// Enregistrement
addaction('init', function() {
$manager = new CustomEndpointsManager();
$manager->register();
});
Authentification JWT pour API Headless
Gestionnaire d'authentification JWT
/
class JWTAuthManager {
private string $secretKey;
private string $namespace = 'monapp/v1';
public function construct() {
$this->secretKey = defined('JWTAUTHSECRETKEY')
? JWTAUTHSECRETKEY
: wpsalt('auth');
}
public function register(): void {
addaction('restapiinit', [$this, 'registerRoutes']);
addfilter('determinecurrentuser', [$this, 'determineCurrentUser'], 10);
addfilter('restpredispatch', [$this, 'validateToken'], 10, 3);
}
public function registerRoutes(): void {
// Endpoint de connexion
registerrestroute($this->namespace, '/auth/login', [
'methods' => 'POST',
'callback' => [$this, 'login'],
'permissioncallback' => 'returntrue',
'args' => [
'username' => [
'required' => true,
'type' => 'string',
],
'password' => [
'required' => true,
'type' => 'string',
],
],
]);
// Endpoint de refresh token
registerrestroute($this->namespace, '/auth/refresh', [
'methods' => 'POST',
'callback' => [$this, 'refreshToken'],
'permissioncallback' => 'returntrue',
'args' => [
'refreshtoken' => [
'required' => true,
'type' => 'string',
],
],
]);
// Endpoint de validation
registerrestroute($this->namespace, '/auth/validate', [
'methods' => 'POST',
'callback' => [$this, 'validateTokenEndpoint'],
'permissioncallback' => 'returntrue',
]);
// Endpoint de déconnexion
registerrestroute($this->namespace, '/auth/logout', [
'methods' => 'POST',
'callback' => [$this, 'logout'],
'permissioncallback' => 'isuserloggedin',
]);
}
/
Login et génération de token
/
public function login(WPRESTRequest $request): WPRESTResponse {
$username = $request->getparam('username');
$password = $request->getparam('password');
// Rate limiting
$rateLimiter = app(MonAppSecurityRateLimitRateLimiter::class);
$key = $rateLimiter->keyForIpAndAction('jwtlogin');
if ($rateLimiter->tooManyAttempts($key)) {
return new WPRESTResponse([
'success' => false,
'message' => 'Too many login attempts. Please try again later.',
], 429);
}
// Authentification
$user = wpauthenticate($username, $password);
if (iswperror($user)) {
$rateLimiter->hit($key, 15); // 15 minutes
return new WPRESTResponse([
'success' => false,
'message' => 'Invalid credentials.',
], 401);
}
$rateLimiter->clear($key);
// Générer les tokens
$accessToken = $this->generateAccessToken($user);
$refreshToken = $this->generateRefreshToken($user);
// Stocker le refresh token
updateusermeta($user->ID, 'jwtrefreshtoken', wphash($refreshToken));
return new WPRESTResponse([
'success' => true,
'data' => [
'accesstoken' => $accessToken,
'refreshtoken' => $refreshToken,
'expiresin' => 3600, // 1 heure
'tokentype' => 'Bearer',
'user' => [
'id' => $user->ID,
'username' => $user->userlogin,
'email' => $user->useremail,
'displayname' => $user->displayname,
'roles' => $user->roles,
],
],
]);
}
/
Refresh du token
/
public function refreshToken(WPRESTRequest $request): WPRESTResponse {
$refreshToken = $request->getparam('refreshtoken');
try {
$decoded = JWT::decode($refreshToken, new Key($this->secretKey, 'HS256'));
// Vérifier que c'est bien un refresh token
if ($decoded->type !== 'refresh') {
throw new Exception('Invalid token type');
}
// Vérifier que le refresh token est stocké
$user = getuserby('id', $decoded->data->user->id);
$storedToken = getusermeta($user->ID, 'jwtrefreshtoken', true);
if (!wpcheckpassword($refreshToken, $storedToken)) {
throw new Exception('Invalid refresh token');
}
// Générer un nouveau access token
$accessToken = $this->generateAccessToken($user);
return new WPRESTResponse([
'success' => true,
'data' => [
'accesstoken' => $accessToken,
'expiresin' => 3600,
'tokentype' => 'Bearer',
],
]);
} catch (Exception $e) {
return new WPRESTResponse([
'success' => false,
'message' => 'Invalid or expired refresh token.',
], 401);
}
}
/
Validation du token
/
public function validateTokenEndpoint(WPRESTRequest $request): WPRESTResponse {
$token = $this->getTokenFromRequest($request);
if (!$token) {
return new WPRESTResponse([
'success' => false,
'message' => 'No token provided.',
], 401);
}
try {
$decoded = JWT::decode($token, new Key($this->secretKey, 'HS256'));
return new WPRESTResponse([
'success' => true,
'data' => [
'userid' => $decoded->data->user->id,
'expires' => $decoded->exp,
],
]);
} catch (Exception $e) {
return new WPRESTResponse([
'success' => false,
'message' => 'Invalid or expired token.',
], 401);
}
}
/
Déconnexion (révocation du refresh token)
/
public function logout(WPRESTRequest $request): WPRESTResponse {
$userId = getcurrentuserid();
// Supprimer le refresh token
deleteusermeta($userId, 'jwtrefreshtoken');
return new WPRESTResponse([
'success' => true,
'message' => 'Logged out successfully.',
]);
}
/
Déterminer l'utilisateur actuel via JWT
/
public function determineCurrentUser($user) {
if ($user) {
return $user;
}
$token = $this->getTokenFromRequest();
if (!$token) {
return $user;
}
try {
$decoded = JWT::decode($token, new Key($this->secretKey, 'HS256'));
if ($decoded->type !== 'access') {
return $user;
}
return $decoded->data->user->id;
} catch (Exception $e) {
return $user;
}
}
/
Valider le token sur chaque requête API
/
public function validateToken($result, $server, $request) {
// Ignorer les endpoints publics
$publicEndpoints = [
'/wp-json/wp/v2/posts',
'/wp-json/wp/v2/pages',
'/wp-json/monapp/v1/auth/login',
];
$route = $request->getroute();
foreach ($publicEndpoints as $endpoint) {
if (strpos($route, $endpoint) === 0) {
return $result;
}
}
// Vérifier le token pour les autres endpoints
$token = $this->getTokenFromRequest($request);
if (!$token) {
return new WPError(
'jwtauthnotoken',
'Authorization token not provided.',
['status' => 401]
);
}
try {
JWT::decode($token, new Key($this->secretKey, 'HS256'));
return $result;
} catch (Exception $e) {
return new WPError(
'jwtauthinvalidtoken',
'Invalid or expired token.',
['status' => 401]
);
}
}
/
Génère un access token
/
private function generateAccessToken(WPUser $user): string {
$issuedAt = time();
$expire = $issuedAt + (60 60); // 1 heure
$payload = [
'iss' => getbloginfo('url'),
'iat' => $issuedAt,
'exp' => $expire,
'type' => 'access',
'data' => [
'user' => [
'id' => $user->ID,
'username' => $user->userlogin,
'email' => $user->useremail,
],
],
];
return JWT::encode($payload, $this->secretKey, 'HS256');
}
/
Génère un refresh token
/
private function generateRefreshToken(WPUser $user): string {
$issuedAt = time();
$expire = $issuedAt + (60 60 24 30); // 30 jours
$payload = [
'iss' => getbloginfo('url'),
'iat' => $issuedAt,
'exp' => $expire,
'type' => 'refresh',
'data' => [
'user' => [
'id' => $user->ID,
],
],
];
return JWT::encode($payload, $this->secretKey, 'HS256');
}
/
Récupère le token depuis la requête
/
private function getTokenFromRequest($request = null): ?string {
// Header Authorization: Bearer {token}
$authHeader = $SERVER['HTTPAUTHORIZATION'] ?? '';
if (pregmatch('/Bearers+(.)$/i', $authHeader, $matches)) {
return $matches[1];
}
// Query parameter ?token=
if ($request && $request->getparam('token')) {
return $request->getparam('token');
}
return null;
}
}
// Configuration dans wp-config.php
// define('JWTAUTHSECRETKEY', 'your-secret-key-here');
// Enregistrement
addaction('init', function() {
$jwtAuth = new JWTAuthManager();
$jwtAuth->register();
});
WPGraphQL : GraphQL pour WordPress
Configuration WPGraphQL Avancée
Configuration GraphQL personnalisée
/
class GraphQLConfig {
public function register(): void {
addaction('graphqlregistertypes', [$this, 'registerCustomTypes']);
addaction('graphqlregistertypes', [$this, 'registerCustomFields']);
addaction('graphqlregistertypes', [$this, 'registerCustomQueries']);
addaction('graphqlregistertypes', [$this, 'registerCustomMutations']);
addfilter('graphqljwtauthsecretkey', [$this, 'setJwtSecretKey']);
}
/
Enregistre des types personnalisés
/
public function registerCustomTypes(): void {
// Type Product
registergraphqlobjecttype('Product', [
'description' => 'A product item',
'fields' => [
'id' => [
'type' => 'ID',
'description' => 'The product ID',
],
'name' => [
'type' => 'String',
'description' => 'The product name',
],
'price' => [
'type' => 'Float',
'description' => 'The product price',
],
'currency' => [
'type' => 'String',
'description' => 'The price currency',
],
'description' => [
'type' => 'String',
'description' => 'The product description',
],
'featuredimage' => [
'type' => 'MediaItem',
'description' => 'The featured image',
],
'gallery' => [
'type' => ['listof' => 'MediaItem'],
'description' => 'Product gallery images',
],
],
]);
// Enum pour le statut
registergraphqlenumtype('ProductStatus', [
'description' => 'Product status',
'values' => [
'DRAFT' => ['value' => 'draft'],
'PUBLISHED' => ['value' => 'publish'],
'ARCHIVED' => ['value' => 'archived'],
],
]);
}
/
Enregistre des champs personnalisés
/
public function registerCustomFields(): void {
// Ajouter des champs aux posts
registergraphqlfield('Post', 'viewCount', [
'type' => 'Integer',
'description' => 'Number of views',
'resolve' => function($post) {
return (int) getpostmeta($post->ID, 'postviews', true);
},
]);
registergraphqlfield('Post', 'readingTime', [
'type' => 'Integer',
'description' => 'Estimated reading time in minutes',
'resolve' => function($post) {
$wordCount = strwordcount(striptags($post->postcontent));
return ceil($wordCount / 200); // 200 mots par minute
},
]);
registergraphqlfield('Post', 'relatedPosts', [
'type' => ['listof' => 'Post'],
'description' => 'Related posts',
'args' => [
'limit' => [
'type' => 'Integer',
'defaultValue' => 5,
],
],
'resolve' => function($post, $args) {
$tags = wpgetposttags($post->ID, ['fields' => 'ids']);
if (empty($tags)) {
return [];
}
$relatedQuery = new WPQuery([
'posttype' => 'post',
'postsperpage' => $args['limit'],
'postnotin' => [$post->ID],
'taxquery' => [
[
'taxonomy' => 'posttag',
'field' => 'termid',
'terms' => $tags,
],
],
]);
return $relatedQuery->posts;
},
]);
// Ajouter un champ à User
registergraphqlfield('User', 'socialProfiles', [
'type' => 'SocialProfiles',
'description' => 'User social media profiles',
'resolve' => function($user) {
return [
'twitter' => getusermeta($user->userId, 'twitterprofile', true),
'linkedin' => getusermeta($user->userId, 'linkedinprofile', true),
'github' => getusermeta($user->userId, 'githubprofile', true),
];
},
]);
registergraphqlobjecttype('SocialProfiles', [
'fields' => [
'twitter' => ['type' => 'String'],
'linkedin' => ['type' => 'String'],
'github' => ['type' => 'String'],
],
]);
}
/
Enregistre des queries personnalisées
/
public function registerCustomQueries(): void {
// Query pour les posts populaires
registergraphqlfield('RootQuery', 'popularPosts', [
'type' => ['listof' => 'Post'],
'description' => 'Get popular posts',
'args' => [
'limit' => [
'type' => 'Integer',
'defaultValue' => 10,
],
'period' => [
'type' => 'String',
'defaultValue' => 'week',
],
],
'resolve' => function($root, $args) {
$query = new WPQuery([
'posttype' => 'post',
'postsperpage' => $args['limit'],
'metakey' => 'postviews',
'orderby' => 'metavaluenum',
'order' => 'DESC',
]);
return $query->posts;
},
]);
// Query de recherche
registergraphqlfield('RootQuery', 'search', [
'type' => 'SearchResults',
'description' => 'Search across content types',
'args' => [
'query' => [
'type' => ['nonnull' => 'String'],
],
'types' => [
'type' => ['listof' => 'String'],
'defaultValue' => ['post', 'page'],
],
'limit' => [
'type' => 'Integer',
'defaultValue' => 20,
],
],
'resolve' => function($root, $args) {
$wpQuery = new WPQuery([
's' => $args['query'],
'posttype' => $args['types'],
'postsperpage' => $args['limit'],
]);
return [
'results' => $wpQuery->posts,
'total' => $wpQuery->foundposts,
];
},
]);
registergraphqlobjecttype('SearchResults', [
'fields' => [
'results' => ['type' => ['listof' => 'Post']],
'total' => ['type' => 'Integer'],
],
]);
}
/
Enregistre des mutations personnalisées
/
public function registerCustomMutations(): void {
// Mutation pour soumettre un formulaire de contact
registergraphqlmutation('submitContactForm', [
'inputFields' => [
'name' => [
'type' => ['nonnull' => 'String'],
],
'email' => [
'type' => ['nonnull' => 'String'],
],
'message' => [
'type' => ['nonnull' => 'String'],
],
],
'outputFields' => [
'success' => ['type' => 'Boolean'],
'message' => ['type' => 'String'],
],
'mutateAndGetPayload' => function($input) {
// Validation
if (!isemail($input['email'])) {
return [
'success' => false,
'message' => 'Invalid email address.',
];
}
// Rate limiting
$rateLimiter = app(MonAppSecurityRateLimitRateLimiter::class);
$key = $rateLimiter->keyForIpAndAction('contactform');
if ($rateLimiter->tooManyAttempts($key)) {
return [
'success' => false,
'message' => 'Too many submissions. Please try again later.',
];
}
$rateLimiter->hit($key, 5);
// Enregistrer la soumission
$postId = wpinsertpost([
'posttype' => 'contactsubmission',
'posttitle' => sprintf('Contact from %s', sanitizetextfield($input['name'])),
'poststatus' => 'private',
'metainput' => [
'contactname' => sanitizetextfield($input['name']),
'contactemail' => sanitizeemail($input['email']),
'contactmessage' => sanitizetextareafield($input['message']),
],
]);
if (iswperror($postId)) {
return [
'success' => false,
'message' => 'Failed to submit form.',
];
}
// Envoyer l'email
wpmail(
getoption('adminemail'),
'New contact form submission',
sprintf(
"Name: %snEmail: %snnMessage:n%s",
$input['name'],
$input['email'],
$input['message']
)
);
return [
'success' => true,
'message' => 'Thank you for your message. We will get back to you soon.',
];
},
]);
// Mutation pour incrémenter les vues
registergraphqlmutation('incrementPostViews', [
'inputFields' => [
'postId' => [
'type' => ['nonnull' => 'ID'],
],
],
'outputFields' => [
'viewCount' => ['type' => 'Integer'],
],
'mutateAndGetPayload' => function($input) {
$postId = absint($input['postId']);
$views = (int) getpostmeta($postId, 'postviews', true);
$views++;
updatepostmeta($postId, 'postviews', $views);
return ['viewCount' => $views];
},
]);
}
public function setJwtSecretKey(): string {
return defined('JWTAUTHSECRETKEY') ? JWTAUTHSECRETKEY : wpsalt('auth');
}
}
// Enregistrement
addaction('init', function() {
if (classexists('WPGraphQL')) {
$graphql = new GraphQLConfig();
$graphql->register();
}
});
Frontend Next.js avec WordPress Headless
Configuration Next.js
// lib/wordpress.ts
const APIURL = process.env.WORDPRESSAPIURL || 'https://example.com/graphql';
async function fetchAPI(query: string, variables = {}) {
const headers = { 'Content-Type': 'application/json' };
const res = await fetch(APIURL, {
method: 'POST',
headers,
body: JSON.stringify({
query,
variables,
}),
});
const json = await res.json();
if (json.errors) {
console.error(json.errors);
throw new Error('Failed to fetch API');
}
return json.data;
}
export async function getAllPosts(preview = false) {
const data = await fetchAPI(
`
query AllPosts {
posts(first: 20, where: { orderby: { field: DATE, order: DESC } }) {
edges {
node {
id
title
slug
date
excerpt
author {
node {
name
avatar {
url
}
}
}
featuredImage {
node {
sourceUrl
altText
}
}
categories {
edges {
node {
name
slug
}
}
}
}
}
}
}
`
);
return data?.posts?.edges;
}
export async function getPostBySlug(slug: string) {
const data = await fetchAPI(
`
query PostBySlug($slug: ID!) {
post(id: $slug, idType: SLUG) {
id
title
content
date
modified
slug
excerpt
readingTime
viewCount
author {
node {
name
avatar {
url
}
description
socialProfiles {
twitter
linkedin
github
}
}
}
featuredImage {
node {
sourceUrl
altText
mediaDetails {
width
height
}
}
}
categories {
edges {
node {
name
slug
}
}
}
tags {
edges {
node {
name
slug
}
}
}
relatedPosts(limit: 3) {
id
title
slug
excerpt
featuredImage {
node {
sourceUrl
altText
}
}
}
seo {
title
metaDesc
opengraphImage {
sourceUrl
}
}
}
}
`,
{ slug }
);
return data?.post;
}
export async function searchContent(query: string, types = ['post', 'page']) {
const data = await fetchAPI(
`
query Search($query: String!, $types: [String]) {
search(query: $query, types: $types, limit: 20) {
results {
id
title
slug
excerpt
date
}
total
}
}
`,
{ query, types }
);
return data?.search;
}
export async function getPopularPosts(limit = 5, period = 'week') {
const data = await fetchAPI(
`
query PopularPosts($limit: Int, $period: String) {
popularPosts(limit: $limit, period: $period) {
id
title
slug
excerpt
viewCount
featuredImage {
node {
sourceUrl
altText
}
}
}
}
`,
{ limit, period }
);
return data?.popularPosts;
}
Page de Blog avec SSG
// app/blog/page.tsx
import { getAllPosts } from '@/lib/wordpress';
import Link from 'next/link';
import Image from 'next/image';
export const revalidate = 3600; // Revalidate every hour
export default async function BlogPage() {
const posts = await getAllPosts();
return (
Blog
{posts.map(({ node }) => (
{node.featuredImage && (
/blog/${node.slug}}>
)}
{node.author.node.avatar && (
)}
{node.author.node.name}
•
/blog/${node.slug}} className="hover:text-blue-600">
{node.title}
html: node.excerpt }}
/>
{node.categories.edges.map(({ node: category }) => (
{category.name}
))}
))}
);
}
Page Article avec ISR
// app/blog/[slug]/page.tsx
import { getPostBySlug, getAllPosts } from '@/lib/wordpress';
import { notFound } from 'next/navigation';
import Image from 'next/image';
import Link from 'next/link';
export const dynamicParams = true;
export const revalidate = 3600; // ISR - Revalidate every hour
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map(({ node }) => ({
slug: node.slug,
}));
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
if (!post) {
return {};
}
return {
title: post.seo?.title || post.title,
description: post.seo?.metaDesc || post.excerpt,
openGraph: {
title: post.seo?.title || post.title,
description: post.seo?.metaDesc || post.excerpt,
images: [
{
url: post.seo?.opengraphImage?.sourceUrl || post.featuredImage?.node.sourceUrl,
},
],
},
};
}
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
if (!post) {
notFound();
}
return (
{/ Header /}
{post.title}
{post.author.node.avatar && (
)}
{post.author.node.name}
•
{post.readingTime} min de lecture
•
{post.viewCount} vues
{post.featuredImage && (
)}
{/ Content /}
html: post.content }}
/>
{/ Tags /}
{post.tags.edges.length > 0 && (
Tags
{post.tags.edges.map(({ node: tag }) => (
/tag/${tag.slug}}
className="px-3 py-1 bg-gray-200 hover:bg-gray-300 rounded-full text-sm"
>
#{tag.name}
))}
)}
{/ Related Posts /}
{post.relatedPosts.length > 0 && (
Articles liés
{post.relatedPosts.map((related) => (
{related.featuredImage && (
/blog/${related.slug}}>
)}
/blog/${related.slug}} className="hover:text-blue-600">
{related.title}
))}
)}
);
}
Conclusion
WordPress headless en 2025 combine :
- APIs REST et GraphQL optimisées
- Authentification JWT sécurisée
- Intégration avec frameworks JavaScript modernes (Next.js, React)
- SSR, SSG, et ISR pour des performances optimales
- Expérience développeur moderne avec TypeScript
- Scalabilité et découplage architectural
Cette architecture permet de construire des expériences web ultra-rapides tout en bénéficiant de la puissance et de la flexibilité de WordPress comme système de gestion de contenu.
Mots-clés : WordPress headless, WPGraphQL, WordPress REST API, Next.js WordPress, React WordPress, headless CMS, WordPress decoupled, JWT authentication WordPress, WordPress API, modern WordPress development
Meta Description
* : Guide expert WordPress headless avec Next.js et GraphQL. Architecture API REST/GraphQL, authentification JWT, SSG/ISR et code production-ready pour applications modernes 2025.
Sources
Headless WordPress With Nextjs & GraphQL
Next.js 13 and WPGraphQL in headless WordPress
Headless WordPress: How to Use WordPress with React/Next.js
WordPress GraphQL vs REST API: Best for Headless CMS?
Build a Headless WordPress App with Next.js and WPGraphQL
Headless WordPress as an API for a Next.js application
Building Fully Headless WordPress Websites With Next.js And GraphQL In 2025
Headless WordPress in 2025 : A Complete Guide
How To Use Headless WordPress With Next.js?