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’architecture microservices permet de scaler WordPress en découplant le frontend du backend, offrant flexibilité, performance et maintenabilité. Ce guide présente une approche production-ready pour transformer WordPress en un système distribué moderne.
┌──────────────────────────────────────────────────────────────────────┐
│ CDN / Edge Network │
│ (CloudFlare, Fastly, CloudFront) │
└────────────────────────────────┬─────────────────────────────────────┘
│
┌────────────────────────────────▼─────────────────────────────────────┐
│ Frontend Applications │
├──────────────────────┬──────────────────┬───────────────────────────┤
│ Next.js / Nuxt │ React SPA │ Mobile Apps │
│ (SSR/SSG) │ (CSR) │ (React Native/Flutter) │
│ Port: 3000 │ Port: 8080 │ API Consumer │
└──────────────────────┴──────────────────┴───────────────────────────┘
│
┌────────────────────────────────▼─────────────────────────────────────┐
│ API Gateway │
│ (Kong, AWS API Gateway, Tyk) │
│ - Rate Limiting - Authentication - Request Routing │
│ - Caching - Transformation - Monitoring │
└────────────────────────────────┬─────────────────────────────────────┘
│
┌───────────────────────┼───────────────────────┐
│ │ │
┌────────▼─────────┐ ┌─────────▼────────┐ ┌──────────▼───────────┐
│ WordPress REST │ │ GraphQL Service │ │ Custom Microservices │
│ API Service │ │ (WPGraphQL) │ │ (Auth, Search, etc) │
│ Port: 8000 │ │ Port: 8001 │ │ Ports: 8002-8099 │
└────────┬─────────┘ └─────────┬────────┘ └──────────┬───────────┘
│ │ │
└───────────────────────┼───────────────────────┘
│
┌────────────────────────────────▼─────────────────────────────────────┐
│ Shared Services Layer │
├──────────────────┬─────────────────┬────────────────┬───────────────┤
│ Redis Cache │ Message Queue │ Storage │ Logging │
│ (Session) │ (RabbitMQ) │ (S3/Minio) │ (ELK Stack) │
└──────────────────┴─────────────────┴────────────────┴───────────────┘
│
┌────────────────────────────────▼─────────────────────────────────────┐
│ Database Layer │
│ MySQL Cluster (Read/Write Split) │
└──────────────────────────────────────────────────────────────────────┘
[a-zA-Z0-9*-]+)', [
'methods' => 'GET',
'callback' => [ $this, 'get_menu' ],
'permission_callback' => '**return_true',
'args' => [
'location' => [
'required' => true,
'type' => 'string',
],
],
] );
// Site settings endpoint
register_rest_route( 'headless/v1', '/settings', [
'methods' => 'GET',
'callback' => [ $this, 'get_site_settings' ],
'permission_callback' => '**return_true',
] );
// Preview endpoint
register_rest_route( 'headless/v1', '/preview/(?Pd+)', [
'methods' => 'GET',
'callback' => [ $this, 'get_preview' ],
'permission_callback' => function() {
return current_user_can( 'edit_posts' );
},
'args' => [
'id' => [
'required' => true,
'type' => 'integer',
],
],
] );
// Revalidation webhook endpoint
register_rest_route( 'headless/v1', '/revalidate', [
'methods' => 'POST',
'callback' => [ $this, 'trigger_revalidation' ],
'permission_callback' => [ $this, 'verify_webhook_secret' ],
] );
}
/**
* Get navigation menu
*/
public function get_menu( $request ) {
$location = $request['location'];
$locations = get_nav_menu_locations();
if ( ! isset( $locations[ $location ] ) ) {
return new WP_Error( 'menu_not_found', 'Menu location not found', [ 'status' => 404 ] );
}
$menu_items = wp_get_nav_menu_items( $locations[ $location ] );
if ( ! $menu_items ) {
return [];
}
// Build hierarchical menu structure
$menu = $this->build_menu_tree( $menu_items );
return rest_ensure_response( $menu );
}
/**
* Build hierarchical menu tree
*/
private function build_menu_tree( $items, $parent_id = 0 ) {
$branch = [];
foreach ( $items as $item ) {
if ( $item->menu_item_parent == $parent_id ) {
$children = $this->build_menu_tree( $items, $item->ID );
$menu_item = [
'id' => $item->ID,
'title' => $item->title,
'url' => $item->url,
'target' => $item->target ?: '*self',
'classes' => implode( ' ', $item->classes ),
];
if ( $children ) {
$menu_item['children'] = $children;
}
$branch[] = $menu_item;
}
}
return $branch;
}
/**
* Get site settings
*/
public function get_site_settings() {
return rest_ensure_response( [
'name' => get_bloginfo( 'name' ),
'description' => get_bloginfo( 'description' ),
'url' => get_bloginfo( 'url' ),
'language' => get_bloginfo( 'language' ),
'timezone' => get_option( 'timezone_string' ),
'date_format' => get_option( 'date_format' ),
'time_format' => get_option( 'time_format' ),
'posts_per_page' => get_option( 'posts_per_page' ),
] );
}
/**
* Get post preview
*/
public function get_preview( $request ) {
$post_id = $request['id'];
$post = get_post( $post_id );
if ( ! $post ) {
return new WP_Error( 'post_not_found', 'Post not found', [ 'status' => 404 ] );
}
// Get latest autosave
$preview = wp_get_post_autosave( $post_id );
if ( $preview ) {
$post = $preview;
}
setup_postdata( $post );
$response = $this->prepare_post_for_response( $post );
wp_reset_postdata();
return rest_ensure_response( $response );
}
/**
* Optimize REST API response
*/
public function optimize_rest_response( $response, $post, $request ) {
$data = $response->get_data();
// Add featured image data
if ( has_post_thumbnail( $post->ID ) ) {
$thumbnail_id = get_post_thumbnail_id( $post->ID );
$data['featured_image'] = [
'id' => $thumbnail_id,
'url' => wp_get_attachment_image_url( $thumbnail_id, 'full' ),
'sizes' => [
'thumbnail' => wp_get_attachment_image_url( $thumbnail_id, 'thumbnail' ),
'medium' => wp_get_attachment_image_url( $thumbnail_id, 'medium' ),
'large' => wp_get_attachment_image_url( $thumbnail_id, 'large' ),
],
'alt' => get_post_meta( $thumbnail_id, '*wp_attachment_image_alt', true ),
];
}
// Add author data
if ( ! empty( $data['author'] ) ) {
$author = get_user_by( 'id', $data['author'] );
$data['author_data'] = [
'id' => $author->ID,
'name' => $author->display_name,
'avatar' => get_avatar_url( $author->ID ),
'url' => get_author_posts_url( $author->ID ),
];
}
// Add SEO metadata
$data['seo'] = [
'title' => get_post_meta( $post->ID, '*yoast_wpseo_title', true ) ?: $data['title']['rendered'],
'description' => get_post_meta( $post->ID, '*yoast_wpseo_metadesc', true ),
'canonical' => get_permalink( $post->ID ),
'og_image' => get_post_meta( $post->ID, '*yoast_wpseo_opengraph-image', true ),
];
// Add reading time
$content = strip_tags( $data['content']['rendered'] );
$word_count = str_word_count( $content );
$data['reading_time'] = ceil( $word_count / 200 ); // 200 words per minute
$response->set_data( $data );
return $response;
}
/**
* JWT Authentication Handler
*/
public function jwt_auth_handler( $user_id ) {
if ( $user_id ) {
return $user_id;
}
$auth_header = $*SERVER['HTTP_AUTHORIZATION'] ?? '';
if ( ! $auth_header ) {
return $user_id;
}
list( $token ) = sscanf( $auth_header, 'Bearer %s' );
if ( ! $token ) {
return $user_id;
}
try {
$decoded = $this->decode_jwt( $token );
return $decoded->data->user->id ?? $user_id;
} catch ( Exception $e ) {
return $user_id;
}
}
/**
* Decode JWT token
*/
private function decode_jwt( $token ) {
require_once __DIR__ . '/jwt-helper.php';
$secret = defined( 'JWT_AUTH_SECRET_KEY' ) ? JWT_AUTH_SECRET_KEY : '';
return JWT::decode( $token, $secret, [ 'HS256' ] );
}
/**
* Notify frontend of content changes via webhook
*/
public function notify_content_change( $post_id, $post, $update ) {
// Only notify on publish
if ( $post->post_status !== 'publish' ) {
return;
}
// Get frontend revalidation webhook URL
$webhook_url = get_option( 'headless_revalidation_webhook' );
if ( ! $webhook_url ) {
return;
}
// Send async webhook request
wp_remote_post( $webhook_url, [
'blocking' => false,
'body' => json_encode( [
'event' => $update ? 'post_updated' : 'post_created',
'post_id' => $post_id,
'post_type' => $post->post_type,
'slug' => $post->post_name,
'timestamp' => current_time( 'mysql' ),
] ),
'headers' => [
'Content-Type' => 'application/json',
'X-Webhook-Secret' => get_option( 'headless_webhook_secret' ),
],
] );
}
/**
* Verify webhook secret
*/
public function verify_webhook_secret() {
$secret = $*SERVER['HTTP_X_WEBHOOK_SECRET'] ?? '';
$expected = get_option( 'headless_webhook_secret' );
return hash_equals( $expected, $secret );
}
/**
* Trigger revalidation
*/
public function trigger_revalidation( $request ) {
$params = $request->get_json_params();
// Queue revalidation job
do_action( 'headless_revalidate', $params );
return rest_ensure_response( [
'success' => true,
'message' => 'Revalidation triggered',
] );
}
}
// Initialize
new Headless_WordPress();
// next.config.js
module.exports = {
reactStrictMode: true,
images: {
domains: ['wordpress.example.com'],
formats: ['image/avif', 'image/webp'],
},
env: {
WORDPRESS_API_URL: process.env.WORDPRESS_API_URL || 'https://wordpress.example.com/wp-json',
REVALIDATE_SECRET: process.env.REVALIDATE_SECRET,
},
async rewrites() {
return [
{
source: '/blog/:path*',
destination: '/posts/:path*',
},
];
},
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
},
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains; preload'
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin'
}
],
},
];
},
};
// lib/wordpress.js
const WORDPRESS_API_URL = process.env.WORDPRESS_API_URL;
class WordPressAPI {
constructor() {
this.baseURL = WORDPRESS_API_URL;
this.cache = new Map();
}
async fetch(endpoint, options = {}) {
const url = ${this.baseURL}${endpoint};
const cacheKey = ${url}:${JSON.stringify(options)};
// Check cache
if (this.cache.has(cacheKey)) {
const cached = this.cache.get(cacheKey);
if (Date.now() - cached.timestamp < 60000) { // 1 minute cache
return cached.data;
}
}
try {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
throw new Error(API error: ${response.status});
}
const data = await response.json();
// Cache result
this.cache.set(cacheKey, {
data,
timestamp: Date.now(),
});
return data;
} catch (error) {
console.error('WordPress API Error:', error);
throw error;
}
}
async getPosts(params = {}) {
const queryString = new URLSearchParams({
per_page: params.perPage || 10,
page: params.page || 1,
*embed: true,
...params,
}).toString();
return this.fetch(/wp/v2/posts?${queryString});
}
async getPost(slug) {
const posts = await this.fetch(/wp/v2/posts?slug=${slug}&*embed=true);
return posts[0] || null;
}
async getPostPreview(id, token) {
return this.fetch(/headless/v1/preview/${id}, {
headers: {
Authorization: Bearer ${token},
},
});
}
async getPages(params = {}) {
const queryString = new URLSearchParams({
per_page: params.perPage || 10,
page: params.page || 1,
*embed: true,
...params,
}).toString();
return this.fetch(/wp/v2/pages?${queryString});
}
async getPage(slug) {
const pages = await this.fetch(/wp/v2/pages?slug=${slug}&*embed=true);
return pages[0] || null;
}
async getCategories() {
return this.fetch('/wp/v2/categories?per_page=100');
}
async getMenu(location) {
return this.fetch(/headless/v1/menu/${location});
}
async getSiteSettings() {
return this.fetch('/headless/v1/settings');
}
clearCache() {
this.cache.clear();
}
}
export default new WordPressAPI();
// pages/posts/[slug].js
import { useRouter } from 'next/router';
import Head from 'next/head';
import Image from 'next/image';
import WordPressAPI from '../../lib/wordpress';
export default function Post({ post, settings }) {
const router = useRouter();
if (router.isFallback) {
return Loading...;
}
if (!post) {
return Post not found;
}
return (
<>
{post.seo.title} | {settings.name}
{post.featured_image && (
)}
{post.author_data && (
Par {post.author_data.name}
)}
{post.reading_time} min de lecture
>
);
}
export async function getStaticPaths() {
// Get most recent posts for initial build
const posts = await WordPressAPI.getPosts({ per_page: 50 });
const paths = posts.map((post) => ({
params: { slug: post.slug },
}));
return {
paths,
fallback: 'blocking', // ISR for new posts
};
}
export async function getStaticProps({ params }) {
try {
const post = await WordPressAPI.getPost(params.slug);
const settings = await WordPressAPI.getSiteSettings();
if (!post) {
return {
notFound: true,
revalidate: 60,
};
}
return {
props: {
post,
settings,
},
revalidate: 3600, // Revalidate every hour
};
} catch (error) {
console.error('Error fetching post:', error);
return {
notFound: true,
revalidate: 60,
};
}
}
// pages/api/revalidate.js
export default async function handler(req, res) {
// Verify webhook secret
const secret = req.headers['x-webhook-secret'];
if (secret !== process.env.REVALIDATE_SECRET) {
return res.status(401).json({ message: 'Invalid secret' });
}
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' });
}
const { event, post_id, post_type, slug } = req.body;
try {
// Revalidate specific pages based on post type
const pathsToRevalidate = [];
if (post_type === 'post') {
pathsToRevalidate.push(/posts/${slug});
pathsToRevalidate.push('/'); // Homepage
pathsToRevalidate.push('/blog'); // Blog index
} else if (post_type === 'page') {
pathsToRevalidate.push(/${slug});
}
// Revalidate all paths
await Promise.all(
pathsToRevalidate.map(path => res.revalidate(path))
);
return res.json({
revalidated: true,
paths: pathsToRevalidate,
timestamp: new Date().toISOString(),
});
} catch (err) {
console.error('Revalidation error:', err);
return res.status(500).json({
revalidated: false,
error: err.message,
});
}
}
// services/auth/index.js
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const redis = require('redis');
const mysql = require('mysql2/promise');
const app = express();
app.use(express.json());
// Configuration
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRY = '24h';
const REFRESH_TOKEN_EXPIRY = 7 * 24 * 60 * 60; // 7 days
// Redis client for token storage
const redisClient = redis.createClient({
host: process.env.REDIS_HOST || '127.0.0.1',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD,
});
redisClient.on('error', (err) => console.error('Redis error:', err));
redisClient.connect();
// MySQL connection pool
const dbPool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
});
/**
* User login
*/
app.post('/auth/login', async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Missing credentials' });
}
try {
// Get user from WordPress database
const [rows] = await dbPool.execute(
'SELECT ID, user_login, user_pass, user_email, display_name FROM wp_users WHERE user_login = ? OR user_email = ?',
[username, username]
);
if (rows.length === 0) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const user = rows[0];
// Verify password (WordPress uses PHPass)
const isValid = await verifyWordPressPassword(password, user.user_pass);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate tokens
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
// Store refresh token in Redis
await redisClient.setEx(
refresh_token:${user.ID},
REFRESH_TOKEN_EXPIRY,
refreshToken
);
// Log session
await logUserSession(user.ID, req);
res.json({
access_token: accessToken,
refresh_token: refreshToken,
expires_in: JWT_EXPIRY,
user: {
id: user.ID,
username: user.user_login,
email: user.user_email,
display_name: user.display_name,
},
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* Refresh access token
*/
app.post('/auth/refresh', async (req, res) => {
const { refresh_token } = req.body;
if (!refresh_token) {
return res.status(400).json({ error: 'Missing refresh token' });
}
try {
// Verify refresh token
const decoded = jwt.verify(refresh_token, JWT_SECRET);
// Check if token exists in Redis
const storedToken = await redisClient.get(refresh_token:${decoded.user_id});
if (!storedToken || storedToken !== refresh_token) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
// Get user
const [rows] = await dbPool.execute(
'SELECT ID, user_login, user_email, display_name FROM wp_users WHERE ID = ?',
[decoded.user_id]
);
if (rows.length === 0) {
return res.status(401).json({ error: 'User not found' });
}
const user = rows[0];
// Generate new access token
const accessToken = generateAccessToken(user);
res.json({
access_token: accessToken,
expires_in: JWT_EXPIRY,
});
} catch (error) {
console.error('Refresh error:', error);
res.status(401).json({ error: 'Invalid refresh token' });
}
});
/**
* Logout
*/
app.post('/auth/logout', authenticateToken, async (req, res) => {
try {
// Remove refresh token from Redis
await redisClient.del(refresh_token:${req.user.user_id});
res.json({ message: 'Logged out successfully' });
} catch (error) {
console.error('Logout error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* Verify token middleware
*/
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Missing token' });
}
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid token' });
}
req.user = user;
next();
});
}
/**
* Generate access token
*/
function generateAccessToken(user) {
return jwt.sign(
{
user_id: user.ID,
username: user.user_login,
email: user.user_email,
},
JWT_SECRET,
{ expiresIn: JWT_EXPIRY }
);
}
/**
* Generate refresh token
*/
function generateRefreshToken(user) {
return jwt.sign(
{
user_id: user.ID,
type: 'refresh',
},
JWT_SECRET,
{ expiresIn: '7d' }
);
}
/**
* Verify WordPress password
*/
async function verifyWordPressPassword(password, hash) {
// WordPress uses PHPass - simplified version
// In production, use proper PHPass library
const bcryptHash = hash.replace(/^$P$/, '$2a$');
try {
return await bcrypt.compare(password, bcryptHash);
} catch {
return false;
}
}
/**
* Log user session
*/
async function logUserSession(userId, req) {
const ipAddress = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
const userAgent = req.headers['user-agent'];
await dbPool.execute(
'INSERT INTO wp_user_sessions (user_id, ip_address, user_agent, login_time) VALUES (?, ?, ?, NOW())',
[userId, ipAddress, userAgent]
);
}
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'healthy', service: 'auth', timestamp: new Date().toISOString() });
});
const PORT = process.env.PORT || 8002;
app.listen(PORT, () => {
console.log(Auth service running on port ${PORT});
});
# docker-compose.yml
version: '3.8'
services:
# WordPress Backend
wordpress:
image: wordpress:php8.2-fpm
restart: always
environment:
WORDPRESS_DB_HOST: mysql:3306
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}
WORDPRESS_DB_NAME: wordpress
WORDPRESS_CONFIG_EXTRA: |
define('WP_REDIS_HOST', 'redis');
define('WP_REDIS_PORT', 6379);
define('JWT_AUTH_SECRET_KEY', '${JWT_SECRET}');
volumes:
- wordpress_data:/var/www/html
networks:
- backend
# Next.js Frontend
nextjs:
build:
context: ./frontend
dockerfile: Dockerfile
restart: always
environment:
WORDPRESS_API_URL: http://nginx/wp-json
REVALIDATE_SECRET: ${REVALIDATE_SECRET}
NODE_ENV: production
ports:
- "3000:3000"
networks:
- frontend
- backend
depends_on:
- wordpress
# Auth Microservice
auth_service:
build:
context: ./services/auth
dockerfile: Dockerfile
restart: always
environment:
JWT_SECRET: ${JWT_SECRET}
DB_HOST: mysql
DB_USER: wordpress
DB_PASSWORD: ${DB_PASSWORD}
DB_NAME: wordpress
REDIS_HOST: redis
REDIS_PASSWORD: ${REDIS_PASSWORD}
ports:
- "8002:8002"
networks:
- backend
depends_on:
- mysql
- redis
# NGINX Reverse Proxy
nginx:
image: nginx:alpine
restart: always
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- wordpress_data:/var/www/html:ro
ports:
- "80:80"
- "443:443"
networks:
- frontend
- backend
depends_on:
- wordpress
# MySQL Database
mysql:
image: mysql:8.0
restart: always
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
command: >
--default-authentication-plugin=mysql_native_password
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
--max_connections=500
--innodb_buffer_pool_size=1G
networks:
- backend
# Redis Cache
redis:
image: redis:7-alpine
restart: always
command: redis-server --requirepass ${REDIS_PASSWORD} --maxmemory 512mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
networks:
- backend
# RabbitMQ Message Queue
rabbitmq:
image: rabbitmq:3-management-alpine
restart: always
environment:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
ports:
- "5672:5672"
- "15672:15672"
volumes:
- rabbitmq_data:/var/lib/rabbitmq
networks:
- backend
volumes:
wordpress_data:
mysql_data:
redis_data:
rabbitmq_data:
networks:
frontend:
driver: bridge
backend:
driver: bridge
# Monolithic WordPress (baseline)
ab -n 10000 -c 100 https://monolith.example.com/
Requests per second: 145.32 [#/sec]
Time per request: 688.12 [ms]
99th percentile: 2,145ms
# Headless WordPress + Next.js ISR
ab -n 10000 -c 100 https://headless.example.com/
Requests per second: 1,247.89 [#/sec]
Time per request: 80.13 [ms]
99th percentile: 234ms
# Performance improvement: 8.6x throughput, 8.6x faster response
| Metric | Monolithic | Microservices | Improvement |
|---|---|---|---|
| Throughput | 145 req/s | 1,248 req/s | 8.6x |
| P50 Latency | 450ms | 52ms | 8.7x |
| P99 Latency | 2,145ms | 234ms | 9.2x |
| TTFB | 380ms | 28ms | 13.6x |
| Scalability | Vertical | Horizontal | Unlimited |
| Deployment | All-or-nothing | Independent | Flexible |
| Cost (1M req/day) | $450/mo | $320/mo | 29% savings |
L’architecture microservices avec WordPress headless offre:
Recommandations**:
Cette architecture est battle-tested en production sur des sites générant 10M+ pages vues/mois.
Cet article est vivant - corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisis celui qui te convient.