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.
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
namespace, '/search', [
'methods' => 'GET',
'callback' => [$this, 'advancedSearch'],
'permission_callback' => '**return_true',
'args' => [
'query' => [
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
],
'post_type' => [
'default' => 'post',
'type' => 'string',
'enum' => ['post', 'page', 'product'],
],
'per_page' => [
'default' => 10,
'type' => 'integer',
'minimum' => 1,
'maximum' => 100,
],
'page' => [
'default' => 1,
'type' => 'integer',
'minimum' => 1,
],
],
]);
// Endpoint de posts populaires
register_rest_route($this->namespace, '/posts/popular', [
'methods' => 'GET',
'callback' => [$this, 'getPopularPosts'],
'permission_callback' => '**return_true',
'args' => [
'limit' => [
'default' => 10,
'type' => 'integer',
],
'period' => [
'default' => 'week',
'type' => 'string',
'enum' => ['day', 'week', 'month', 'year'],
],
],
]);
// Endpoint de posts liés
register_rest_route($this->namespace, '/posts/(?Pd+)/related', [
'methods' => 'GET',
'callback' => [$this, 'getRelatedPosts'],
'permission_callback' => '**return_true',
'args' => [
'id' => [
'required' => true,
'type' => 'integer',
'validate_callback' => function($param) {
return is_numeric($param) && get_post($param) !== null;
},
],
'limit' => [
'default' => 5,
'type' => 'integer',
],
],
]);
// Endpoint de formulaire de contact
register_rest_route($this->namespace, '/contact', [
'methods' => 'POST',
'callback' => [$this, 'submitContactForm'],
'permission_callback' => '**return_true',
'args' => [
'name' => [
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
],
'email' => [
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_email',
'validate_callback' => function($param) {
return is_email($param);
},
],
'message' => [
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_textarea_field',
],
],
]);
}
/**
* Recherche avancée avec Elasticsearch ou Algolia
*/
public function advancedSearch(WP_REST_Request $request): WP_REST_Response {
$query = $request->get_param('query');
$postType = $request->get_param('post_type');
$perPage = $request->get_param('per_page');
$page = $request->get_param('page');
// Recherche avec ElasticSearch (si disponible)
if (class_exists('ElasticPressElasticsearch')) {
$args = [
's' => $query,
'post_type' => $postType,
'posts_per_page' => $perPage,
'paged' => $page,
'ep_integrate' => true,
];
} else {
// Recherche WordPress standard
$args = [
's' => $query,
'post_type' => $postType,
'posts_per_page' => $perPage,
'paged' => $page,
];
}
$wpQuery = new WP_Query($args);
$posts = array_map(function($post) {
return $this->preparePostForResponse($post);
}, $wpQuery->posts);
$response = new WP_REST_Response([
'results' => $posts,
'total' => $wpQuery->found_posts,
'pages' => $wpQuery->max_num_pages,
'current_page' => $page,
]);
$response->header('X-WP-Total', $wpQuery->found_posts);
$response->header('X-WP-TotalPages', $wpQuery->max_num_pages);
return $response;
}
/**
* Posts populaires basés sur les vues
*/
public function getPopularPosts(WP_REST_Request $request): WP_REST_Response {
$limit = $request->get_param('limit');
$period = $request->get_param('period');
$dateQuery = $this->getDateQueryForPeriod($period);
$args = [
'post_type' => 'post',
'posts_per_page' => $limit,
'meta_key' => '*post_views',
'orderby' => 'meta_value_num',
'order' => 'DESC',
'date_query' => $dateQuery,
];
$query = new WP_Query($args);
$posts = array_map(
fn($post) => $this->preparePostForResponse($post),
$query->posts
);
return new WP_REST_Response([
'posts' => $posts,
'period' => $period,
]);
}
/**
* Posts liés basés sur les taxonomies
*/
public function getRelatedPosts(WP_REST_Request $request): WP_REST_Response {
$postId = $request->get_param('id');
$limit = $request->get_param('limit');
$post = get_post($postId);
// Récupérer les tags/catégories
$tags = wp_get_post_tags($postId, ['fields' => 'ids']);
$categories = wp_get_post_categories($postId, ['fields' => 'ids']);
$args = [
'post_type' => $post->post_type,
'posts_per_page' => $limit,
'post__not_in' => [$postId],
'tax_query' => [
'relation' => 'OR',
],
];
if (!empty($tags)) {
$args['tax_query'][] = [
'taxonomy' => 'post_tag',
'field' => 'term_id',
'terms' => $tags,
];
}
if (!empty($categories)) {
$args['tax_query'][] = [
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => $categories,
];
}
$query = new WP_Query($args);
$posts = array_map(
fn($post) => $this->preparePostForResponse($post),
$query->posts
);
return new WP_REST_Response([
'related_posts' => $posts,
]);
}
/**
* Soumission de formulaire de contact
*/
public function submitContactForm(WP_REST_Request $request): WP_REST_Response {
$name = $request->get_param('name');
$email = $request->get_param('email');
$message = $request->get_param('message');
// Rate limiting
$rateLimiter = app(MonAppSecurityRateLimitRateLimiter::class);
$key = $rateLimiter->keyForIpAndAction('contact_form');
if ($rateLimiter->tooManyAttempts($key)) {
return new WP_REST_Response([
'success' => false,
'message' => 'Too many submissions. Please try again later.',
], 429);
}
$rateLimiter->hit($key, 5); // 5 minutes
// Stocker la soumission
$postId = wp_insert_post([
'post_type' => 'contact_submission',
'post_title' => sprintf('Contact from %s', $name),
'post_status' => 'private',
'meta_input' => [
'*contact_name' => $name,
'*contact_email' => $email,
'*contact_message' => $message,
'*contact_ip' => $*SERVER['REMOTE_ADDR'],
'*contact_user_agent' => $*SERVER['HTTP_USER_AGENT'],
],
]);
if (is_wp_error($postId)) {
return new WP_REST_Response([
'success' => false,
'message' => 'Failed to submit form.',
], 500);
}
// Envoyer l'email
$adminEmail = get_option('admin_email');
$subject = sprintf('[%s] New contact form submission', get_bloginfo('name'));
$body = sprintf(
"Name: %snEmail: %snnMessage:n%s",
$name,
$email,
$message
);
wp_mail($adminEmail, $subject, $body);
return new WP_REST_Response([
'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(WP_Post $post): array {
$thumbnailId = get_post_thumbnail_id($post->ID);
return [
'id' => $post->ID,
'title' => get_the_title($post->ID),
'slug' => $post->post_name,
'excerpt' => get_the_excerpt($post->ID),
'content' => apply_filters('the_content', $post->post_content),
'author' => [
'id' => $post->post_author,
'name' => get_the_author_meta('display_name', $post->post_author),
'avatar' => get_avatar_url($post->post_author),
],
'date' => $post->post_date,
'modified' => $post->post_modified,
'featured_image' => $thumbnailId ? [
'id' => $thumbnailId,
'url' => wp_get_attachment_url($thumbnailId),
'alt' => get_post_meta($thumbnailId, '*wp_attachment_image_alt', true),
'sizes' => $this->getImageSizes($thumbnailId),
] : null,
'categories' => $this->getTermsForPost($post->ID, 'category'),
'tags' => $this->getTermsForPost($post->ID, 'post_tag'),
'link' => get_permalink($post->ID),
];
}
private function getImageSizes(int $attachmentId): array {
$sizes = ['thumbnail', 'medium', 'medium_large', 'large', 'full'];
$imageSizes = [];
foreach ($sizes as $size) {
$image = wp_get_attachment_image_src($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 = get_the_terms($postId, $taxonomy);
if (!$terms || is_wp_error($terms)) {
return [];
}
return array_map(function($term) {
return [
'id' => $term->term_id,
'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
add_action('init', function() {
$manager = new CustomEndpointsManager();
$manager->register();
});
Authentification JWT pour API Headless
secretKey = defined('JWT_AUTH_SECRET_KEY')
? JWT_AUTH_SECRET_KEY
: wp_salt('auth');
}
public function register(): void {
add_action('rest_api_init', [$this, 'registerRoutes']);
add_filter('determine_current_user', [$this, 'determineCurrentUser'], 10);
add_filter('rest_pre_dispatch', [$this, 'validateToken'], 10, 3);
}
public function registerRoutes(): void {
// Endpoint de connexion
register_rest_route($this->namespace, '/auth/login', [
'methods' => 'POST',
'callback' => [$this, 'login'],
'permission_callback' => '**return_true',
'args' => [
'username' => [
'required' => true,
'type' => 'string',
],
'password' => [
'required' => true,
'type' => 'string',
],
],
]);
// Endpoint de refresh token
register_rest_route($this->namespace, '/auth/refresh', [
'methods' => 'POST',
'callback' => [$this, 'refreshToken'],
'permission_callback' => '**return_true',
'args' => [
'refresh_token' => [
'required' => true,
'type' => 'string',
],
],
]);
// Endpoint de validation
register_rest_route($this->namespace, '/auth/validate', [
'methods' => 'POST',
'callback' => [$this, 'validateTokenEndpoint'],
'permission_callback' => '**return_true',
]);
// Endpoint de déconnexion
register_rest_route($this->namespace, '/auth/logout', [
'methods' => 'POST',
'callback' => [$this, 'logout'],
'permission_callback' => 'is_user_logged_in',
]);
}
/**
* Login et génération de token
*/
public function login(WP_REST_Request $request): WP_REST_Response {
$username = $request->get_param('username');
$password = $request->get_param('password');
// Rate limiting
$rateLimiter = app(MonAppSecurityRateLimitRateLimiter::class);
$key = $rateLimiter->keyForIpAndAction('jwt_login');
if ($rateLimiter->tooManyAttempts($key)) {
return new WP_REST_Response([
'success' => false,
'message' => 'Too many login attempts. Please try again later.',
], 429);
}
// Authentification
$user = wp_authenticate($username, $password);
if (is_wp_error($user)) {
$rateLimiter->hit($key, 15); // 15 minutes
return new WP_REST_Response([
'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
update_user_meta($user->ID, '*jwt_refresh_token', wp_hash($refreshToken));
return new WP_REST_Response([
'success' => true,
'data' => [
'access_token' => $accessToken,
'refresh_token' => $refreshToken,
'expires_in' => 3600, // 1 heure
'token_type' => 'Bearer',
'user' => [
'id' => $user->ID,
'username' => $user->user_login,
'email' => $user->user_email,
'display_name' => $user->display_name,
'roles' => $user->roles,
],
],
]);
}
/**
* Refresh du token
*/
public function refreshToken(WP_REST_Request $request): WP_REST_Response {
$refreshToken = $request->get_param('refresh_token');
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 = get_user_by('id', $decoded->data->user->id);
$storedToken = get_user_meta($user->ID, '*jwt_refresh_token', true);
if (!wp_check_password($refreshToken, $storedToken)) {
throw new Exception('Invalid refresh token');
}
// Générer un nouveau access token
$accessToken = $this->generateAccessToken($user);
return new WP_REST_Response([
'success' => true,
'data' => [
'access_token' => $accessToken,
'expires_in' => 3600,
'token_type' => 'Bearer',
],
]);
} catch (Exception $e) {
return new WP_REST_Response([
'success' => false,
'message' => 'Invalid or expired refresh token.',
], 401);
}
}
/**
* Validation du token
*/
public function validateTokenEndpoint(WP_REST_Request $request): WP_REST_Response {
$token = $this->getTokenFromRequest($request);
if (!$token) {
return new WP_REST_Response([
'success' => false,
'message' => 'No token provided.',
], 401);
}
try {
$decoded = JWT::decode($token, new Key($this->secretKey, 'HS256'));
return new WP_REST_Response([
'success' => true,
'data' => [
'user_id' => $decoded->data->user->id,
'expires' => $decoded->exp,
],
]);
} catch (Exception $e) {
return new WP_REST_Response([
'success' => false,
'message' => 'Invalid or expired token.',
], 401);
}
}
/**
* Déconnexion (révocation du refresh token)
*/
public function logout(WP_REST_Request $request): WP_REST_Response {
$userId = get_current_user_id();
// Supprimer le refresh token
delete_user_meta($userId, '*jwt_refresh_token');
return new WP_REST_Response([
'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->get_route();
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 WP_Error(
'jwt_auth_no_token',
'Authorization token not provided.',
['status' => 401]
);
}
try {
JWT::decode($token, new Key($this->secretKey, 'HS256'));
return $result;
} catch (Exception $e) {
return new WP_Error(
'jwt_auth_invalid_token',
'Invalid or expired token.',
['status' => 401]
);
}
}
/**
* Génère un access token
*/
private function generateAccessToken(WP_User $user): string {
$issuedAt = time();
$expire = $issuedAt + (60 * 60); // 1 heure
$payload = [
'iss' => get_bloginfo('url'),
'iat' => $issuedAt,
'exp' => $expire,
'type' => 'access',
'data' => [
'user' => [
'id' => $user->ID,
'username' => $user->user_login,
'email' => $user->user_email,
],
],
];
return JWT::encode($payload, $this->secretKey, 'HS256');
}
/**
* Génère un refresh token
*/
private function generateRefreshToken(WP_User $user): string {
$issuedAt = time();
$expire = $issuedAt + (60 * 60 * 24 * 30); // 30 jours
$payload = [
'iss' => get_bloginfo('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['HTTP_AUTHORIZATION'] ?? '';
if (preg_match('/Bearers+(.*)$/i', $authHeader, $matches)) {
return $matches[1];
}
// Query parameter ?token=
if ($request && $request->get_param('token')) {
return $request->get_param('token');
}
return null;
}
}
// Configuration dans wp-config.php
// define('JWT_AUTH_SECRET_KEY', 'your-secret-key-here');
// Enregistrement
add_action('init', function() {
$jwtAuth = new JWTAuthManager();
$jwtAuth->register();
});
WPGraphQL : GraphQL pour WordPress
Configuration WPGraphQL Avancée
'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',
],
'featured_image' => [
'type' => 'MediaItem',
'description' => 'The featured image',
],
'gallery' => [
'type' => ['list_of' => 'MediaItem'],
'description' => 'Product gallery images',
],
],
]);
// Enum pour le statut
register_graphql_enum_type('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
register_graphql_field('Post', 'viewCount', [
'type' => 'Integer',
'description' => 'Number of views',
'resolve' => function($post) {
return (int) get_post_meta($post->ID, '*post_views', true);
},
]);
register_graphql_field('Post', 'readingTime', [
'type' => 'Integer',
'description' => 'Estimated reading time in minutes',
'resolve' => function($post) {
$wordCount = str_word_count(strip_tags($post->post_content));
return ceil($wordCount / 200); // 200 mots par minute
},
]);
register_graphql_field('Post', 'relatedPosts', [
'type' => ['list_of' => 'Post'],
'description' => 'Related posts',
'args' => [
'limit' => [
'type' => 'Integer',
'defaultValue' => 5,
],
],
'resolve' => function($post, $args) {
$tags = wp_get_post_tags($post->ID, ['fields' => 'ids']);
if (empty($tags)) {
return [];
}
$relatedQuery = new WP_Query([
'post_type' => 'post',
'posts_per_page' => $args['limit'],
'post__not_in' => [$post->ID],
'tax_query' => [
[
'taxonomy' => 'post_tag',
'field' => 'term_id',
'terms' => $tags,
],
],
]);
return $relatedQuery->posts;
},
]);
// Ajouter un champ à User
register_graphql_field('User', 'socialProfiles', [
'type' => 'SocialProfiles',
'description' => 'User social media profiles',
'resolve' => function($user) {
return [
'twitter' => get_user_meta($user->userId, 'twitter_profile', true),
'linkedin' => get_user_meta($user->userId, 'linkedin_profile', true),
'github' => get_user_meta($user->userId, 'github_profile', true),
];
},
]);
register_graphql_object_type('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
register_graphql_field('RootQuery', 'popularPosts', [
'type' => ['list_of' => 'Post'],
'description' => 'Get popular posts',
'args' => [
'limit' => [
'type' => 'Integer',
'defaultValue' => 10,
],
'period' => [
'type' => 'String',
'defaultValue' => 'week',
],
],
'resolve' => function($root, $args) {
$query = new WP_Query([
'post_type' => 'post',
'posts_per_page' => $args['limit'],
'meta_key' => '*post_views',
'orderby' => 'meta_value_num',
'order' => 'DESC',
]);
return $query->posts;
},
]);
// Query de recherche
register_graphql_field('RootQuery', 'search', [
'type' => 'SearchResults',
'description' => 'Search across content types',
'args' => [
'query' => [
'type' => ['non_null' => 'String'],
],
'types' => [
'type' => ['list_of' => 'String'],
'defaultValue' => ['post', 'page'],
],
'limit' => [
'type' => 'Integer',
'defaultValue' => 20,
],
],
'resolve' => function($root, $args) {
$wpQuery = new WP_Query([
's' => $args['query'],
'post_type' => $args['types'],
'posts_per_page' => $args['limit'],
]);
return [
'results' => $wpQuery->posts,
'total' => $wpQuery->found_posts,
];
},
]);
register_graphql_object_type('SearchResults', [
'fields' => [
'results' => ['type' => ['list_of' => 'Post']],
'total' => ['type' => 'Integer'],
],
]);
}
/**
* Enregistre des mutations personnalisées
*/
public function registerCustomMutations(): void {
// Mutation pour soumettre un formulaire de contact
register_graphql_mutation('submitContactForm', [
'inputFields' => [
'name' => [
'type' => ['non_null' => 'String'],
],
'email' => [
'type' => ['non_null' => 'String'],
],
'message' => [
'type' => ['non_null' => 'String'],
],
],
'outputFields' => [
'success' => ['type' => 'Boolean'],
'message' => ['type' => 'String'],
],
'mutateAndGetPayload' => function($input) {
// Validation
if (!is_email($input['email'])) {
return [
'success' => false,
'message' => 'Invalid email address.',
];
}
// Rate limiting
$rateLimiter = app(MonAppSecurityRateLimitRateLimiter::class);
$key = $rateLimiter->keyForIpAndAction('contact_form');
if ($rateLimiter->tooManyAttempts($key)) {
return [
'success' => false,
'message' => 'Too many submissions. Please try again later.',
];
}
$rateLimiter->hit($key, 5);
// Enregistrer la soumission
$postId = wp_insert_post([
'post_type' => 'contact_submission',
'post_title' => sprintf('Contact from %s', sanitize_text_field($input['name'])),
'post_status' => 'private',
'meta_input' => [
'*contact_name' => sanitize_text_field($input['name']),
'*contact_email' => sanitize_email($input['email']),
'*contact_message' => sanitize_textarea_field($input['message']),
],
]);
if (is_wp_error($postId)) {
return [
'success' => false,
'message' => 'Failed to submit form.',
];
}
// Envoyer l'email
wp_mail(
get_option('admin_email'),
'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
register_graphql_mutation('incrementPostViews', [
'inputFields' => [
'postId' => [
'type' => ['non_null' => 'ID'],
],
],
'outputFields' => [
'viewCount' => ['type' => 'Integer'],
],
'mutateAndGetPayload' => function($input) {
$postId = absint($input['postId']);
$views = (int) get_post_meta($postId, '*post_views', true);
$views++;
update_post_meta($postId, '*post_views', $views);
return ['viewCount' => $views];
},
]);
}
public function setJwtSecretKey(): string {
return defined('JWT_AUTH_SECRET_KEY') ? JWT_AUTH_SECRET_KEY : wp_salt('auth');
}
}
// Enregistrement
add_action('init', function() {
if (class_exists('WPGraphQL')) {
$graphql = new GraphQLConfig();
$graphql->register();
}
});
Frontend Next.js avec WordPress Headless
Configuration Next.js
// lib/wordpress.ts
const API_URL = process.env.WORDPRESS_API_URL || 'https://example.com/graphql';
async function fetchAPI(query: string, variables = {}) {
const headers = { 'Content-Type': 'application/json' };
const res = await fetch(API_URL, {
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}
{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 */}
{/* 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
- 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?
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.