Arquitectura Headless WordPress con Next.js 14 y GraphQL
[INFO] Este artículo asume que tienes experiencia previa con WordPress (tradicional o como CMS) y estás familiarizado con conceptos de React y Node.js. Si eres nuevo en Next.js, te recomiendo repasar los fundamentos de getStaticProps y getServerSideProps antes de sumergirte en la implementación GraphQL.
La arquitectura headless WordPress ha pasado de ser una tendencia experimental a un estándar de facto para proyectos que exigen rendimiento extremo, flexibilidad en el frontend y escalabilidad. Al separar el backend (WordPress como CMS) del frontend (una aplicación React moderna), rompemos las ataduras del tema PHP tradicional y abrimos la puerta a experiencias de usuario dinámicas y altamente optimizadas.
En este artículo, exploraremos en profundidad cómo implementar una arquitectura headless usando Next.js 14 (con su App Router y Server Components) y GraphQL (a través de WPGraphQL). Analizaremos desde la configuración inicial hasta estrategias de despliegue, pasando por patrones de obtención de datos, SEO y rendimiento.
¿Por qué Headless WordPress con Next.js 14 y GraphQL?
La combinación de estas tres tecnologías ofrece ventajas que ningún stack tradicional puede igualar:
- Rendimiento de primer nivel: Next.js 14 ofrece Server Components, que renderizan HTML en el servidor y reducen drásticamente el JavaScript del lado del cliente. Al combinarlo con un WordPress headless, eliminamos la sobrecarga de temas y plugins que ralentizan el TTFB (Time To First Byte).
- Experiencia de desarrollo moderna: GraphQL nos permite consultar exactamente los datos que necesitamos, ni más ni menos. Olvídate de los endpoints REST hinchados de WordPress. Con WPGraphQL, obtenemos posts, páginas, campos personalizados (ACF) y taxonomías en una sola query tipada.
- Escalabilidad y seguridad: Al separar el frontend del backend, podemos escalar cada capa de forma independiente. El frontend puede servirse desde una CDN global (Vercel, Netlify) mientras que WordPress permanece en un servidor privado, reduciendo la superficie de ataque.
- Preparado para WordPress 2025: La comunidad de WordPress está adoptando masivamente el enfoque headless. Herramientas como WPGraphQL, FaunaDB y el ecosistema de plugins headless están madurando rápidamente. Esta arquitectura no es una moda, es el futuro de la plataforma.
Configuración del Backend: WordPress como Headless CMS
Antes de tocar una línea de código en Next.js, debemos preparar nuestro WordPress para servir datos vía GraphQL.
Instalación de WPGraphQL y Plugins Esenciales
- WordPress limpio: Instala una instancia de WordPress (local o en un hosting). No necesitas un tema frontend complejo; con uno básico como Twenty Twenty-Four basta, ya que no se mostrará al usuario final.
- WPGraphQL: Ve a Plugins > Añadir nuevo y busca "WPGraphQL". Actívalo. Este plugin expone un endpoint
/graphqlen tu sitio. - WPGraphQL para ACF (opcional pero muy recomendable): Si usas Advanced Custom Fields, instala el plugin "WPGraphQL for Advanced Custom Fields". Esto expondrá tus campos personalizados en el esquema GraphQL.
- CORS y Seguridad: Para permitir que tu frontend (posiblemente en otro dominio) se conecte, necesitas configurar CORS. Puedes usar un plugin como "Headless Mode" o añadir este código a tu
wp-config.php:
// wp-config.php
define('GRAPHQL_HTTP_METHOD', 'POST');
header("Access-Control-Allow-Origin: *"); // Ajusta en producción
header("Access-Control-Allow-Headers: Content-Type");
Creación de un Tipo de Contenido Personalizado (CPT) y Campos ACF
Supongamos que nuestro sitio headless mostrará "Proyectos" además de posts y páginas.
- Crea un CPT "Proyecto" usando un plugin como "Custom Post Type UI" o código.
- Añade campos ACF:
cliente(texto),fecha_lanzamiento(fecha),url_demo(url). - En la configuración de ACF, asegúrate de que "Show in GraphQL" esté activado y establece un nombre de campo GraphQL (ej:
cliente,fechaLanzamiento).
Ahora, al acceder a https://tusitio.com/graphql con GraphiQL, podrás explorar el esquema y ver tus CPTs y campos.
Configuración del Frontend: Next.js 14 con App Router
Next.js 14 introdujo el App Router como la forma principal de construir aplicaciones. Usaremos Server Components para obtener datos directamente desde el servidor.
Creación del Proyecto
npx create-next-app@latest mi-proyecto-headless --typescript --tailwind --app
cd mi-proyecto-headless
npm install graphql-request
Configuración del Cliente GraphQL
Creamos un cliente reutilizable para conectarnos a WordPress.
// lib/graphql-client.ts
import { GraphQLClient } from 'graphql-request';
const endpoint = process.env.WORDPRESS_API_URL || 'https://tusitio.com/graphql';
export const client = new GraphQLClient(endpoint, {
headers: {
// Si necesitas autenticación (para previews, etc.)
// 'Authorization': `Bearer ${process.env.WORDPRESS_AUTH_TOKEN}`,
},
});
Consulta GraphQL para el Listado de Posts
Creamos una query tipada para obtener los posts. GraphQL nos permite seleccionar campos exactos.
# lib/queries/getPosts.ts
export const GET_POSTS = `
query GetPosts {
posts(first: 10) {
nodes {
id
title
slug
excerpt
featuredImage {
node {
sourceUrl
altText
}
}
categories {
nodes {
name
slug
}
}
}
}
}
`;
Componente de Página Principal con Server Component
Usamos un Server Component para obtener los datos y renderizar HTML estático.
// app/page.tsx
import { client } from '@/lib/graphql-client';
import { GET_POSTS } from '@/lib/queries/getPosts';
import Link from 'next/link';
interface Post {
id: string;
title: string;
slug: string;
excerpt: string;
featuredImage?: {
node: {
sourceUrl: string;
altText: string;
};
};
categories: {
nodes: { name: string; slug: string }[];
};
}
export default async function HomePage() {
const data: { posts: { nodes: Post[] } } = await client.request(GET_POSTS);
const posts = data.posts.nodes;
return (
<main className="container mx-auto p-4">
<h1 className="text-4xl font-bold mb-8">Últimos Artículos</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{posts.map((post) => (
<article key={post.id} className="border rounded-lg p-4 shadow">
{post.featuredImage && (
<img
src={post.featuredImage.node.sourceUrl}
alt={post.featuredImage.node.altText || post.title}
className="w-full h-48 object-cover rounded-t-lg"
/>
)}
<h2 className="text-xl font-semibold mt-2">{post.title}</h2>
<div
className="text-gray-600 mt-2"
dangerouslySetInnerHTML={{ __html: post.excerpt }}
/>
<div className="mt-4 flex gap-2">
{post.categories.nodes.map((cat) => (
<span key={cat.slug} className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded">
{cat.name}
</span>
))}
</div>
<Link href={`/posts/${post.slug}`} className="mt-4 inline-block text-blue-500 hover:underline">
Leer más →
</Link>
</article>
))}
</div>
</main>
);
}
[WARNING] No uses dangerouslySetInnerHTML con contenido no confiable. En este caso, el excerpt de WordPress está sanitizado por el propio CMS, pero si renderizas contenido de usuarios no administradores, considera usar una librería de sanitización como DOMPurify.
Página de Detalle del Post con Rutas Dinámicas
Para mostrar un post individual, creamos una ruta dinámica usando generateStaticParams para generar páginas estáticas en build time.
Consulta para un Post Individual
# lib/queries/getPostBySlug.ts
export const GET_POST_BY_SLUG = `
query GetPostBySlug($slug: String!) {
postBy(slug: $slug) {
id
title
content
date
featuredImage {
node {
sourceUrl
altText
}
}
author {
node {
name
}
}
seo {
title
metaDesc
}
}
}
`;
Componente de Página Dinámica
// app/posts/[slug]/page.tsx
import { client } from '@/lib/graphql-client';
import { GET_POST_BY_SLUG } from '@/lib/queries/getPostBySlug';
import { notFound } from 'next/navigation';
interface PostPageProps {
params: { slug: string };
}
export default async function PostPage({ params }: PostPageProps) {
const { slug } = params;
try {
const data: { postBy: { title: string; content: string; date: string; featuredImage?: { node: { sourceUrl: string; altText: string } }; author: { node: { name: string } }; seo?: { title: string; metaDesc: string } } } = await client.request(GET_POST_BY_SLUG, { slug });
const post = data.postBy;
if (!post) {
notFound();
}
return (
<article className="container mx-auto p-4 max-w-3xl">
{post.featuredImage && (
<img
src={post.featuredImage.node.sourceUrl}
alt={post.featuredImage.node.altText || post.title}
className="w-full h-64 object-cover rounded-lg mb-6"
/>
)}
<h1 className="text-4xl font-bold mb-2">{post.title}</h1>
<p className="text-gray-500 mb-4">
Por {post.author.node.name} | {new Date(post.date).toLocaleDateString('es-ES')}
</p>
<div
className="prose max-w-none"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
</article>
);
} catch (error) {
notFound();
}
}
Generación de Rutas Estáticas
Para que Next.js genere las páginas de los posts en build time (SSG), usamos generateStaticParams.
// app/posts/[slug]/page.tsx (mismo archivo, añadimos la función)
export async function generateStaticParams() {
const GET_ALL_SLUGS = `
query GetAllSlugs {
posts(first: 100) {
nodes {
slug
}
}
}
`;
const data: { posts: { nodes: { slug: string }[] } } = await client.request(GET_ALL_SLUGS);
return data.posts.nodes.map((post) => ({
slug: post.slug,
}));
}
SEO y Metadatos en Next.js 14
Next.js 14 permite exportar metadatos estáticos o dinámicos desde los Server Components.
// app/posts/[slug]/page.tsx (añadir export)
import { Metadata } from 'next';
export async function generateMetadata({ params }: PostPageProps): Promise<Metadata> {
const { slug } = params;
const data: { postBy: { seo?: { title: string; metaDesc: string } } } = await client.request(GET_POST_BY_SLUG, { slug });
const post = data.postBy;
if (!post?.seo) {
return { title: 'Post no encontrado' };
}
return {
title: post.seo.title,
description: post.seo.metaDesc,
};
}
[INFO] Para un SEO avanzado, considera usar el plugin "Yoast SEO" o "Rank Math" en WordPress, ya que exponen metadatos enriquecidos a través de WPGraphQL. Puedes consultar campos como opengraphTitle, twitterImage, etc.
Despliegue y Estrategias de Caching
Despliegue en Vercel (Recomendado)
- Conecta tu repositorio de GitHub a Vercel.
- Añade las variables de entorno:
WORDPRESS_API_URL. - Activa la opción "Incremental Static Regeneration (ISR)" en Next.js para que las páginas se regeneren bajo demanda.
Configuración de ISR
En el componente de página de posts, puedes añadir revalidate para que Next.js regenere la página cada cierto tiempo o cuando se recibe un webhook.
// app/posts/[slug]/page.tsx (dentro del componente)
export const revalidate = 3600; // Revalidar cada hora
Para una regeneración instantánea, configura un webhook en WordPress que notifique a Vercel cuando se publique un post.
Optimización de Imágenes con Next.js
Usa el componente next/image para servir imágenes optimizadas desde WordPress.
import Image from 'next/image';
// En lugar de , usa:
<Image
src={post.featuredImage.node.sourceUrl}
alt={post.featuredImage.node.altText || post.title}
width={800}
height={400}
className="rounded-lg"
/>
Consideraciones de Rendimiento y Seguridad
- Caching persistente: Usa Redis o Vercel Edge Cache para almacenar respuestas GraphQL. Así evitas llamadas repetitivas a WordPress.
- Autenticación: Para contenido privado (previews, borradores), usa Application Passwords de WordPress y pásalos en el header de autorización de GraphQL.
- Limitación de tasa: WPGraphQL puede ser vulnerable a ataques de fuerza bruta. Implementa un plugin de rate limiting en WordPress o usa un proxy como Cloudflare.
Conclusión
La arquitectura headless WordPress con Next.js 14 y GraphQL no solo es viable en 2025, sino que es la opción más potente para proyectos que buscan rendimiento, flexibilidad y una experiencia de desarrollo moderna. Has aprendido a configurar el backend con WPGraphQL, a consumir datos con Server Components, a generar rutas estáticas y a optimizar el SEO. Este stack te prepara para escalar tu contenido sin sacrificar velocidad ni control.
[TIP] No subestimes la fase de planificación del esquema GraphQL. Dedica tiempo a diseñar las queries que necesitarás antes de escribir código. Un esquema bien pensado ahorrará horas de refactorización.
Ahora es tu turno: despliega un proyecto de prueba, juega con las queries y experimenta con ISR. La comunidad headless WordPress te espera.
