Avance 2 min de lecture · 241 mots

WordPress Headless : API REST et Frameworks JavaScript Modernes

Estimated reading time: 1 minute

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


  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.featuredImage.node.altText )}
{node.author.node.avatar && ( {node.author.node.name} )} {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.author.node.name}
{post.readingTime} min de lecture {post.viewCount} vues
{post.featuredImage && ( {post.featuredImage.node.altText )}
{/
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}}> {related.featuredImage.node.altText )}

/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?
  • Une remarque, un retour ?

    Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.