Avance 1 min de lecture · 186 mots

WordPress Headless : API REST et Frameworks JavaScript Modernes

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.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}

{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 */}
{/* 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 :

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.

Laisser un commentaire