How it works

In short, Lupus Decoupled Drupal bridges the gap between your Drupal backend and modern frontend frameworks, giving you the flexibility to leverage the power of Drupal while using a modern framework for rendering. It does that by providing an API for fetching page-data from Drupal, utilizing custom elements to compose pages from high-level components.

What is a custom element?

Custom elements refer to non-standard markup elements, commonly used for components in frontend frameworks. For example: <flag-icon country="nl">Netherlands</flag-icon>. Custom elements consist of:

  • Element name - flag-icon
  • Attributes: such as country
  • Slots: which can contain:
    • Nested custom elements
    • Markup or Plain text

For example, this is how a teaser listing using two custom elements, teaser-listing and article-teaser could look like:

<teaser-listing title="Latest news" icon="news">
    <article-teaser
        href="https://example.com/news/1"
        excerpt="The excerpt of the news entry."
    >
    </article-teaser>
    <article-teaser
        href="https://example.com/news/2"
        excerpt="The excerpt of another news entry."
    >
    </article-teaser>
</teaser-listing>

Providing custom elements

While custom elements can be easily created in a custom Drupal route with code, the most common needs can be served by utilizing the Drupal UI to configure the API output. Lupus Decoupled Drupal already provides custom elements output for the main routes of all content entities and allows customizing it via UI, see Customized API output.

Custom Elements Page-API

Lupus Decoupled Drupal introduces the /ce-api/<route> Drupal API endpoint, which takes care of rendering a regular Drupal request into a custom-elements API response. So Drupal paths like /node/1 or /news would be served by the API as pages rendered with custom element at /ce-api/node/1 and /ce-api/news.

This endpoint results in the following custom-elements API response, which can serialize the elements either into JSON (default) or markup. For example, the API output of a /news page outputting the previous example custom elements would be:

/ce-api/news
{

    "title": "News Listing",
    "content_format": "json",
    "content": {
          "element": "teaser-listing",
          "title": "Latest news",
          "icon": "news",
          "content": [
                {
                      "element": "article-teaser",
                      "href": "https://example.com/news/1",
                      "excerpt": "The excerpt of the news entry."
                },
                {
                      "element": "article-teaser",
                      "href": "https://example.com/news/2",
                      "excerpt": "The excerpt of another news entry."
                }
          ]
    }
}
/ce-api/news?_content_format=markup
{
    "title": "News Listing",
    "content_format": "markup",
    "content": "
      <teaser-listing title=\"Latest news\" icon=\"news\">
        <article-teaser to=\"https://example.com/news/1\" excerpt=\"The excerpt of the news entry.\" slot=\"default\"></article-teaser>
        <article-teaser to=\"https://example.com/news/2\" excerpt=\"The excerpt of another news entry.\" slot=\"default\"></article-teaser>
      </teaser-listing>",
}

The frontend

The frontend typically proxies requests to the Drupal /ce-api endpoint, while preserving the request URI and optionally some request headers. That way, a request to /news is served by the frontend, which requests the backend API at /ce-api/news and takes care of rendering the response. By forwarding the cookie header, the frontend may easily leverage Drupal authentication handling.

While any frontend framework or tooling may be used to render the custom elements API response, Lupus Decoupled Drupal comes with a ready-to-go Nuxt setup by default. This enables you to get started quickly using Vue Single-File-Components, which implements a template and web-standard oriented approach.

Rendering custom elements

Generally, each custom element maps to a Vue component, which is automatically picked up for rendering, when the component is named exactly like the custom element. For example, for the element drupal-markup create the component drupal-markup.vue.

The following example shows how Vue components for the previous custom elements could look like:

ArticleTeaser.vue
<template>
  <article>
    <a :href="href">{{  excerpt }}</a>
  </article>
</template>

<script setup lang="ts">
  defineProps<{
    href: string;
    excerpt: string;
  }>();
</script>
TeaserListing.vue
<template>
  <div class="teaser-listing">
    <h2>Title: {{ title }}</h2>
    <slot>
      <component :is="useDrupalCe().renderCustomElements($attrs.content)" />
    </slot>
  </div>
</template>

<script setup lang="ts">
  defineSlots<{
    default();
  }>()
  defineProps<{
    title: String;
  }>();
</script>

The above example of TeaserListing.vue makes use of slot content, which should contain our rendered article teasers. Vue.js renders contained markup instead of the <slot> element when using markup-based rendering. For rendering the JSON serialization of custom elements (which is used by default), the slot value is passed to the component as data via $attrs.content. This data gets rendered with the help of the renderCustomElements() utility, when no slot content is passed to the component. Thus, this way the slot renders correctly for both JSON and markup based rendering. Refer to Render custom elements docs for more details on that.

First steps

Best, test things yourself by either quickly launching a cloud environment or setting up a new project locally, then continue with your first steps.