title: >- Arquitectura Headless WP + Next.js 16: On-Demand ISR, Webhooks HMAC y Modo Borrador description: >- Diseño de un enlace desacoplado de alta resiliencia entre WordPress 7.0+ (Origin CMS) y Next.js 16 App Router: invalidación granular mediante revalidateTag y cacheTag, webhooks criptográficos HMAC-SHA256 en wp_after_insert_post, previsualizaciones con draftMode() y blindaje del servidor de origen. module: rest-api-headless order: 5 difficulty: advanced estimatedTime: 42 min topics:
Adoptar una arquitectura Headless WordPress (CMS desacoplado) divide el monolito en dos circuitos operacionales independientes: el Content Origin (WordPress 7.0+ en PHP 8.4+), donde los editores gestionan publicaciones, residen los archivos multimedia y operan las reglas de negocio; y la Edge Presentation Layer (Next.js 16 App Router), consagrada al renderizado instantáneo, la entrega perimetral en redes CDN globales e islas interactivas del lado del cliente.
No obstante, las implementaciones ingenuas suelen colapsar en dos puntos críticos:
cache: 'no-store'), cada petición de usuario atraviesa Next.js por completo e impacta de lleno en los procesos PHP-FPM de WordPress, anulando los beneficios de una arquitectura desacoplada. Si por el contrario se configura una generación estática prolongada (SSG), los redactores guardan cambios en wp-admin y no ven las actualizaciones reflejadas en el sitio durante horas.draft / autosave), dado que las APIs públicas bloquean entradas no publicadas y el frontend desconoce la existencia de la revisión.El estándar de ingeniería moderno resuelve la primera problemática mediante la Invalidación Granular Orientada a Eventos (On-Demand ISR): el frontend almacena los datos de forma indefinida con etiquetas de servidor (cacheTag('post-42', 'collection-posts')), mientras que WordPress emite un webhook asíncrono firmado criptográficamente con HMAC-SHA256 al completarse el guardado (wp_after_insert_post) hacia un Route Handler en Next.js 16 que ejecuta revalidateTag(). Para resolver el segundo desafío, se implementa el modo seguro draftMode() de Next.js 16 validado con tokens efímeros firmados.
Conciba la integración de WordPress 7.0+ y Next.js 16 como una Casa de Moneda Central (WordPress Origin) y una Red Global de Vitrinas Blindadas (Next.js Edge Data Cache). Los visitantes nunca ingresan a la casa de moneda; observan instantáneas en vitrinas locales con tiempos de respuesta ínfimos (TTFB < 20ms). La casa de moneda jamás atiende al público en general; solo emite despachos criptográficos seguros (HMAC-SHA256 Webhooks) a las vitrinas: «El elemento post-42 y la taxonomía category-architecture se han modificado: regeneren en segundo plano exactamente esas dos vitrinas».
[Editor en WP Admin] ──► Guarda el post #42 (hook: wp_after_insert_post)
│
▼
[Despachador WP 7.0+] ─► Calcula HMAC-SHA256(payload, WEBHOOK_SECRET)
Emite POST /api/revalidate (timeout: 2s)
│
▼
[Next.js 16 Route Handler] ─► Valida X-Agentic-Signature-256 (timingSafeEqual)
Ejecuta revalidateTag('post:42', 'default')
Ejecuta revalidateTag('posts:list', 'default')
│
▼
[Next.js 16 Edge Cache] ─► Marca etiquetas como Stale de inmediato
y consulta JSON actualizado a WP Origin en segundo planoLas funciones de consulta en el servidor Next.js 16 etiquetan cada llamada REST/GraphQL con claves granulares: post:{slug}, post:{id}, term:{id}.
Una vez que WordPress persiste entradas, metadatos (ACF) y taxonomías de forma atómica, el despachador genera el paquete de invalidación.
El cuerpo del webhook se firma con un secreto compartido (hash_hmac('sha256', ...)) y se despacha en la cabecera X-Agentic-Signature-256.
El Route Handler de Next.js 16 valida la firma con crypto.timingSafeEqual y purga selectivamente las etiquetas afectadas sin recompilar todo el sitio.
Al hacer clic en 'Vista previa' en Gutenberg, se redirige al editor a /api/draft?postId=42&secret=..., activando la cookie __prerender_bypass.
En Fullstack Agentic Lab, los patrones de esta lección constituyen la piedra angular de cualquier despliegue productivo de Headless WordPress:
< 300ms desde el clic en wp-admin hasta la entrega en la CDN) sin requerir una compilación completa del sitio.layout:navigation o term:architecture en todas las rutas afectadas de Next.js 16.draftMode()): Los editores inspeccionan borradores sin publicar dentro del diseño real en Next.js 16 antes de lanzarlos.stale-while-revalidate).Comparativa entre tres generaciones de estrategias de sincronización entre WordPress y Next.js:
A continuación se presenta el artefacto bidireccional (WordPress 7.0+ / PHP 8.4+ y Next.js 16 App Router / TypeScript 5.8) que implementa el canal de sincronización seguro y el modo de previsualización de borradores (draftMode()).
<?php
/**
* PARTE 1: WordPress 7.0+ (PHP 8.4+) — Despachador de Webhooks HMAC y enlaces de Draft Mode.
* Archivo: wp-content/mu-plugins/agentic-headless-sync.php
*/
declare(strict_types=1);
namespace AgenticLab\Headless;
use WP_Post;
defined('ABSPATH') || exit;
final readonly class NextJsEdgeSyncDispatcher
{
public function __construct(
private string $nextWebhookUrl,
private string $nextDraftUrl,
private string $webhookSecret,
) {}
public function registerHooks(): void
{
// CRÍTICO: emplear wp_after_insert_post en lugar de save_post para asegurar
// que metadatos y taxonomías estén confirmados en MySQL antes de que Next.js consulte!
add_action('wp_after_insert_post', $this->dispatchPostRevalidation(...), 20, 4);
add_action('edited_term', $this->dispatchTermRevalidation(...), 20, 3);
add_filter('preview_post_link', $this->rewriteGutenbergPreviewLink(...), 10, 2);
}
/**
* Emite un webhook firmado con HMAC-SHA256 al publicar o modificar una entrada.
*/
public function dispatchPostRevalidation(
int $postId,
WP_Post $post,
bool $update,
?WP_Post $postBefore
): void {
// Omite autoguardados, revisiones y tipos de post técnicos
if (wp_is_post_autosave($postId) || wp_is_post_revision($postId)) {
return;
}
// Invalida solo si el post está publicado actualmente O estuvo publicado previamente (ej: despublicado)
$wasPublished = $postBefore instanceof WP_Post && $postBefore->post_status === 'publish';
$isPublished = $post->post_status === 'publish';
if (!$isPublished && !$wasPublished) {
return;
}
$tags = [
"post:{$post->ID}",
"post:slug:{$post->post_name}",
"collection:{$post->post_type}",
];
// Si cambió el slug de un post publicado, invalida también el slug previo
if ($wasPublished && $postBefore !== null && $postBefore->post_name !== $post->post_name) {
$tags[] = "post:slug:{$postBefore->post_name}";
}
$this->sendSignedPayload([
'event' => 'post.updated',
'postId' => $post->ID,
'postType' => $post->post_type,
'slug' => $post->post_name,
'tags' => array_values(array_unique($tags)),
'paths' => ["/{$post->post_name}", '/'],
'timestamp' => time(),
]);
}
/**
* Purga etiquetas taxonómicas al renombrar categorías o etiquetas.
*/
public function dispatchTermRevalidation(int $termId, int $ttId, string $taxonomy): void
{
$this->sendSignedPayload([
'event' => 'term.updated',
'termId' => $termId,
'taxonomy' => $taxonomy,
'tags' => ["term:{$termId}", "taxonomy:{$taxonomy}"],
'paths' => [],
'timestamp' => time(),
]);
}
/**
* Redirige el botón de previsualización en Gutenberg al endpoint seguro de Next.js Draft Mode.
*/
public function rewriteGutenbergPreviewLink(string $previewLink, WP_Post $post): string
{
$signature = hash_hmac('sha256', "{$post->ID}:{$post->post_type}", $this->webhookSecret);
return add_query_arg([
'postId' => $post->ID,
'postType' => $post->post_type,
'signature' => $signature,
], $this->nextDraftUrl);
}
/**
* Firma el payload JSON con HMAC-SHA256 y realiza una petición HTTP no bloqueante.
*
* @param array<string, mixed> $payload
*/
private function sendSignedPayload(array $payload): void
{
$body = (string) wp_json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$signature = 'sha256=' . hash_hmac('sha256', $body, $this->webhookSecret);
wp_remote_post($this->nextWebhookUrl, [
'timeout' => 2.0,
'redirection' => 0,
'blocking' => false, // No retarda la respuesta visual al editor en Gutenberg
'headers' => [
'Content-Type' => 'application/json',
'X-Agentic-Signature-256' => $signature,
'User-Agent' => 'AgenticLab-WP-Origin/7.0',
],
'body' => $body,
]);
}
}
/*
* ============================================================================
* PARTE 2: Next.js 16 App Router (TypeScript 5.8) — Route Handler /app/api/revalidate/route.ts
* ============================================================================
*
* import { createHmac, timingSafeEqual } from 'node:crypto';
* import { revalidatePath, revalidateTag } from 'next/cache';
* import { NextRequest, NextResponse } from 'next/server';
*
* export async function POST(request: NextRequest): Promise<NextResponse> {
* const rawBody = await request.text();
* const signatureHeader = request.headers.get('x-agentic-signature-256') ?? '';
* const secret = process.env.WP_WEBHOOK_SECRET ?? '';
*
* const expectedSig = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
* const sigBuffer = Buffer.from(signatureHeader);
* const expectedBuffer = Buffer.from(expectedSig);
*
* if (sigBuffer.length !== expectedBuffer.length || !timingSafeEqual(sigBuffer, expectedBuffer)) {
* return NextResponse.json({ error: 'Invalid HMAC signature' }, { status: 401 });
* }
*
* const payload = JSON.parse(rawBody) as { tags: string[]; paths: string[]; timestamp: number };
*
* // Mitigación contra ataques de repetición (margen temporal de 5 minutos)
* if (Math.abs(Math.floor(Date.now() / 1000) - payload.timestamp) > 300) {
* return NextResponse.json({ error: 'Expired webhook timestamp' }, { status: 408 });
* }
*
* for (const tag of payload.tags) {
* revalidateTag(tag, 'default');
* }
* for (const path of payload.paths) {
* revalidatePath(path);
* }
*
* return NextResponse.json({ revalidated: true, tags: payload.tags });
* }
*/
Estandarice las claves entre PHP y TypeScript: post:{id}, post:slug:{slug}, collection:{post_type}, term:{id}, global:settings.
Genere una clave secreta de 256 bits WP_WEBHOOK_SECRET en wp-config.php y .env.production de Next.js, verificando la firma con timingSafeEqual.
Sobrescriba preview_post_link en WordPress para que la acción 'Vista previa' en Gutenberg active draftMode() en Next.js con tokens de autenticación.
Inyecte la cabecera X-Robots-Tag: noindex en el CMS, reescriba enlaces multimedia (wp_get_attachment_url) hacia dominios CDN y asegure las APIs con Cloudflare WAF.