Whitepaper

Componer la navegación en un CMS desacoplado

Cómo construimos una navegación híbrida que se mantiene sola para Drupal 11 desacoplado y Next.js: los editores curan el nivel superior, los hijos se completan solos a partir del contenido publicado y cada URL se mantiene canónica.
July 12, 2026
Topics:Headless CMSNavigation & IA
Tags:DrupalNext.js

Cómo una plataforma desacoplada de Drupal 11 + Next.js obtiene una navegación que se mantiene sola (curada donde debe serlo, automática donde puede serlo, correcta en todas partes) y por qué convertimos el resultado en un módulo de código abierto.

El problema escondido dentro de "solo agréguelo al menú"

La navegación es la función más usada de casi cualquier sitio y una de las piezas menos examinadas de su arquitectura. En un sitio Drupal tradicional, los menús se curan a mano: un editor coloca cada enlace manualmente y el tema renderiza lo que contenga el árbol del menú. Ese modelo se rompe silenciosamente en una plataforma desacoplada con mucho contenido, y se rompe de cuatro maneras distintas a la vez.

Nos topamos con las cuatro al hacer algo que debería haber sido trivial: publicar una sola página nueva y agregarla al menú principal.

  • Sin comportamiento híbrido. Publicar una nueva oferta nunca la hace aparecer en la navegación. Cada enlace se coloca a mano, así que el menú es perpetuamente un espejo manual del contenido, y perpetuamente desactualizado.
  • URL de enlaces rotas. Un enlace de menú creado mediante el formulario de contenido apuntaba a una ruta editorial: /node/107/latest. No existe alias de ruta para esa ruta, así que el frontend desacoplado la resolvió literalmente y devolvió un 404. El enlace se veía bien en Drupal y estaba muerto en el sitio.
  • Navegación obsoleta. Publicar o despublicar un nodo no invalidaba la caché de navegación. Y cuando los cambios de menú se revalidaban, la señal iba a un solo frontend mientras los cambios de contenido se propagaban a todos, de modo que un despliegue multientorno podía servir un menú obsoleto indefinidamente.
  • No reutilizable. El único mecanismo de menú dinámico que ya teníamos era un script de arranque con nombres de vocabulario codificados, generado en tiempo de ejecución y nunca confirmado como configuración. Resolvía el problema de un sitio y no podía viajar.

Cada uno de estos es un error pequeño. Juntos describen una capacidad ausente: navegación híbrida que se compone correctamente a sí misma para una arquitectura desacoplada. Este documento explica cómo la construimos y los principios que la hicieron reutilizable.

Principio: el backend es dueño de todo el árbol

La primera decisión, y la de mayores consecuencias, es dónde se compone la navegación. En un sistema desacoplado hay dos opciones: componer el árbol en el frontend (obtener el contenido y decidir la estructura en React) o componerlo en el CMS y dejar que el frontend renderice un único árbol terminado.

Elegimos la composición en el backend, sin dudarlo, por una razón: la navegación es una preocupación transversal que más de un consumidor necesita. Un CMS desacoplado puede alimentar un sitio Next.js hoy, una aplicación nativa mañana y una integración JSON:API después. Si la lógica que decide "qué va debajo de Plataformas" vive en la aplicación React, todos los demás consumidores tienen que reimplementarla, y cada uno la implementará sutilmente mal. Si vive en el CMS, todos los consumidores hacen la misma pregunta y obtienen la misma respuesta.

Así que el CMS es dueño de todo el árbol del menú, incluidas las partes generadas automáticamente, y lo expone mediante exactamente una consulta. El frontend obtiene menu(name: MAIN) y renderiza lo que recibe. No hay código específico de navegación en el frontend ni un esquema específico de navegación que la capa de API deba aprender, porque los elementos generados son enlaces de menú ordinarios, indistinguibles de los colocados a mano para cualquier cosa aguas abajo.

El modelo híbrido: curar el nivel superior, automatizar los hijos

La idea que hace que la navegación sea a la vez editorial y automática es que se trata de trabajos distintos en niveles distintos del árbol. Los editores tienen opiniones firmes sobre el nivel superior: el puñado de secciones que define la arquitectura de la información del sitio. Tienen mucho menos apetito por mantener a mano los hijos: las largas y cambiantes listas de páginas individuales debajo de cada sección.

