05 / Cómo funciona Lupus
Del contrato al HTML
La sencillez consiste en tener un protocolo predecible para múltiples orígenes, no en eliminar la implementación del frontend.
Todos los recorridos ↗1. Resolver una URL
La ruta catch-all de Next solicita la misma ruta bajo /ce-api, con prefijo de idioma y query. Drupal decide qué contenido corresponde y aplica acceso. Los errores HTTP no se convierten en éxito: el frontal conserva 403/404 y muestra sus pantallas.
2. Validar la respuesta
parsePage comprueba título, content_format, árbol, metatags y, cuando existe, bloques por región. El contrato inválido produce error explícito. No intentamos ejecutar nombres recibidos como código ni importar componentes arbitrarios.
3. Resolver componentes y slots
El registro React reconoce NttHero, NttSection y los demás componentes editoriales. Casos explícitos representan fichas, teasers, Views y el nuevo ejemplo de módulo/bloque. Los slots anidados se recorren recursivamente. Un elemento no soportado genera un error, no contenido silenciosamente vacío.
4. Generar HTML y añadir interacción
Next genera HTML en servidor. La paginación utiliza el pager recibido; el inspector y el selector usan interacción frontend. Solo renderizamos HTML editorial filtrado del Drupal de confianza; las props de texto se escapan con React. Los assets y la lógica de widgets no viajan dentro del JSON.
Ejemplo real de esta instalación
El mismo renderer presenta el contenido del controlador y el bloque de región. Abre el ejemplo completo y compáralo con su JSON o alterna los cuatro orígenes en el inspector.
Inspeccionar contrato recibido en servidor
{
"title": "Drupal module / CE",
"content_format": "json",
"content": {
"element": "ntt-module-example",
"props": {
"title": "Un módulo Drupal, renderizado en Next.js",
"description": "Esta ruta devuelve un CustomElement desde un controlador PHP. No es una página Canvas ni un nodo. Drupal resuelve el acceso y añade el bloque configurado; React presenta ambos.",
"routeName": "ntt_lupus_examples.module"
}
},
"metatags": {
"meta": [],
"link": []
},
"blocks": {
"sidebar_first": {
"canvas_lupus_theme_page_title": {
"element": "drupal-markup",
"slots": {
"default": "\n\n"
}
},
"canvas_lupus_theme_powered": {
"element": "drupal-markup",
"slots": {
"default": "<span>Powered by <a href=\"https://www.drupal.org\">Drupal</a></span>"
}
}
},
"content": {
"ntt_delivery_note": {
"element": "ntt-delivery-note",
"props": {
"title": "Un bloque Drupal reutilizable",
"description": "Este mensaje procede de un bloque configurado en Drupal, no de Canvas ni de un texto fijo en React."
}
}
},
"header": {
"canvas_lupus_theme_site_branding": {
"element": "drupal-markup",
"slots": {
"default": "<img src=\"https://dev-headless-lupus.pantheonsite.io/themes/custom/canvas_lupus_theme/logo.svg\" alt=\"Home\" />\nCanvas Lupus POC"
}
}
},
"primary_menu": {
"canvas_lupus_theme_main_menu": {
"element": "drupal-markup",
"slots": {
"default": "\n <ul>\n <li>\n <a href=\"/\" data-drupal-link-system-path=\"<front>\">Home</a>\n </li>\n </ul>\n \n\n"
}
}
},
"secondary_menu": {
"canvas_lupus_theme_account_menu": {
"element": "drupal-markup",
"slots": {
"default": "\n <ul>\n <li>\n <a href=\"/user/login\" data-drupal-link-system-path=\"user/login\">Log in</a>\n </li>\n </ul>\n \n\n"
}
}
},
"highlighted": {
"canvas_lupus_theme_messages": {
"element": "drupal-markup",
"slots": {
"default": "<div data-drupal-messages-fallback class=\"hidden\"></div>"
}
}
},
"breadcrumb": {
"canvas_lupus_theme_breadcrumbs": {
"element": "drupal-markup",
"slots": {
"default": " <nav aria-labelledby=\"system-breadcrumb\">\n <h2 id=\"system-breadcrumb\" class=\"visually-hidden\">Breadcrumb</h2>\n <ol>\n <li>\n <a href=\"/\">Home</a>\n </li>\n </ol>\n </nav>\n"
}
}
}
}
}