05 / How Lupus works

From contract to HTML

Simplicity comes from a predictable protocol for multiple sources, not from eliminating frontend implementation.

All walkthroughs ↗

1. Resolve a URL

The Next catch-all requests the same route under /ce-api with language prefix and query. Drupal decides what content belongs there and applies access. HTTP errors do not become success: the frontend preserves 403/404 and displays its screens.

2. Validate the response

parsePage validates title, content_format, tree, metatags and optional blocks by region. Invalid contracts cause explicit errors. We do not execute received names as code or import arbitrary components.

3. Resolve components and slots

The React registry recognises NttHero, NttSection and other editorial components. Explicit cases handle insights, teasers, Views and the new module/block example. Nested slots are traversed recursively. Unsupported elements cause an error rather than silently empty content.

4. Generate HTML and add interaction

Next generates server HTML. Pagination uses the received pager; the inspector and selector add frontend interaction. We only render filtered editorial HTML from trusted Drupal; React escapes text props. Widget assets and logic do not travel inside the JSON.

Real example from this installation

The same renderer presents controller content and the region block. Open the complete example and compare it with its JSON, or switch between four sources in the inspector.

Inspect the contract received on the server
{
  "title": "Drupal module / CE",
  "content_format": "json",
  "content": {
    "element": "ntt-module-example",
    "props": {
      "title": "A Drupal module, rendered in Next.js",
      "description": "This route returns a CustomElement from a PHP controller. It is not a Canvas page or a node. Drupal resolves access and adds the configured block; React renders both.",
      "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": "A reusable Drupal block",
          "description": "This message comes from a block configured in Drupal, not Canvas or hardcoded React text."
        }
      }
    },
    "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=\"/en\" data-drupal-link-system-path=\"&lt;front&gt;\">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=\"/en/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=\"/en\">Home</a>\n              </li>\n        </ol>\n  </nav>\n"
        }
      }
    }
  }
}

Architecture sources