Así que dejamos que los editores curen el nivel superior exactamente como siempre lo han hecho, y dejamos que cada elemento curado declare de dónde provienen sus hijos. Un enlace de menú padre puede marcarse como "dinámico", y sus hijos se generan y reconcilian continuamente a partir del contenido publicado. Un editor elige la fuente por elemento, entre:

  • Un término de taxonomía: todos los nodos publicados cuyo campo de referencia apunta a un término dado (por ejemplo, todas las Soluciones cuyo tipo es "Plataforma").
  • Un tipo de contenido: todos los nodos publicados de un bundle.
  • Una lista seleccionada a mano: un conjunto explícito y ordenado de nodos, para los casos en que la curaduría es el objetivo.

Cuando el contenido se publica, actualiza, reclasifica o despublica, los padres afectados se reconcilian automáticamente: los enlaces hijos se crean, retitulan, reordenan y podan para coincidir con el conjunto actual de contenido publicado. La reconciliación es un diff: calcula el conjunto deseado de hijos, lo compara con los enlaces que ya posee y hace solo los cambios necesarios. Ejecutarla dos veces seguidas no produce ningún cambio. Esta disciplina de "diff antes de escribir" es lo que permite que la misma operación se ejecute con seguridad en cada guardado de contenido y como un barrido nocturno de autorreparación.

Y algo fundamental: el módulo solo toca los enlaces que él creó. Los enlaces colocados a mano bajo un padre dinámico se dejan exactamente donde el editor los puso. Lo automático y lo curado coexisten en un solo árbol.

El invariante que lo hace seguro para lo desacoplado

El enlace roto /node/107/latest no fue un accidente del flujo de trabajo de un editor; fue un síntoma de un invariante ausente. En un sistema desacoplado, la URL almacenada de un enlace de menú es resuelta por la capa de API en aquello a lo que el frontend navegará. Si el valor almacenado es una ruta editorial o interna, el frontend renderiza fielmente una URL sin página detrás.

La solución es una sola regla, aplicada en todos los lugares donde se escriben enlaces:

Un enlace generado siempre almacena una referencia canónica de entidad, entity:node/<id>, nunca un alias, una ruta interna ni una ruta editorial.

Una referencia canónica de entidad se resuelve, en el momento de la consulta y por idioma, al alias de ruta publicado real del nodo. Es estable ante cambios de alias, inmune a la clase de error de /latest y correcta en todos los idiomas automáticamente. El módulo escribe solo referencias canónicas para los enlaces que gestiona, e incluye un normalizador (disponible como comando Drush y como actualización en tiempo de despliegue) que reescribe cualquier enlace preexistente con rutas editoriales (el problema de /node/107/latest) a su forma canónica. Adoptar el módulo limpia el desorden que lo precede.

Este es el tipo de invariante que vale la pena nombrar en voz alta, porque es invisible hasta que se viola. Es la diferencia entre una navegación que resulta funcionar y una navegación que no puede romperse.

Mantener el árbol actualizado, en todas partes

Un árbol compuesto solo es tan bueno como su frescura. Había que resolver dos problemas de revalidación a la vez.

Primero, los cambios de menú deben invalidar la caché de navegación. Como los hijos generados son enlaces de menú reales, cada creación, actualización, reordenamiento y eliminación que realiza el reconciliador ya dispara los hooks de entidad que invalidan la etiqueta de caché compartida menu: la sincronización automática y la invalidación de caché son el mismo evento, no dos cosas que mantener sincronizadas.

Segundo, la invalidación debe llegar a todos los frontends. Nuestro código original invalidaba la etiqueta de menú en un solo endpoint mientras los cambios de contenido se propagaban a todos los frontends configurados: una asimetría que dejaría a los entornos secundarios sirviendo un menú obsoleto. La cerramos: la revalidación de menú ahora recorre el mismo registro de frontends que usa la revalidación de contenido, y agrupa las invalidaciones idénticas dentro de una solicitud, de modo que una publicación masiva o una reconciliación completa dispara como máximo una invalidación por frontend en lugar de una tormenta.

El resultado es una caché de navegación agresiva (con un tiempo de vida largo, de modo que las lecturas en caliente nunca pagan un viaje de ida y vuelta) y siempre correcta (cualquier cambio de menú, alias o contenido que pueda mover un enlace invalida todas las variantes de idioma en todos los frontends a la vez).

La accesibilidad es responsabilidad del frontend, y ya está resuelta

Como el backend entrega al frontend un árbol coherente, el comportamiento accesible vive por completo en la capa de presentación, donde corresponde. Eso hizo posible lograr una accesibilidad de navegación genuinamente correcta, y sacó a la luz un error común que vale la pena señalar.

