How it works

In a nutshell, Lupus Decoupled Drupal provides an API to fetch data about pages from Drupal by using custom elements. That way the backend may compose pages out of high-level components, while a modern frontend framework like Vue.js may be used for rendering.

What is a custom element?

Custom elements refer to non-standard markup elements, e.g. <flag-icon country="nl">Netherlands</flag-icon>. They consist of the element name flag-icon, attributes like country and contained markup in slots (Netherlands). Slots may contain nested custom elements, markup or just 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

To provide custom elements to the frontend, simply define a route with the format value custom_elements. The following example lists some teasers using article-teaser and teaser-listing from above:

Controller/News.php
  <?php
     class NewsController extends ControllerBase {
      public function buildNewsListing() {
        
        $articles[] = CustomElement::create('article-teaser')
          ->setAttribute('href', 'https://example.com/news/1')
          ->setAttribute('excerpt', 'The excerpt of the news entry.');

        $articles[] = CustomElement::create('article-teaser')
          ->setAttribute('href', 'https://example.com/news/2')
          ->setAttribute('excerpt', 'The excerpt of another news entry.');

        return CustomElement::create('teaser-listing')
          ->setSlotFromNestedElements('default', $articles);
      }
     }
MODULE.routing.yml
  MODULE.listing:
      path: '/news'
      defaults:
        _title: 'News Listing'
        _controller: '\Drupal\MODULE\Controller\NewsController::buildNewsListing'
      requirements:
        _format: 'custom_elements'
        _permission: 'access content'

This results in the following custom-elements API response, which can serialize the elements either into JSON (default) or markup: /news page:

/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>",
}

Rendering custom elements

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, which enables you to get started quickly using Vue.js

So this is how some Vue components for the previous custom elements would look like:

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

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

<script setup lang="ts">
const props = defineProps<{
  title: String;
  content?: Object;
}>();
</script>

The above example of TeaserListing.vue assumes that slot content is passed using the JSON serialization of custom elements (default). If you opt for custom elements markup, the <component> would have to be replaced with a <slot></slot>. Refer to Render custom elements docs for more details on that.

Support for Drupal content

Note that often, you don't have to code custom routes like this, since Lupus Decoupled Drupal provides custom-elements output for Drupal's content, like associated pages and the various display modes of your content (e.g. a teaser). The custom element output for Drupal's content entities may be configured to suit your frontend components by using its powerful UI and configuration management system.

First steps

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