Tutorial Drupal headless: qué vamos a montar
En resumen: Tutorial paso a paso para montar Drupal headless con Next.js desde cero. Activa JSON:API en Drupal, consume el contenido con ISR en Next.js, crea páginas de detalle con rutas dinámicas y despliega en producción.
- Activa JSON:API en Drupal (incluido en core desde Drupal 8.7)
- Consume Drupal desde Next.js con fetch y revalidate (ISR a 60 segundos)
- Genera páginas de detalle con rutas dinámicas y generateStaticParams
- Añade alias de URL, previsualización y revalidación on-demand con next-drupal
En este tutorial de Drupal headless vamos a construir la arquitectura mínima viable desde cero: Drupal actuando únicamente como API de contenido y un frontend en Next.js que lo consume y lo renderiza en servidor. Al terminar tendrás un listado de artículos y sus páginas de detalle funcionando con ISR (regeneración estática incremental), listo para desplegar.
No es un “hola mundo” de juguete: es el mismo esqueleto sobre el que se montan portales reales. Si aún dudas sobre si este enfoque es el adecuado para tu proyecto, antes de nada lee la comparativa Drupal headless vs decoupled, donde explico qué implica cada grado de desacoplamiento.
Requisitos previos
- Un Drupal 10 u 11 funcionando (en local con DDEV es lo más cómodo).
- Node.js 18+ y un proyecto Next.js con App Router.
- Drush para activar módulos.
- Un tipo de contenido con el que trabajar; usaremos
article, que viene de serie en el perfil estándar.
Paso 1: activar JSON:API en Drupal
Drupal incluye JSON:API en core desde Drupal 8.7: no hay que instalar nada, solo activarlo:
drush en jsonapi -y
drush cr
Con esto, cada entidad de Drupal queda expuesta automáticamente bajo la ruta /jsonapi. Compruébalo:
curl https://tu-drupal.ddev.site/jsonapi/node/article | jq '.data[0].attributes.title'
Puntos clave de JSON:API que conviene conocer desde el principio:
- Respeta permisos: los usuarios anónimos solo ven contenido publicado. No expones borradores por accidente.
- Paginación: por defecto devuelve 50 elementos; controla con
page[limit]ypage[offset]. - Campos dispersos: con
fields[node--article]=title,body,createdreduces la respuesta a lo que necesitas. - Relaciones: con
include=field_image,uidincrustas las entidades relacionadas en la misma petición.
Una petición realista para un listado queda así:
curl "https://tu-drupal.ddev.site/jsonapi/node/article?fields[node--article]=title,created,path&page[limit]=10&sort=-created"
Paso 2: consumir Drupal desde Next.js
En el frontend creamos un cliente mínimo. La URL base de Drupal la guardamos en una variable de entorno (DRUPAL_BASE_URL en un .env.local), así el mismo código vale para local, staging y producción.
El listado de artículos con App Router es un Server Component que hace fetch con revalidación:
// app/articulos/page.jsx
async function getArticulos() {
const url = `${process.env.DRUPAL_BASE_URL}/jsonapi/node/article` +
`?fields[node--article]=title,created,path` +
`&page[limit]=10&sort=-created`;
const res = await fetch(url, { next: { revalidate: 60 } });
if (!res.ok) throw new Error('Error al cargar artículos de Drupal');
return res.json();
}
export default async function ArticulosPage() {
const { data } = await getArticulos();
return (
<main>
<h1>Artículos</h1>
<ul>
{data.map((articulo) => (
<li key={articulo.id}>
<a href={`/articulos/${articulo.id}`}>{articulo.attributes.title}</a>
</li>
))}
</ul>
</main>
);
}
El parámetro next: { revalidate: 60 } es la clave: Next.js genera la página de forma estática y la regenera en segundo plano cada 60 segundos como máximo (ISR). Obtenemos el rendimiento de un estático sin renunciar a contenido fresco.
Paso 3: páginas de detalle con rutas dinámicas
Para el detalle usamos el id (UUID) que JSON:API asigna a cada nodo. Con generateStaticParams pre-generamos las páginas existentes y con ISR las nuevas aparecen solas:
// app/articulos/[id]/page.jsx
export async function generateStaticParams() {
const res = await fetch(
`${process.env.DRUPAL_BASE_URL}/jsonapi/node/article?page[limit]=50`
);
const { data } = await res.json();
return data.map((nodo) => ({ id: nodo.id }));
}
async function getArticulo(id) {
const url = `${process.env.DRUPAL_BASE_URL}/jsonapi/node/article/${id}` +
`?fields[node--article]=title,body,created`;
const res = await fetch(url, { next: { revalidate: 60 } });
if (!res.ok) throw new Error('Artículo no encontrado');
return res.json();
}
export default async function ArticuloPage({ params }) {
const { data } = await getArticulo(params.id);
return (
<article>
<h1>{data.attributes.title}</h1>
<div
dangerouslySetInnerHTML={{ __html: data.attributes.body.processed }}
/>
</article>
);
}
Dos detalles importantes:
body.processedya viene con el HTML filtrado por el formato de texto de Drupal, así que es seguro de insertar. Si algún día renderizas texto sin procesar, sanitízalo antes.- Para producción querrás añadir
generateMetadataen estas páginas para que el<title>y la meta description salgan del contenido de Drupal — el SEO de un headless depende por completo del frontend.
Paso 4: de aquí a producción
Con esto tienes el esqueleto funcionando. Para llevarlo a producción real, estos son los siguientes pasos habituales:
- Rutas por alias en lugar de UUID. JSON:API no resuelve aliases de URL por defecto. El módulo Decoupled Router expone un endpoint que traduce
/mi-aliasa su entidad, y es la pieza que usan casi todos los proyectos serios. - El ecosistema next-drupal. El proyecto Next.js for Drupal te da cliente JSON:API, previsualización de borradores, manejo de menús y revalidación on-demand cuando el editor publica. Cuando el proyecto crece, es el camino maduro.
- CORS. Las peticiones de este tutorial van de servidor a servidor, así que CORS no aplica. Si algún día consultas JSON:API desde el navegador, configura
cors.configen elservices.ymlde Drupal. - Caché en Drupal. Aunque el frontend cachee, JSON:API también se beneficia de la caché interna de Drupal. Si el tema te interesa, en la guía de cache bins en Drupal explico cómo funciona a bajo nivel.
Errores que ya he cometido para que no los cometas tú
De la experiencia llevando esta arquitectura a producción con portales universitarios:
- No hagas N peticiones por página. Si tu home necesita noticias, eventos y avisos, pedir cada bloque por separado desde el frontend multiplica la latencia. En el caso real lo resolvimos en el lado de Drupal: las peticiones de nodo —tanto en su versión por defecto (por id) como por alias— devolvían el layout configurado en el tema, con todos sus bloques ya resueltos en una única respuesta. Toda la historia está en la guía de Drupal headless con Next.js.
- No gestiones caché a mano en Node. La tentación existe y acaba en código espagueti. Usa ISR y la caché de Drupal, que es mucho más madura.
- No dejes el SEO para el final. Sin SSR/SSG y sin metadatos generados en el frontend, Google solo ve una página vacía. En un monolito eso salía gratis; aquí es tu responsabilidad.
Conclusión
Montar un Drupal headless con Next.js hoy es sorprendentemente directo: JSON:API viene en core, Next.js resuelve el renderizado con ISR y el despliegue son dos aplicaciones independientes. La dificultad no está en el tutorial, sino en las decisiones que vienen después: alias, previsualización, agregación de datos y caché.
Si quieres ver cómo se resuelven esos problemas en un proyecto real, el siguiente paso es la guía de Drupal headless con Next.js en producción. Y si estás explorando el CMS en general, todo lo que publico sobre él está en el hub de Drupal.
Preguntas frecuentes
¿Cómo montar Drupal headless con Next.js desde cero?
Se construye la arquitectura mínima viable: Drupal actúa solo como API de contenido y un frontend en Next.js lo consume y renderiza en servidor. El proceso tiene cuatro pasos: activar JSON:API en Drupal, crear un cliente en Next.js con fetch y revalidación, generar páginas de detalle con rutas dinámicas y desplegar las dos aplicaciones por separado. Es el mismo esqueleto sobre el que se montan portales reales.
¿Cómo activar JSON:API en Drupal?
JSON:API viene incluido en core desde Drupal 8.7, solo hay que activarlo con `drush en jsonapi -y` y limpiar la caché con `drush cr`. Con esto cada entidad queda expuesta automáticamente bajo la ruta `/jsonapi`. Respeta permisos (los anónimos solo ven contenido publicado), pagina por defecto en 50 elementos con `page[limit]` y `page[offset]`, y admite campos dispersos con `fields[node--article]=...` y relaciones con `include=`.
¿Cómo consumir Drupal desde Next.js con ISR?
Se crea un Server Component que hace fetch a la URL base de Drupal guardada en una variable de entorno, con el parámetro `next: { revalidate: 60 }`. Ese parámetro es la clave del ISR: Next.js genera la página de forma estática y la regenera en segundo plano cada 60 segundos como máximo. Se obtiene el rendimiento de un estático sin renunciar a contenido fresco.
¿Qué es ISR o revalidate en Next.js?
ISR (Incremental Static Regeneration) es la técnica de renderizado que combina la velocidad de las páginas estáticas con contenido fresco. Con `fetch(url, { next: { revalidate: 60 } })`, Next.js genera la página una vez, la sirve desde el edge o CDN, y cada 60 segundos como máximo la regenera en segundo plano con los datos actualizados de Drupal. Las páginas nuevas aparecen automáticamente sin rebuild completo.
¿Cómo crear páginas de detalle en Next.js con rutas dinámicas?
Se usa la carpeta `app/articulos/[id]/page.jsx`. Con `generateStaticParams` se pre-generan las páginas existentes consultando JSON:API, y con el mismo patrón `fetch` + `revalidate: 60` las nuevas aparecen solas gracias al ISR. El `body.processed` de Drupal ya llega con el HTML filtrado por el formato de texto, listo para insertar de forma segura.
¿Qué errores evitar en una arquitectura Drupal headless?
Tres errores comunes: hacer una petición HTTP por cada bloque de la página (multiplica la latencia; conviene agregar en Drupal), gestionar la caché a mano en Node.js (acaba en código espagueti; usa ISR y la caché de Drupal) y dejar el SEO para el final (sin SSR/SSG y sin metadatos generados, Google solo ve una página vacía). En producción se añaden alias con Decoupled Router, previsualización con next-drupal y revalidación on-demand.