La elección intuitiva para una navegación con desplegables es el rol ARIA menubar. Es la elección equivocada para la navegación de un sitio. El rol menubar modela la barra de menú de una aplicación de escritorio: captura las teclas de flecha, saca los elementos del orden normal de tabulación y hace que la tecla Tab se salte toda la navegación, nada de lo cual espera una persona que navega por un sitio web. Peor aún, declarar el rol sin implementar su modelo de teclado completo es activamente dañino, porque un lector de pantalla anuncia un comportamiento que no existe.

El patrón correcto para la navegación de un sitio es la Navegación por Divulgación (Disclosure Navigation): un <nav> real y una lista de enlaces, donde un padre con hijos es un botón que revela su submenú. Tab recorre los elementos como recorre cualquier página; Enter o Espacio alterna un submenú; Escape lo cierra y devuelve el foco al botón; las teclas de flecha se mueven dentro de un submenú abierto. La página actual se marca con aria-current="page", y todo el movimiento se suprime bajo prefers-reduced-motion. Es más usable, más robusto y conforme con WCAG 2.1 AA, y es el patrón que la plataforma incluye.

Listo para más de un idioma

El mismo invariante de referencia canónica que corrige las URL rotas también hace que la navegación sea consciente del idioma sin costo adicional. Los enlaces de menú son entidades de contenido traducibles; sus títulos pueden llevar etiquetas por idioma, mientras la referencia compartida entity:node/<id> se resuelve al alias correcto en cualquier idioma en que se solicite el menú. La API expone un argumento de idioma opcional, y el frontend almacena en caché el árbol resuelto por idioma bajo una única etiqueta de caché compartida, de modo que una sola invalidación refresca todas las traducciones a la vez. Un sitio monolingüe no paga nada por esto; un sitio multilingüe obtiene un menú traducido correcto sin plomería adicional.

De la solución de un sitio a una capacidad de plataforma

El último de nuestros cuatro problemas era que nuestro mecanismo existente no podía viajar. Lo tratamos como el más importante. Una capacidad que resuelve un problema recurrente entre clientes no es una corrección de errores; es un producto disfrazado, y la disciplina de construirlo como tal es lo que convierte el trabajo a medida en un portafolio.

Así que el motor no es código de webcms. Es un módulo Drupal independiente y de código abierto, Menu Autopilot, sin nombres de vocabulario codificados, sin particularidades de cliente y sin supuestos sobre el frontend más allá del único que no puede evitar: que las URL limpias importan. Todo lo específico del sitio permanece en la configuración; el núcleo reutilizable se distribuye como un módulo que cualquier sitio Drupal puede instalar. Es, deliberadamente, para la navegación lo que Pathauto es para los alias de URL: el valor predeterminado automático y basado en configuración que un sitio activa y deja de pensar en él.

Principios que vale la pena conservar

El módulo concreto importa menos que los principios que el trabajo sacó a la luz, que se aplican a cualquier plataforma de contenido desacoplada:

  • Componga las preocupaciones compartidas en el backend. Si más de un consumidor necesita la respuesta, el CMS debe ser dueño de la pregunta. La navegación, como las facetas de búsqueda y las migas de pan, es una preocupación transversal, no un detalle del frontend.
  • Nombre sus invariantes. "Los enlaces siempre almacenan referencias canónicas" es invisible hasta que se viola. Escribirlo, y hacerlo cumplir en el código, es lo que produce un sistema que no puede romperse en lugar de uno que resulta funcionar.
  • Haga que la corrección y la frescura sean el mismo evento. Cuando el mecanismo que cambia los datos es también el mecanismo que invalida su caché, los dos no pueden desviarse.
  • Elija el patrón de accesibilidad correcto, no el intuitivo. Para la navegación de un sitio, eso es Navegación por Divulgación, no menubar.
  • Construya lo hecho a medida como si fuera un producto. Parametrice en lugar de codificar valores, mantenga las particularidades del cliente en la configuración, y la costura para la extracción ya estará ahí cuando surja la oportunidad.

La navegación que se mantiene sola es una función pequeña con una gran superficie. Hacerla bien exigió tratarla como un problema de arquitectura y no como un problema de menú, y la recompensa es un sitio cuya navegación es correcta por construcción, una experiencia accesible que cumple el estándar sin heroicidades y una capacidad reutilizable que sobrevive al contrato que la produjo.