# 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:
`Netherlands`. 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:
```html
```
## Providing custom elements
While custom elements can be easily created in a custom Drupal route [with code](lupus-decoupled.org/drupal/custom-elements), 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](lupus-decoupled.org/guide/customized-api-output).
## Custom Elements Page-API
Lupus Decoupled Drupal introduces the `/ce-api/` 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:
::code-group
```json [/ce-api/news]
{
"title": "News Listing",
"content_format": "json",
"content": {
"element": "teaser-listing",
"props": {
"title": "Latest news",
"icon": "news"
},
"slots": {
"default": [
{
"element": "article-teaser",
"props": {
"href": "https://example.com/news/1",
"excerpt": "The excerpt of the news entry."
}
},
{
"element": "article-teaser",
"props": {
"href": "https://example.com/news/2",
"excerpt": "The excerpt of another news entry."
}
}
]
}
},
"messages": [ ],
"breadcrumbs": [ ],
"metatags": {
"meta": [
{
"name": "title",
"content": "Drupal powered metatags"
}
],
"link": [
{
"rel": "canonical",
"href": "https://example.com/"
}
]
}
}
```
```json [/ce-api/news?_content_format=markup]
{
"title": "News Listing",
"content_format": "markup",
"content": "
",
"messages": [ ],
"breadcrumbs": [ ],
"metatags": {
"meta": [
{
"name": "title",
"content": "Drupal powered metatags"
}
],
"link": [
{
"rel": "canonical",
"href": "https://example.com/"
}
]
}
}
```
::
## 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](lupus-decoupled.org/guide/authentication).
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](https://nuxt.com){rel=""nofollow""} setup by default. This enables you to get started quickly using [Vue](https://vuejs.org){rel=""nofollow""} [Single-File-Components](https://vuejs.org/guide/scaling-up/sfc){rel=""nofollow""}, 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 `article-teaser` create the component `ArticleTeaser.vue`.
The following example shows how Vue components for the previous custom elements could look like:
::code-group
```vue [TeaserListing.vue]
Title: {{ title }}
```
```vue [ArticleTeaser.vue]
```
::
The above example of `TeaserListing.vue` makes use of native Vue slots to render the article teasers. With JSON-based rendering (the default), the Nuxt Drupal CE connector automatically handles slot content - simply use `` in your template and it works out of the box. Markup-based rendering requires additional setup and configuration. Refer to [Render custom elements](lupus-decoupled.org/nuxt/render-custom-elements) docs for more details.
## First steps
Best, test things yourself by either quickly launching a [cloud environment](lupus-decoupled.org/get-started/play-online) or setting up a [new project](lupus-decoupled.org/get-started/create-new-project) locally, then continue with your [first steps](lupus-decoupled.org/get-started/first-steps).
# Play online
## About GitHub Codespaces
[GitHub Codespaces](https://github.com/features/codespaces){rel=""nofollow""} is a cloud-based development environment integrated with
GitHub, allowing you to develop directly within your browser. Github Codespaces
provides 120 hours and 15 GB/month storage for *free*! This allows you to explore
**Lupus Decoupled Drupal** without a local setup.
## Quick Start with Preconfigured Codespaces
Choose one of the following preconfigured development environments based on
your needs (clicking on a link launches that Codespace):
- [Drupal 11 + Nuxt Naked Starter](https://codespaces.new/drunomics/lupus-decoupled-project?quickstart=1){rel=""nofollow""}
- [Drupal 11 + Nuxt with shadcn/ui components](https://codespaces.new/drunomics/lupus-decoupled-project?quickstart=1&devcontainer_path=.devcontainer%2Fnuxt-shadcn%2Fdevcontainer.json){rel=""nofollow""}
- [Drupal 11 + Next.js frontend](https://codespaces.new/drunomics/lupus-decoupled-project?quickstart=1&devcontainer_path=.devcontainer%2Fnextjs%2Fdevcontainer.json){rel=""nofollow""}
## How to Use It?
1. Click on the link for your desired configuration.
2. GitHub will automatically create a new Codespace with the selected setup.
3. Wait for the container to initialize—this may take a few minutes.
4. Once ready, the development environment is fully set up and running in your browser.
5. In the `ports`tab there are links to the running services.
- Frontend is available by clicking the link next to port 3000.
- Backend is at port 80. By default it redirects to frontend. To prevent redirect visit the /user/login path of backend URL.
The environment may take a few minutes to initialize. Once you are up and running, try adding some content nodes and
menu items to see it working . Then continue with your [first steps](lupus-decoupled.org/get-started/first-steps)
## What’s Included in the Setup?
- A fully configured development container, with both Drupal and a frontend.
- Automatic port forwarding for running services.
- Development setup based upon ddev.
# Create a new project
## Local setup with ddev
For setting up a new project, you may want to use the [lupus-decoupled-project](https://github.com/drunomics/lupus-decoupled-project/){rel=""nofollow""} template, which supports both [cloud environments](lupus-decoupled.org/get-started/play-online)
and local docker-based development environments via [DDEV](https://docs.ddev.com){rel=""nofollow""}.
### Prerequisites: ddev & docker
ddev is a ridiculously simple setup for complex development environments, based upon docker compose.
Requirements:
- ddev v1.23 or later. Please follow the [installation instructions](https://ddev.readthedocs.io/en/latest/users/install/ddev-installation/){rel=""nofollow""}
### Launch it
To spin up the project locally run:
```bash
git clone git@github.com:drunomics/lupus-decoupled-project.git
cd lupus-decoupled-project
ddev start
ddev composer install
ddev drush site-install -y --account-pass=admin --site-name='lupus_decoupled' standard
ddev drush pm-enable lupus_decoupled, services_env_parameter -y
# Configure lupus-decoupled frontend base URL
ddev drush config:set lupus_decoupled_ce_api.settings frontend_base_url https://lupus-nuxt.ddev.site -y
# Login and get started adding some test-nodes
ddev drush user-login
```
When using ddev locally, the URLs are by default:
- Frontend: {rel=""nofollow""}
- Backend: {rel=""nofollow""}
Once you are up and running, try adding some content nodes and menu items to see it working and continue with your [first steps](lupus-decoupled.org/get-started/first-steps)
### ddev configuration
#### URLs
If you want to customize URLs, take a look at the [.ddev/config.yaml](https://github.com/drunomics/lupus-decoupled-project/blob/main/.ddev/config.yaml){rel=""nofollow""} file and re-run `ddev start`.
#### Frontend repository
By default, the naked [nuxt-starter](https://github.com/drunomics/lupus-decoupled-nuxt-starter){rel=""nofollow""} frontend repository is used. It can be customized by setting the `FRONTEND_REPOSITORY` via ddev config:
```bash
# Configure a different frontend repository.
ddev config --web-environment-add="FRONTEND_REPOSITORY=https://github.com/drunomics/lupus-decoupled-nuxt-shadcn"
```
### Resources
- [.ddev/config.yaml](https://github.com/drunomics/lupus-decoupled-project/blob/main/.ddev/config.yaml){rel=""nofollow""} - main ddev (default) configuration, can be generated by running `ddev config`
- [ddev docs](https://ddev.readthedocs.io){rel=""nofollow""}
## Custom setup
If preferred, you can incorporate the pieces in your custom setup and/or setup everything from scratch by following the next steps.
### Setting up Drupal
If you don't have an existing Drupal installation yet, or you want to start from scratch,
install Drupal first:
```bash
composer create-project drupal/recommended-project drupal-project
cd drupal-project
```
If you do not have composer installed, see the official [composer installation instructions](https://getcomposer.org/download/){rel=""nofollow""}.
Continue with the Drupal installation, e.g. by using Drupal's quick start command:
```bash
php ./web/core/scripts/drupal quick-start standard
```
The quick-start command uses PHP's built-in webserver to run your site. Just keep it running after installation.
If you prefer a full local development setup based upon docker, check the
[Local development guide](https://www.drupal.org/docs/official_docs/local-development-guide){rel=""nofollow""}.
Add [drush](https://drush.org){rel=""nofollow""} - the Drupal cli:
```bash
composer require drush/drush
# If no global drush launcher is already in use, run
alias drush=$PWD/vendor/bin/drush
```
### Adding Lupus Decoupled Drupal
The [Lupus Custom Elements renderer module](https://www.drupal.org/project/lupus_ce_renderer){rel=""nofollow""} lets Drupal provide
an API backend rendering custom elements. Add the module and its dependencies, then enable the module
via the UI or if installed, via [drush](https://drush.org){rel=""nofollow""}.
```bash
composer require drupal/lupus_decoupled
drush en lupus_decoupled -y
drush user-login
```
Test your installation by opening `http://127.0.0.1:8888/ce-api/` - when logged in,
you should see a JSON response with the following `content` property:
```html
Welcome to your custom-elements enabled Drupal site!
```
### Setting up Nuxt
::code-group
```bash [npx]
npx nuxi@latest init
```
```bash [pnpm]
pnpm dlx nuxi@latest init
```
::
Answer the questions as preferred, generally the defaults work fine.
Once the project is created, you can remove the example components.
For further details, please refer to the [official Nuxt installation docs](https://nuxt.com/docs/getting-started/installation){rel=""nofollow""}.
### Adding the Nuxt connector module
Please refer to the [Setup steps](lupus-decoupled.org/nuxt/setup) then continue below.
### Run nuxt
Ready to go! Start testing by running Nuxt in development mode:
::code-group
```bash [npm]
npm run dev
```
```bash [yarn]
yarn dev
```
```bash [pnpm]
pnpm dev
```
::
Now, when accessing the nuxt dev server (e.g. at {rel=""nofollow""})
you should see a naked page rendered, having the "Home" breadcrumb
and the message shown to logged-out users on the frontpage:
"You are not authorized to access this page."
### Connecting the dots
In your Drupal backend, set the URL of your frontend site, either via UI or via console:
```bash
drush config:set lupus_decoupled_ce_api.settings frontend_base_url http://localhost:3000 -y
```
# First steps
This page provides a short playbook which introduces you to the most important features of Lupus Decoupled Drupal. It only takes 1-2 minutes to follow:
## Creating and viewing content
1. Create an article
In the backend, login and go to `/node/add/article` to create some first example content. Fill in some random content and save it.
2. After saving, you are redirected to the frontend, which renders the basic data of your content. You can customize that easily, as described under the [Customized API output](lupus-decoupled.org/guide/customized-api-output) page. For now, stay here and remember the frontend URL of your page, e.g. `/node/1`.
3. On the frontend go the user login, e.g. `/user/login?destination=/`. Login with your usual Drupal credentials, e.g. as admin or editor account. For the Gitpod demo environments you can log in with `admin` and password `lupus123`.
4. After logging in, go back to your created content in the frontend, e.g. `/node/1`. Take note of the local task links (or "Drupal-tabs") that appear now.
5. Click the `Edit` task link, to get back to the Drupal edit page. Now, let's change the *URL-alias* of the content in the right sidebar, e.g. to `/example-path`. Also, in the sidebar, enable the option to provide a menu item and keep the defaults.
6. Save the form and take note of the changes appearing in the frontend:
- The URL of the content changed to `/example-path`.
- A menu items is appearing in the main menu, which is part of the site header.
- A success message is provided by Drupal and shown in the frontend.
7. Optionally, you may log out in the frontend, separate from the backend, by visiting the `/user/logout` path in the frontend.
## Inspecting API output of content
1. After creating content, it's listed in Drupal's content management screen at `/admin/content/node` in the backend.
2. To inspect the custom element API output of some content, use the drop-down next to the `Edit` operation button on the right and choose the entry `API output`. It shows the API output of the associated page.
3. Optionally, switch between markup and JSON serialization by appending the query parameter `?_content_format=markup` or `?_content_format=json` (default) to the URL of the API output.
4. Continue with [customizing the API output](lupus-decoupled.org/guide/customized-api-output).
# Customized API Output
Lupus Decoupled Drupal provides custom elements output for all content entities. This includes the page of a content node ("Full content"), just as the individual view-modes that may be used in content listings, e.g. teasers (Drupal allows the configuration of any number of view-modes).
A content entity can be rendered into custom elements via three methods, while the method used can be configured by view-mode. For example, when editing a content type at `admin/structure/types` go to the tab "Manage custom element" to configure your method.
**Note**: This requires the Custom Elements UI submodule, make sure it is enabled.
The three methods are:
- Custom Element Display Configuration - The typically used default method. It allows fine-grained control of the output via UI. Please see below for more details.
- Layout builder - When enabling the "Layout Builder" module for a view-mode, the layout builder may be used to allow editors to compose pages that are displayed using custom elements. Refer to the [Layout Builder guide](lupus-decoupled.org/guide/layout-builder) for details. Activate it by ticking the "Use Layout Builder" checkbox in the UI; this checkbox is visible only after a layout is enabled through "Manage display".
- Automatic processing (advanced) - A method that allows taking full control of the output via PHP code, as outlined [here](lupus-decoupled.org/drupal/custom-element-processors). Activate it by ticking the "Automatic processing" checkbox in the UI.
## Custom Element Display Configuration
For each view-mode, the display configuration may be used to configure the `name` of the custom element used. Next, for each content field the data may be mapped to a slot or property with a configurable name:

For each field various options are available:
- **Auto** - Provides a reasonable default based upon [automatic processing](lupus-decoupled.org/drupal/custom-element-processors) of the field.
- **Raw** - Provides the raw Drupal field data.
- **Flattened** - Instead of containing a nested object for individual data properties of a field, the output is flattened and each property gets added to the main element, prefixed by the property name. For example, a field with the data properties `value` and `format` would get flattened to `foo-value` and `foo-format` attributes, when the name `foo` is configured. Note that for fields containing multiple values, only the first value is output and flattened.
- **Formatted - ...**: Every Drupal field formatter rendering a field into HTML is available.
- **Custom element - Rendered entity**: This is available only for entity reference fields. It's a powerful way to configure the rendering of the referenced data. The display of the selected entity and view-mode can be configured via its own "Custom Element Display Configuration". That way the output data of referenced entities can be controlled in high detail. Furthermore, by enabling the "Flatten" option the data may be flattened into the main element.
Note that the list of available options is plugin-based, and thus extendable by Drupal modules.
# Pages & Routing
By default, the frontend forwards all page requests to Drupal. Given that, Drupal's routes, URL aliases as well as 404 or 403 pages all work out of the box. However, the frontend may add custom routes in front of Drupal's routing, see "Custom routes in the frontend".
## Configure error page and frontpage
The default pages (front, 403, and 404) are configured in the Drupal admin UI under:
**Administration** > **Configuration** > **System** > **Site information**
## URL Aliases
URL aliases are configured in the Drupal admin UI and just work. Configure them at:
**Administration** > **Configuration** > **Search and metadata** > **URL aliases**
## Custom routes in the frontend
The frontend can also define custom routes. When the frontend defines a custom route, it takes priority and works as usual. When no custom route is defined, the frontend default route forwards requests to Drupal and Drupal's routing takes effect.
For more information on defining custom routes with nuxt, please see the [Custom routes](lupus-decoupled.org/nuxt/custom-routes) section.
# Site layout & Menus
## Site layout
By default, Lupus Decoupled Drupal only serves the main page content as part of its page API response. That way, the overall site layout may be custom-built in the frontend, while some dynamic elements like navigation menus are fetched from Drupal using separate API requests.

Thanks to client-side navigation, the header and footer does not need to be re-fetched for subsequent pages.
That means, by default, the Drupal "Block layout" configuration is ignored and not applied. By installing the "Lupus Decoupled Drupal - Blocks" submodule, support for that configuration may be enabled. Please refer to [Advanced Topics > Block layout](lupus-decoupled.org/advanced-topics/block-layout) for more details on that.
Finally, it's possible to swap the used site-layout for some pages, e.g. by changing the value of the "layout" attribute for the page API responses. Please refer to [Nuxt > Page Layouts](lupus-decoupled.org/nuxt/page-layouts) for more details on that.
## Navigation menus
Lupus Decoupled Drupal integrates with the [Rest menu items](https://drupal.org/project/rest_menu_items){rel=""nofollow""} module.
That module is automatically installed and configured as part of Lupus Decoupled Drupal, so the REST API endpoints providing the Drupal menu items (see /admin/structure/menu) are available under [https://yourdrupalsite.com/ce-api/api/menu\_items/{menu\_name}](https://yourdrupalsite.com/ce-api/api/menu_items/%7Bmenu_name%7D){rel=""nofollow""}.
Combined with the default menu components provided by the Nuxt Drupal CE Connector modules, the main menu works out of the box, while additional menus may be fetched and displayed the same way.
# Authentication
Since page requests are processed by Drupal as usual, Drupal's authentication and session handling
stays fully working. Thus, when requests provide an authentication cookie, authentication just works. By default, cookie-based authentication with a separate frontend cookie is used.
## Cookie-based authentication
Cookie-based authentication can be achieved in one of the following three ways:
### 1. Separate frontend cookie
This is the default configured of the provided project template. The Nuxt Drupal CE Connector module forwards the cookie of every request. Thus, when a user logs in on the frontend domain, the set-cookie header is also forwarded and a cookie gets set on the frontend domain and thus is applied automatically.
The login on the frontend works via the regular Drupal login form when the "Lupus Decoupled User Form" is enabled. Simply visit `/user/login?destination=/` on the frontend to access it.
That way, the frontend login works with the same user accounts as the backend, but uses a separate session. The frontend and the backend needs a separate login. Also, since this variant requires a server, it only works in a server-rendered frontend setup.
### 2. Shared cookie domain
The Nuxt Drupal CE Connector module makes sure to forward a received cookie to the backend, such that authenticated requests just work. Instead of having a separate frontend cookie, Drupal may be configured to set the cookie on a shared, parent cookie domain. For example `drupal.example.com` and `nuxt.example.com` can both use the cookie on `.example.com`.
The Drupal cookie domain is configured via its `services.yml` file. Besides that, the project template supports setting the cookie domain via an environment variable, please refer to its [README](https://github.com/drunomics/lupus-decoupled-project?tab=readme-ov-file#automatic-frontend-login-via-a-shared-cookie-domain){rel=""nofollow""} for further details.
### 3. CORS setup
When the JavaScript application sends requests directly to Drupal, it's possible to leverage cookie-based authentication when configuring CORS appropriately. Lupus Decoupled Drupal comes with a CORS submodule which takes care of the necessary setup. However, browser privacy features may intervene and not send the cookie to another domain reliably in some cases.
## OAuth / OpenID Connect
Drupal provides great support for OAuth based login flows via the [Simple OAuth](https://www.drupal.org/project/simple_oauth){rel=""nofollow""} extension module. A frontend may use it to authenticate all or some requests with it. For example, it might be useful for authenticating requests to a protected API that is called from the frontend server.
# Metatags & Local tasks
Lupus Decoupled Drupal integrates with the Drupal Metatags module and local tasks (Drupal tabs) and provides them as part of the API response.
The [Nuxt CE connector](https://github.com/drunomics/nuxtjs-drupal-ce){rel=""nofollow""} has that functionality built-in, such that metatags, link relations and local tasks work out of the box.
## Metatags
The frontend can access the metatags via the `metatags` property of the API response.
The metatags are grouped by their group name, e.g. `meta` or `jsonld`. Each group contains an array of metatags.
Example API output:
```json
"metatags": {
"meta": [
{
"name": "title",
"content": "Lupus Decoupled Drupal"
},
{
"name": "description",
"content": "Lupus Decoupled Drupal"
},
{
"property": "og:url",
"content": "https://lupus-decoupled.org"
},
],
"link": [
{
"rel": "canonical",
"href": "https://lupus-decoupled.org"
}
]
},
```
The metatags can be configured by visiting:
**Administration** > **Configuration** > **Search and metadata** > **Metatag**
## Schema.org via JSON-LD
The [Schema.org](https://schema.org/){rel=""nofollow""} structured data markup is supported via the Drupal extension module [Schema Metatag](https://www.drupal.org/project/schema_metatag){rel=""nofollow""}. When installed, the structured data can be configured through the Metatag module's user interface. The data is output in the API response under the `jsonld` metatag group. On the frontend, the Nuxt Drupal-CE connector supports outputting JSON-LD and handles it automatically.
## Local tasks
The frontend can access the local tasks via the `local_tasks` property of the API response.
The local tasks are grouped by their primary and secondary tabs.
Example API output:
```json
"local_tasks": {
"primary": [
{
"url": "/frontpage",
"label": "View",
"active": true
},
{
"url": "/node/1/edit",
"label": "Edit",
"active": false
},
{
"url": "/node/1/delete",
"label": "Delete",
"active": false
},
{
"url": "/node/1/layout",
"label": "Layout",
"active": false
},
{
"url": "/node/1/revisions",
"label": "Revisions",
"active": false
},
{
"url": "/entity_clone/node/11",
"label": "Clone",
"active": false
},
],
"secondary": []
}
```
# Breadcrumbs & Messages
## Breadcrumbs
The Lupus custom elements renderer module integrates with Drupal's breadcrumb handling and simply provides the breadcrumbs generated by Drupal as part of its API response.
Combined with the default components provided by the Nuxt Drupal CE Connector module, the breadcrumbs are output as generated by default.
Drupal breadcrumbs may be customized using Drupal's API or by installing some contributed
module that provide UI for doing so, for example [Easy breadcrumb](https://www.drupal.org/project/easy_breadcrumb){rel=""nofollow""}.
## Messages
The Lupus custom elements renderer module integrates with Drupal's message handling
and simply provides Drupal messages as part of its API response. Combined with the
default components provided by the Nuxt Drupal CE Connector modules, Drupal messages are displayed automatically.
### Messages with static site generation
When a redirect points to a statically pre-generated page, there is no API request made for the static page. For a message to be displayed, the message must be sent with the redirect response (unlike with the next page response, which is the Drupal default).
### Messages with server renderings
For server-rendering, messages need to be sent with the subsequent page response. If sent with the redirect response, a message is only displayed when the redirect response is handled by a client-side API request. To fix this for server-rendered redirect responses, please see [this issue](https://www.drupal.org/project/lupus_ce_renderer/issues/3467399){rel=""nofollow""}.
# Redirects
Lupus Decoupled Drupal supports CMS-controlled redirects, i.e. Drupal's page API explicitly informs the frontend
about redirects to be generated. The frontend, in turn, simply generates the right redirect responses. That way,
Drupal-managed redirects just work in the decoupled frontend.
## Redirect API responses
The Drupal [page API](lupus-decoupled.org/get-started/how-it-works#custom-elements-page-api) may return data about the page to render OR alternatively
a redirect response. A redirect response is formatted like this:
```json [/some-redirected-path]
{
"redirect": {
"external": false,
"url": "/new-path",
"statusCode": 301
},
"messages": [ ]
}
```
In the above example, the frontend would return a HTTP 301 redirect to `/new-path` when the path `/some-redirect-path`
is requested.
## Managing redirects via UI
The Drupal [Redirect module](https://www.drupal.org/project/redirect){rel=""nofollow""} provides the ability to create manual redirects and maintains a canonical URL for all content, redirecting all other requests to that path. To use the module with Lupus Decoupled Drupal, simply install and use it - it just works.
## Creating redirects via code
Drupal modules may programmatically create redirect responses. To do so, simply return a regular redirect response
object from the route, the Lupus CE Renderer module is taking care of converting it to a redirect JSON
response when the custom-elements format is requested automatically.
# Editorial previews
## Content previews
When editing a content node, Drupal displays a preview button. The Lupus custom elements renderer module supports rendering preview routes, so previews of content nodes work fine. For improving this preview with a back button, please refer to [this issue](https://www.drupal.org/project/lupus_ce_renderer/issues/3347003){rel=""nofollow""}.
## Responsive preview
Lupus Decoupled Drupal integrates with the Drupal [responsive preview module](https://www.drupal.org/project/responsive_preview){rel=""nofollow""}. After installing the extension module, simply enable the provided "Lupus Decoupled Responsive Preview" module.
When the module is setup, the responsive preview widget in the Drupal admin toolbar may be used on the content node edit form to open the preview in a modal dialog.
# Layout builder
::note
**Consider using [Drupal Canvas](lupus-decoupled.org/guide/canvas) instead.**
Canvas is Drupal's modern page builder and supersedes Layout Builder for most decoupled use cases. Layout Builder remains supported but Canvas is recommended for new projects.
::
The Drupal [layout builder](https://www.drupal.org/docs/8/core/modules/layout-builder){rel=""nofollow""} module may be leveraged for customizing the page layout per entity or content type within Drupal. Optionally, this default may be customized by each individual content item. That way, the module can be used as a custom page builder tool.
## Rendering layouts
By default, layouts are rendered using a `` custom element for each layout section, such that any section settings are forwarded to the frontend. Contained blocks may build their content using custom elements, or fallback to Drupal rendering and regular HTML.
Please see [Drupal > Providing blocks](lupus-decoupled.org/drupal/providing-blocks) for more details on how to provide Drupal blocks that render custom elements.
## Layout previews
Lupus Decoupled Drupal provides a route for previewing layouts in the frontend, i.e. `node/XX/layout-preview`. For providing a nice UX to open the preview, the integration with the responsive preview module is leveraged (see [editorial previews](lupus-decoupled.org/guide/editorial-previews)). The responsive preview widget in the Drupal admin toolbar may be used on the layout builder page to open the preview in a modal dialog. Additionally, a link to open a full-page preview in a new tab is provided.
# Decoupling Drupal Canvas
[Drupal Canvas](https://www.drupal.org/project/canvas){rel=""nofollow""} is Drupal's modern page builder ([docs](https://project.pages.drupalcode.org/canvas/){rel=""nofollow""}). When paired with Lupus Decoupled, Canvas becomes a powerful decoupled page builder that supports varying frontend frameworks.
## How It Works
Canvas pages are built from components in Drupal's Canvas editor. In this context, a component can be a custom element (a Vue/React component provided by the frontend) or a regular Drupal block — which can render any Drupal content, including Views blocks that themselves output custom elements. While editing, the editor renders live previews of components via JavaScript using a preview provider (i.e. the frontend). Once published, the page is served through the CE API like any other content (e.g. `/ce-api/page/{id}`), as regular [custom elements API](lupus-decoupled.org/drupal/custom-elements) output, so components are regularly rendered by the frontend.
## Drupal CMS
When using [Lupus Decoupled Starter](lupus-decoupled.org/advanced-topics/site-templates) or a similar site template based upon Lupus Decoupled, you get a fully working setup with Canvas pre-configured. Refer to the [Drupal CMS page](lupus-decoupled.org/guide/drupal-cms) for details.
## Manual Setup
Apply the [Lupus Decoupled Canvas Recipe](https://www.drupal.org/project/lupus_decoupled_recipe_canvas){rel=""nofollow""} to set up the module and its dependencies with a single recipe apply:
```bash
composer require drupal/lupus_decoupled_recipe_canvas
drush recipe ../recipes/lupus_decoupled_recipe_canvas
```
This installs and configures [Canvas](https://www.drupal.org/project/canvas){rel=""nofollow""}, [Canvas ExtJS](https://www.drupal.org/project/canvas_extjs){rel=""nofollow""}, and the Lupus Decoupled Canvas sub-module.
### Configuring the Editor Theme
Configure the theme used for Canvas editor previews at `/admin/config/system/lupus-decoupled/settings` under "Lupus Decoupled theme". A minimal theme like Stark or [Lupus Stark](https://www.drupal.org/project/lupus_stark){rel=""nofollow""} is recommended.
## Providing Components
Components are provided via a **component index**: a JSON file listing available components, their props, and metadata. [Canvas External JS Components](https://www.drupal.org/project/canvas_extjs){rel=""nofollow""} (a Canvas extension for integrating external JavaScript components) reads this index and makes the components available in the Canvas editor UI.
- [Vue/Nuxt](lupus-decoupled.org/nuxt/canvas-components): very well supported via [nuxt-component-preview](https://github.com/drunomics/nuxt-component-preview){rel=""nofollow""}, see [Component Index](lupus-decoupled.org/nuxt/canvas-components) and [Component Previews](lupus-decoupled.org/nuxt/component-previews)
- React: no ready-made setup yet. [fullfatthings/drupal-canvas-react](https://github.com/fullfatthings/drupal-canvas-react){rel=""nofollow""} provides a library for exposing React components and generating a component index, but some integration work is required to wire everything together.
For documentation on how to integrate other frontend frameworks, see [Multi-Frontend](lupus-decoupled.org/advanced-topics/multi-frontend#canvas-support).
## Component Registration
### Automatic Registration via Lupus CSR Themes
When a Lupus CSR-based theme is installed, components are automatically registered from the theme's bundled component-index. No manual steps are needed.
Automatic registration also works with a decoupled frontend when the `lupus_decoupled_canvas` module is installed, provided the configured frontend exposes a component index at the configured frontend base URL.
### Decoupled Frontends
When using a decoupled frontend (e.g. a dev server), component registration must be triggered manually:
- **UI**: Go to **Administration > Appearance > Components > External JS** (`/admin/appearance/component/extjs`)
- **Drush**: `drush canvas:extjs-auto-register`
- **Recipe config action**: Use `lupus_decoupled_canvas:registerComponents` after configuring the preview provider URL
When the frontend base URL is changed via the Lupus Decoupled settings form, a prompt will remind you to update the component registration.
## Relationship to Canvas Code Components
Drupal Canvas has built-in support for (P)React "code components" with their own bundler. These components are purely client-side rendered and currently do not integrate with Lupus Decoupled's CE API pipeline. Adding support for embedding them as custom elements would be straightforward, but is not implemented yet.
# Drupal CMS with Lupus Decoupled
[Drupal CMS](https://www.drupal.org/project/cms){rel=""nofollow""} is Drupal's user-friendly distribution aimed at marketers and content teams. Lupus Decoupled supports it via site templates, providing a fully decoupled setup out of the box.
The [Lupus Decoupled Starter](https://git.drupalcode.org/project/lupus_decoupled_starter){rel=""nofollow""} is a ready-to-use Drupal CMS site template that bundles:
- **Drupal CMS base setup**: content types, editorial workflow, and other Drupal CMS defaults
- **Lupus Decoupled Recipe**: core modules and configuration
- **Lupus Decoupled Canvas**: Canvas visual editor integration
- **Lupus CSR**: client-side rendering theme with the [Nuxt Starter](https://github.com/drunomics/lupus-decoupled-nuxt-starter){rel=""nofollow""} (set as default)
## Installation
Install the starter as a site template when setting up Drupal CMS, or apply it manually:
```bash
composer require drupal/lupus_decoupled_starter
drush site-install ../recipes/lupus_decoupled_starter
```
For a full local setup with DDEV:
```bash
mkdir my-drupal-site && cd my-drupal-site
ddev config --project-type=drupal11 --docroot=web
ddev composer create-project drupal/cms
ddev composer require drupal/lupus_decoupled_starter
drush site-install ../recipes/lupus_decoupled_starter
ddev launch
```
## First Steps
After installation, navigate to **Content > Pages** in the Drupal admin to find the provided demo content. Open a page, review it, and try editing it with the Canvas visual page builder to get familiar with the setup.
## Client-Side Rendering (CSR)
The starter uses [Lupus CSR](https://www.drupal.org/project/lupus_csr){rel=""nofollow""} (Client-Side Rendering) as the default theme. This means:
- The frontend is pre-built and served directly from Drupal, with no separate frontend server needed
- The frontend JavaScript runs in the browser and fetches content from the CE API at `/ce-api/...` on the same origin, so no CORS configuration is required
- This is ideal for getting started quickly, but **not recommended for SEO-critical production sites** (use SSR instead)
All Lupus Decoupled documentation applies regardless of whether you use CSR or SSR. For Nuxt rendering mode details, see [Rendering modes](lupus-decoupled.org/nuxt/rendering-modes).
## Customizing the Theme
To customize the frontend, run a local frontend dev server and point Drupal to it during development:
- Go to **Configuration → Lupus Decoupled Settings** (`/admin/config/services/lupus-decoupled`)
- Set **Frontend base URL** to your dev server and enable frontend redirects
Pages and Canvas previews now render via the dev server. When adding new Canvas components or changing their props, go to **Administration > Appearance > Components > External JS** (`/admin/appearance/component/extjs`) and press **Update components**.
### Provide an Updated Lupus CSR Theme
Once customization is done, generate a production build of your frontend and configure Lupus CSR to serve it via `/admin/appearance/settings/lupus_csr`. For sub-theme packaging, see [Creating a Sub-Theme](lupus-decoupled.org/drupal/themes#creating-a-sub-theme).
For detailed instructions for the Nuxt-based default theme, see [CUSTOMIZING.md](https://git.drupalcode.org/project/lupus_csr/-/blob/1.x/examples/nuxt-starter/CUSTOMIZING.md?ref_type=heads){rel=""nofollow""}.
## Switching to Server-Side Rendering
To move from CSR to SSR:
1. Set up a separate frontend server
2. Configure the Lupus Decoupled frontend URL to point to your frontend server and enable the frontend redirect
Refer to [Deployment strategies](lupus-decoupled.org/deployment/deployment-strategy) for options.
## Creating New Site Templates
The Lupus Decoupled Starter is designed to serve as a template for building new Drupal CMS site templates. See the [Site Templates](lupus-decoupled.org/advanced-topics/site-templates) page for guidance.
# Multi-frontend
Thanks to the component-oriented decoupling of Lupus Decoupled Drupal, it's really easy to connect a frontend of choice!
## Supported frontend frameworks
Lupus Decoupled provides supports for the following frontend frameworks:
| Framework | Technology | Status | Description | Link |
| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | ---------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------- |
| [Nuxt](lupus-decoupled.org/nuxt/introduction) | Vue.js, JavaScript | Default, Stable & Complete | [Easy setup](lupus-decoupled.org/nuxt/setup) via re-usable connector module. | [Connector module](https://github.com/drunomics/nuxtjs-drupal-ce){rel=""nofollow""} |
| [Next.js](https://nextjs.org){rel=""nofollow""} | React, JavaScript | Proof-of-concept | Example implementation, see README for details. | [Example implementation](https://github.com/drunomics/lupus-decoupled-nextjs-example){rel=""nofollow""} |
| Yours? | [Add it!](https://github.com/drunomics/lupus-decoupled-website/edit/main/content/3.advanced-topics/05.multi-frontend.md){rel=""nofollow""} | | | |
## Example frontends
- [Nuxt Naked Starter](https://github.com/drunomics/lupus-decoupled-nuxt-starter){rel=""nofollow""}
- [Nuxt Shadcn Example](https://github.com/drunomics/lupus-decoupled-nuxt-shadcn){rel=""nofollow""}
- [Next.js Example](https://github.com/drunomics/lupus-decoupled-nextjs-example){rel=""nofollow""}
- [Nuxt 2 Example (legacy)](https://github.com/drunomics/lupus-decoupled-nuxt2-demo){rel=""nofollow""}
## Adding a new frontend framework
Adding a support for a new framework is quite easy. Here is a short overview of what the frontend needs to take care of:
- *Proxying requests*: 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.
- *Forwarding headers*: By forwarding the cookie header, the frontend may easily leverage Drupal [authentication handling](lupus-decoupled.org/guide/authentication).
- *Rendering page content*: The `content` section of a response contains a nested tree of custom elements that need to be rendered on the page.
- *Render a page shell*: Request [navigation menus](lupus-decoupled.org/guide/site-layout-menus#navigation-menus) and render them as part of a [site layout](lupus-decoupled.org/guide/site-layout-menus).
- *Support page attributes*: Support all page attributes provided by the Drupal [page API](lupus-decoupled.org/get-started/how-it-works#custom-elements-page-api), i.e. render [breadcrumbs and messages](lupus-decoupled.org/guide/breadcrumbs-messages), support [Metatags and Local tasks](lupus-decoupled.org/guide/metatags-local-tasks).
- *Handle errors*: Take care to not add special handling for HTTP 4xx responses, such that Drupal-controlled error pages get rendered as usual. Simply take care of forwarding the status code.
- *Handle redirects*: When the page-API generates a [redirect response](lupus-decoupled.org/guide/redirects#redirect-api-responses) instead of a page, take care to issue a suitable redirect response with the given status code instead of rendering a page.
- *Canvas support* (optional): To support [Drupal Canvas](lupus-decoupled.org/guide/canvas), two additional pieces are needed:
- *Custom Elements preview provider*: Implement a preview provider plugin so the Canvas editor can render live component previews via an iframe. The provider receives a component name and props and renders the component in isolation. See the [Nuxt preview provider plugin](https://git.drupalcode.org/project/custom_elements/-/blob/3.x/src/Plugin/CustomElementsPreviewProvider/NuxtPreviewProvider.php?ref_type=heads){rel=""nofollow""} as a reference implementation.
- *Component index*: Expose a `component-index.json` endpoint listing available components, their props, and metadata. [Canvas External JS Components](https://www.drupal.org/project/canvas_extjs){rel=""nofollow""} (a Canvas extension for integrating external JavaScript components) reads this index to populate the Canvas editor UI. See the [nuxt-component-preview component index](https://github.com/drunomics/nuxt-component-preview#component-index){rel=""nofollow""} and [fullfatthings/drupal-canvas-react](https://github.com/fullfatthings/drupal-canvas-react){rel=""nofollow""} for Vue and React reference implementations.
That's it!
# Listings, Views
The [Lupus Decoupled Views](https://git.drupalcode.org/project/lupus_decoupled/-/blob/1.x/modules/lupus_decoupled_views/README.md){rel=""nofollow""}
submodule provides support for Drupal's Views module. The Views module is Drupal's native way and very powerful way of creating listings of content. It provides a UI for configuring complex queries, searches and the rendering of results, including paging.
As alternative to Views-based listings, the frontend may [create custom routes](lupus-decoupled.org/nuxt/custom-routes) and listings by querying the backend, see [JSON-API, GraphQL](lupus-decoupled.org/advanced-topics/jsonapi-graphql).
## Usage
After enabling the sub-module, create a view, skip the wizard and follow the following steps:
1. Add `Custom elements display page` to a view (not a regular page!).
2. Select `Custom elements` style as view format.
3. Select `Content` as row style. Choose the view-mode you want to use for the listed elements.
4. Configure filters and other options as needed and set a path for the view.
5. The Custom elements API of the view is available at `/ce-api/`. Take note of the custom element name visible in the output, which will be `drupal-view-{ID}`.
6. On the frontend clone the `drupal-view--default` component and create a component using the noted custom element name `drupal-view-{ID}`. Test the view on the frontend at the configured path `` and customize the component as desired.
Note that the chosen view-mode is automatically rendered with custom elements within the view. It's not required to force rendering it via custom elements.
## Video tutorial
In the following tutorial an example configuration can be followed: [](https://www.youtube.com/live/KKTl4z_MGSg?si=j4qWfVlo9IOiobDy&t=1352){rel=""nofollow""}
Note: The tutorial is a year old and uses `drupal-view` as custom element name. That changed meanwhile, use `drupal-view-{ID}` as custom element name or stay with the default `drupal-view--default`.
## Status
- Support for Views is rather new and problems may arise with certain configurations. Please report any issues in the [Lupus Decoupled Drupal issue queue](https://www.drupal.org/project/issues/lupus_decoupled?categories=All){rel=""nofollow""}.
- Atm there is no support for rendering exposed filter forms. So those need to be added manually in the frontend, when needed.
- The Row style `Fields` is not supported yet (see
[#3461874](https://www.drupal.org/project/lupus_decoupled/issues/3461874){rel=""nofollow""}).
- Items formatted as `Rendered entities` are supported. These are needed for [Search API Index Views](lupus-decoupled.org/searches#search-api-views).
# Searches
## Searches with Views
Basic searches based upon simple SQL-queries can be built easily with the
help of the Drupal Views module. Create a regular View and add one or more
exposed filters, for example a fulltext filter. The frontend needs to build
a custom form for the search input and send the search input data as request
query parameters to the view.
## Search API
The Drupal extension module [Search API](https://drupal.org/project/search_api){rel=""nofollow""} constitutes a powerful framework for building searches. While out-of-the-box it comes with a database backend, there are many modules providing support for powerful Search solutions available, for example:
- [Solr](https://drupal.org/project/search_api_solr){rel=""nofollow""}
- [Elasticsearch](https://www.drupal.org/project/elasticsearch_connector){rel=""nofollow""}
- [OpenSearch](https://www.drupal.org/project/search_api_opensearch){rel=""nofollow""}
- [Typesense](https://www.drupal.org/project/search_api_typesense){rel=""nofollow""}
- [Meilisearch](https://www.drupal.org/project/search_api_meilisearch){rel=""nofollow""}
- [Algolia](https://www.drupal.org/project/search_api_algolia){rel=""nofollow""}
The Search API module provides configuration for the content to be indexed, including configuration for the indexed data (and its processing) on a per-field basis. The content gets indexed whenever it is changed, either instantly or delayed via Drupal's cron service (as configured). For more details, please refer to the [Search API documentation](https://www.drupal.org/docs/contributed-modules/search-api){rel=""nofollow""}.
### Search API + Views
For building the actual Search page, again the Views module maybe be leveraged, by building a View with the Search API as query backend. Please refer to the [module documentation](https://www.drupal.org/docs/8/modules/search-api/getting-started/search-forms-and-results-pages/searching-with-views/creating-a-search-view){rel=""nofollow""} for details on how to setup such a View. This might be a good option for simple searches.
Search index view could be configured to have a `Custom Elements Page`
display and with that configuration a custom elements search endpoint is
available. (see [Lupus Decoupled Views](lupus-decoupled.org/listings-views#status))
### Search API + Custom queries
A powerful alternative for building ambitious searches is to leverage the Search API module to index data to the search server and to have the frontend query the search server directly. That way, the frontend stays in control of the query and may make use of advanced search-server specific features in order to build a great, ambitious search experience!
# JSON-API & GraphQL
When data needs to be queried or updated from the frontend, Drupal offers plenty of options. Lupus Decoupled Drupal's provided API may be combined with any of these:
## JSON-API
Drupal core provides the JSON-API module, which is a RESTful API that works with all Drupal entities (and its access system) out-of-the-box. It comes with support for all CRUD operations, as well as custom queries. Following are the most relevant resources to get started:
- [Documentation](https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module){rel=""nofollow""}
- [jsonapi-client npm package](https://www.npmjs.com/package/@drupal-api-client/json-api-client){rel=""nofollow""}
- [Json\:API Extras module](https://www.drupal.org/project/jsonapi_extras){rel=""nofollow""} - Configure which entities are exposed
- [Simple OAuth](https://www.drupal.org/project/simple_oauth){rel=""nofollow""} - Authenticate requests via API token
## GraphQL
Alternatively, there are Drupal extension modules that add GraphQL support:
- [GraphQL module](https://www.drupal.org/project/graphql/){rel=""nofollow""}
- [GraphQL compose](https://www.drupal.org/project/graphql_compose){rel=""nofollow""}
# Caching
## Page-level caching
Lupus Decoupled Drupal takes advantages of Drupal's built-in caching system based around cache metadata. Given that the following modules provided by Drupal core work as usual:
- Internal Page Cache - Caches page for anonymous users, with cache-tag based invalidation.
- Dynamic Page Cache - Cache pages for authenticated users, with cache-tag based invalidation.
Since all the caching related HTTP headers get set correctly, also CDN or reverse-proxy (e.g. varnish) based caching works as usual. Just like with traditionally rendered Drupal pages, the pages rendered with custom elements have cache metadata associated, thus things like cache-tag based invalidation are supported.
Next, the following Drupal extension modules are great, optional additions and fully supported:
- [Purge module](https://drupal.org/project/purge){rel=""nofollow""} - A general cache invalidation API, optionally with cache-tag based invalidation.
- Varnish based cache and cache-tag invalidation via [Varnish Purger](https://drupal.org/project/varnish_purge){rel=""nofollow""}
- Modules adding further cache backends, like [Redis](https://drupal.org/project/redis){rel=""nofollow""}
- Various CDN modules, like [Cloudflare](https://drupal.org/project/cloudflare){rel=""nofollow""}
## Render caching
Since pages rendered into custom elements are skipping the Drupal internal, traditional render system, the Drupal-internal render-caching is bypassed. Instead of caching individual rendered pieces, whole pages can be cached well via dynamic page cache. Since the LupusCeRenderer module attaches the per-user rendered local tasks (the Drupal tabs) via a Kernel response event that is triggered after the Dynamic Page Cache, the pages can be cached efficiently across user-roles if the page-content is not user or role dependent. Additionally, the page response includes only the main content (no menus etc.), so it's often cacheable.
# Error pages
## Drupal error pages
Error pages provided by Drupal (e.g. 403, 404 page) are shown by default and can be configured in Drupal. Configuration is done in the Drupal backend under `Configuration > System > Basic site settings > Error pages`. With server-rendering, the HTTP status code is correctly forwarded.
The Nuxt error page is only shown when the backend cannot be reached (error 504 Gateway timeout).
## Custom error pages in Nuxt
In order to use regular Nuxt error pages for regular 403, 404 error pages, you can enable the `customErrorPages` option in `nuxt.config.js`:
```js
export default defineNuxtConfig({
drupalCe: {
customErrorPages: true,
// options...
}
})
```
When custom error pages are active, the nuxt error handling is used for all error HTTP status codes returned by Drupal.
# Drupal forms
::note
At the moment Drupal forms are only supported with server-side
[rendering mode](lupus-decoupled.org/nuxt/rendering-modes)
.
::
## Overview
The "Lupus Decoupled Form" sub-module makes it easy to support Drupal forms in a decoupled frontend. The form HTML is rendered by Drupal and wrapped in a `` custom element, such that the frontend can easily add some styling or custom JS-enhancements.
At the moment, progressive form submissions are supported, which work without JavaScript. JavaScript enhanced form submissions are currently not a priority, but can be achieved with some additional JavaScript code, see Issue [#3471135](https://www.drupal.org/project/lupus_decoupled/issues/3471135){rel=""nofollow""} for some code to get started.
## How does it work?
Generally, Drupal renders the form as usual. The resulting HTML markup is wrapped into a `` component, which simply shows the server-rendered HTML of the form. When the form is submitted, the frontend takes care of forwarding the POST request to the backend, where it's processed as usual. After form processing the request is rendered in a custom element response again: Either the page response contains a reloaded form or a redirect. Any messages for form validation fails or success are handled as usual via the [Drupal message system](lupus-decoupled.org/guide/breadcrumbs-messages) and sent as part of the next page response. Thus, subsequently the frontend takes care of rendering the response appropriately in a new page or redirect response - just as it does for any other server-render page.
### Frontend support
- For the Nuxt frontend, The Nuxtjs Drupal-CE Connector module ships with a suitable default component: [drupal-form--default.vue](https://github.com/drunomics/nuxtjs-drupal-ce/blob/2.x/playground/components/global/drupal-form--default.vue){rel=""nofollow""}
- There is [server middleware](https://github.com/drunomics/nuxtjs-drupal-ce/blob/4d8c9e43d8a3ed1f1f6480425b25016b82e57579/src/runtime/server/middleware/drupalFormHandler.ts){rel=""nofollow""} which takes care of handling the POST request and renders the response.
## Rendering forms in the frontend
Generally, the `drupal-form--default` component may be used to add form styles and JavaScript as needed, either generally for all forms via the default component, or individually by customizing things in a per-form component `drupal-form--{FORM-ID}`.
The JavaScript libraries a form attaches - form `#states`, autocomplete, module behaviours - are emitted alongside the form markup as `` elements, such that the frontend can load them and run the Drupal behaviours; see [Drupal JavaScript](lupus-decoupled.org/advanced-topics/drupal-javascript). Drupal CSS assets are not included, thus the frontend needs to take care of providing suitable replacement styles.
In order to change the HTML markup of forms, the form elements need to be themed in Drupal.
### Theming form elements
Since the markup is generated by Drupal, the HTML of individual form elements can be altered only by adjusting the Drupal markup with the help of a Drupal theme:
- For having a clean, semantic output of forms, the [lupus-stark theme](https://www.drupal.org/project/lupus_stark){rel=""nofollow""} has been created - which
adds suitable Drupal twig files to do away with unnecessary Drupal-isms. That way decent markup is
generated and necessary style adaptions are made possible by targeting elements using the
reasonable default classes.
- Generally, the active Drupal theme can be customized by setting the theme as site-wide standard theme. If the form is on a Drupal admin page, the Admin Theme will be applied though.
- The module adds support for a special '\_theme' key in route definitions, which allows Drupal developers to switch themes to the given theme-name specifically for the form routes, as preferred.
## Supported Drupal forms
Lupus Decoupled ships with further sub-modules that enable support for further Drupal-forms as desired:
- **Lupus Decoupled Contact Forms** - Adds support for the contact forms provided by the core contact module.
- **Lupus Decoupled User Forms** - Enables using Drupal user login and user password reset forms in the frontend. That way, user may login while getting a separate frontend cookie. Please refer to the [Authentication page](lupus-decoupled.org/guide/authentication) for details.
If you miss support for some Drupal form, please open an issue in the [Lupus Decoupled issue queue](https://www.drupal.org/project/issues/lupus_decoupled?categories=All){rel=""nofollow""} to add it! Please refer to the page [Adding Drupal Forms](lupus-decoupled.org/drupal/add-drupal-forms) for documentation on how this is done.
## Webforms
The Lupus Decoupled Webform module provides support for [Webform](https://www.drupal.org/project/webform){rel=""nofollow""}
module (version 6.0 or later), a popular module for building forms using the UI.
Webforms integration utilizes the Lupus Decoupled Forms
functionality (see [How it work?](lupus-decoupled.org/#how-does-it-work) section), thus
a webforms get a custom elements API endpoint.
### Limitations
The Drupal JavaScript a webform element attaches is part of the webform API
response, see [Drupal JavaScript](lupus-decoupled.org/advanced-topics/drupal-javascript) - so
conditional (`#states`) elements work as they do in Drupal. Webform elements
built on Drupal's Ajax framework or on core CSS (dialogs, off-canvas) are
not covered by that and need frontend work.
### Confirmation types
Inline and page (default) confirmations are supported, but for page
confirmation type the user (or anonymous) needs to have access to "view own
webform submissions" set on webform settings configured at
`admin/structure/webform/manage/{webform}/access`
### Submission example
A curl request example to submit a custom webform (with `test_ce_webform` id).
Note that the submission webform id is `webform_submission_test_ce_webform_add_form`.
```bash
curl -k -X POST --url https://lupus-decoupled.ddev.site/ce-api/form/test_ce_webform -F 'first_name=John' -F "last_name=Doe" -F "checkbox=1" -F "op=Submit" -F "form_build_id=form-" -F "form_id=webform_submission_test_ce_webform_add_form" -H "Accept: application/json"
```
# Drupal JavaScript
## Overview
Drupal attaches JavaScript libraries to the render arrays it builds - form `#states`, autocomplete, module behaviours. In a decoupled setup only the rendered markup travels to the frontend, so those attachments would be lost on the way out. To avoid that, the [Custom Elements](https://www.drupal.org/project/custom_elements){rel=""nofollow""} module emits every attached library as a `` custom element next to the markup, such that a frontend can load the JavaScript and run the Drupal behaviours.
::note
Library elements are emitted by Custom Elements 3.x, in releases after 3.4.1.
::
## How does it work?
Whenever Drupal converts a render array into a custom element - a [Drupal form](lupus-decoupled.org/advanced-topics/drupal-forms), a fallback-rendered [block](lupus-decoupled.org/advanced-topics/block-layout), a [Canvas](lupus-decoupled.org/guide/canvas) component tree - the libraries that bubbled up into the render array's `#attached` are turned into custom elements:
- Library dependencies are resolved into load order by Drupal, so the frontend can load the files sequentially without knowing anything about Drupal's library graph.
- One element is emitted per library that carries JavaScript, in that order, as a sibling preceding the markup in the same slot. Libraries contributing only CSS or settings are skipped.
- The element name is the library name, lower-cased and with `/`, `.` and `_` replaced by `-`: `core/drupal.states` becomes `drupal-library-core-drupal-states`. The verbatim library name stays available in the `library` prop.
- Each element carries its library's JavaScript files in the `js` prop, as `url` (root-relative, with a cache-busting version query - or absolute for external assets) plus the `attributes` Drupal would put on the `
```
## Rendering slots
Rendering slots in Vue components works seamlessly with both JSON and markup serialization. Simply use native Vue slots in your template:
```vue
```
This works with both JSON and markup serialization. The Nuxt Drupal CE connector automatically handles the slot content in both cases.
### JSON serialization (default)
With JSON-formatted custom elements, slot content is automatically mapped to Vue slots. You can use named slots or the default slot:
```vue
```
Note: When rendering via JSON content, the module supports [default components](lupus-decoupled.org/nuxt/default-components).
### Markup serialization
With markup, the Vue template compiler needs to be enabled:
```js
// nuxt.config.js
export default {
vue: {
runtimeCompiler: true
}
}
```
Then, slots will be directly passed as Vue slots.
```vue
Fallback content.
```
### Full example
Here is an example of a component that leverages props and slots and works with both markup and JSON serialization:
```vue
Node: {{ title }}
```
# Default components
**Note**: This feature is atm only available when using the (default) JSON-based rendered of custom elements.
## Overview
When using JSON-based rendering of custom elements, the module offers fallback component support. If a custom element lacks a corresponding Vue component, the module attempts to find a suitable default component.
## Use-case
This feature makes it possible to define a general custom element, e.g. for drupal forms `drupal-form--default.vue`, which is used to render every custom element `drupal-form-*` when no specific custom element is available. That way, specific elements provided by Drupal, e.g. `drupal-form-user-login-form.vue`, have the default applied, while it's easily possible to copy and customize the default component.
The same way it becomes handy to have general default components for content entities, like `node--default.vue`, paired with element names like `node-article-page`. That way it's easy to implement a general default component that suits multiple cases, while specific elements can easily be customized.
#### How it works:
1. The module removes the last `-`-separated prefix from the element name.
2. It then appends a `--default` suffix.
3. If this modified component exists, it's used for rendering.
4. If the component is not exiting, the process is repeated.
#### Example lookup process
When a specific component isn't found, the module searches for a default component by progressively removing segments from the custom element name. For example when rendering the custom element `node-custom-view` it looks for components in the following order:
```text
x node-custom-view.vue
x node-custom-view--default.vue
x node-custom--default.vue
✓ node--default.vue
```
# Composables
The composable provided by the module provides helpers to fetch page and menu data, handle errors, and render custom components.
### useDrupalCe
This composable exports a collection of utilities to handle Drupal content and functionality.
#### Page Handling
- `fetchPage(path: string, query?: Object, errorHandler?: Function, skipProxy?: boolean)` - Fetches a page by path
- `getPage()` - Returns the current page state
- `usePageHead(page: Ref, include?: Array<'title' | 'meta' | 'link' | 'jsonld'>)` - Sets page head metadata from Drupal data
- `getPageLayout(page?: Ref)` - Gets current page layout name (defaults to 'default')
#### Menu Handling
- `fetchMenu(name: string, options?: Object, errorHandler?: Function, skipProxy?: boolean)` - Fetches a menu by name
#### Component Rendering
- `renderCustomElements(elements: string | object | Array)` - Renders one or multiple custom elements from JSON data
- `resolveCustomElement(element: string)` - Resolves a custom element name to a Vue component
- `loadLibrary(library: Object)` - Loads a [Drupal JS library](lupus-decoupled.org/nuxt/drupal-libraries) emitted by the backend
#### Messages
- `getMessages()` - Returns Drupal messages of the current page
#### API Utilities
- `$ceApi(options?: Object, skipProxy?: boolean)` - Creates a configured fetch instance
- `useCeApi(path: string, options?: Object, passThroughHeaders?: boolean, skipProxy?: boolean)` - Fetches data from Drupal CE API endpoint
- `getCeApiEndpoint(localize?: boolean)` - Gets API endpoint with optional localization
- `getDrupalBaseUrl()` - Gets configured Drupal base URL
- `getMenuBaseUrl()` - Gets configured menu base URL
### Example usage
The following example makes use of the `fetchPage` method to get the current page:
```js
```
# Drupal JS libraries
## Overview
Drupal emits the JavaScript libraries attached to a rendered form, block or component as `` custom elements. See [Drupal JavaScript](lupus-decoupled.org/advanced-topics/drupal-javascript) for what the backend sends and why; this page covers loading them in Nuxt.
## Rendering library elements
Add a global `drupal-library--default.vue` component - the [default component](lupus-decoupled.org/nuxt/default-components) lookup resolves it for every `drupal-library-*` element, whatever library the backend attached. A reference implementation is part of the playground: [drupal-library--default.vue](https://github.com/drunomics/nuxtjs-drupal-ce/blob/2.x/playground/components/global/drupal-library--default.vue){rel=""nofollow""}.
The component is renderless: it takes the `library`, `js` and `drupalSettings` props of the element, hands them to `useDrupalCe().loadLibrary()` on mount, and renders nothing.
```vue
```
## loadLibrary()
`useDrupalCe().loadLibrary(library)` takes a resolved library (`{ js, drupalSettings }`) and returns a promise settling once its files have loaded. On the server it resolves immediately - loading is a client-side concern.
It lazy-loads the actual loader via a dynamic import, so the loader chunk - and the Drupal JavaScript it pulls in - is only fetched by pages that actually render a library element. The loader then:
- resolves the file URLs against the configured `drupalBaseUrl`,
- appends them as `
```
## Nuxt layout documentation
For more information on routing in Nuxt.js, see the [Nuxt layouts documentation](https://nuxt.com/docs/guide/directory-structure/layouts){rel=""nofollow""}.
# Component Previews
Component previews allow Vue components to be rendered in isolation in external contexts, such as the [Drupal Canvas](lupus-decoupled.org/guide/canvas) editor. This enables editors to see live previews of components directly in the page builder.
[nuxt-component-preview](https://github.com/drunomics/nuxt-component-preview){rel=""nofollow""} implements this for Nuxt/Vue. It enables previewing components in external contexts and auto-generates a component index for [Drupal Canvas](lupus-decoupled.org/guide/canvas) integration.
If you are using the [Nuxt Starter](https://github.com/drunomics/lupus-decoupled-nuxt-starter){rel=""nofollow""} or [nuxtjs-drupal-ce](https://github.com/drunomics/nuxtjs-drupal-ce){rel=""nofollow""}, the module is already pre-configured — no setup needed.
## Component Index
The same module also auto-generates a [component index](lupus-decoupled.org/nuxt/canvas-components) that Canvas uses to discover the available components, their props, and their metadata.
## Manual Setup
If you are not using the Nuxt Starter or nuxtjs-drupal-ce, install and configure the module manually.
Install the module:
```bash
npm install nuxt-component-preview
```
Add it to `nuxt.config.ts` and add `` to `app.vue`:
```ts
export default defineNuxtConfig({
modules: ['nuxt-component-preview'],
})
```
```vue
```
When embedding previews from a different domain (e.g. a Drupal backend), disable the app manifest in development to avoid relative URL failures:
```ts
$development: {
experimental: {
appManifest: false
}
}
```
For CORS configuration, see the [nuxt-component-preview CORS setup](https://github.com/drunomics/nuxt-component-preview#cors-configuration){rel=""nofollow""}. When using nuxtjs-drupal-ce, CORS is configured automatically.
# Component Index
To make Vue components available in the [Drupal Canvas](lupus-decoupled.org/guide/canvas) editor, the frontend exposes a **component index**: a JSON file listing all available components together with their props, prop schemas, labels, descriptions, categories, formats and schema references. [Canvas External JS Components](https://www.drupal.org/project/canvas_extjs){rel=""nofollow""} reads this index and uses it to populate the editor UI.
The [nuxt-component-preview](https://github.com/drunomics/nuxt-component-preview){rel=""nofollow""} Nuxt module generates this index automatically from your components, picking up extra metadata from JSDoc annotations (implemented in [`src/runtime/server/utils/`](https://github.com/drunomics/nuxt-component-preview/tree/1.x/src/runtime/server/utils){rel=""nofollow""}). The same module also powers the [Component Previews](lupus-decoupled.org/nuxt/component-previews) used by the Canvas editor.
The index is served at `/nuxt-component-preview/component-index.json`.
Only **global** components are included. Components in `components/global/` are automatically global; others can be registered with `global: true` in `nuxt.config.ts`.
## Grouping Components by Folder
A recommended pattern is to keep Canvas components in a dedicated `components/Canvas/` folder and derive categories from subfolder names:
```ts
components: [
{ path: '~/components/Canvas', global: true, pathPrefix: false, prefix: '' },
{ path: '~/components/global', global: true },
'~/components',
],
componentPreview: {
componentIndex: {
// Derive Canvas component categories from folder names (Base/, Layout/, Card/, Hero/)
category: { directory: true, fallback: 'Misc' },
},
},
```
For working examples, see the [Canvas components in the Lupus Decoupled Nuxt Starter](https://github.com/drunomics/lupus-decoupled-nuxt-starter/tree/main/components/Canvas){rel=""nofollow""}.
## JSDoc Metadata
Components and their props are documented via TypeScript, with JSDoc annotations supplying the additional metadata that TypeScript alone cannot express. The pattern is consistent: a label (display name) and an optional description, plus additional tags.
### Component Metadata
Define component-level metadata via a JSDoc comment at the top of `
```
| Tag | Description |
| -------------- | ------------------------------------------------------------------- |
| First line | Label / display name (falls back to auto-generated from PascalCase) |
| `@description` | Component description shown in the editor |
| `@category` | Category override (alternative to directory-based) |
| `@status` | `experimental`, `stable`, `deprecated`, or `obsolete` |
All fields are optional. Config-level overrides take priority over JSDoc.
### Prop Metadata
Props follow the same pattern — a label and description, plus additional tags for editor behavior:
```vue
```
The prop label is auto-generated from the first JSDoc line or prop name. Use `@title` to override.
| Tag | Description |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `@title` | Explicit label override |
| `@example` | Adds example values (multiple allowed) |
| `@enumLabels` | Custom labels for enum values, e.g. `{"large": "Extra Large (XL)"}` |
| `@contentMediaType text/html` | Enables rich text editing in Canvas for string props |
| `@formattingContext block|inline` | Controls formatting context (default: `block`) |
| `@schemaRef` | Reference Canvas JSON schema definitions (e.g. `canvas/stream-wrapper-uri`); array variant: `@itemsSchemaRef` |
| `@format` | JSON Schema format for semantic validation and UI widgets: `date`, `date-time`, `time`, `duration`, `email`, `hostname`, `ipv4`, `ipv6`, `uuid`, `uri`, `uri-reference`, etc.; array variant: `@itemsFormat` |
| `@pattern` | JSON Schema regex pattern for string validation |
| `@allowed-schemes` | Allowed URI schemes for Canvas field type detection (e.g. `public` or `http, https`) |
| `@minItems` / `@maxItems` | Cardinality bounds for array props (see [Multi-Value Props](lupus-decoupled.org/#multi-value-array-props)) |
## Drupal Canvas Types
For Canvas integration, special TypeScript types are available that generate JSON schemas enabling Canvas UI features like media library selection. These types are auto-imported by Nuxt.
| Type | Description |
| ------------- | -------------------------------------------------------------- |
| `CanvasImage` | Image with media library integration (src, alt, width, height) |
| `CanvasVideo` | Video with poster support |
Example usage:
```vue
```
The `@example` for Canvas types supports two formats:
- Key-value: `src=https://... alt="text" width=800 height=600`
- JS object: `{ src: 'https://...', alt: 'text', width: 800 }`
### Schema References
For advanced use cases, `@schemaRef` allows referencing Canvas JSON schema definitions directly, useful for types like `stream-wrapper-uri` and `stream-wrapper-image-uri`. Use the shorthand `prefix/name` notation:
```vue
/**
* @schemaRef canvas/stream-wrapper-uri
*/
imageUri?: string
```
This expands to `json-schema-definitions://canvas.module/stream-wrapper-uri`.
## Multi-Value (Array) Props
Canvas 1.4 introduced support for multi-value props — arrays of primitives, formatted strings, enums, or known struct shapes. Declare them as TS array types; refinements TypeScript can't express are picked up via JSDoc.
### Cardinality
| Tag | Effect |
| ------------- | ------------------------------------------------------------------------------- |
| `@minItems N` | Required-array — canvas treats this as the multi-value equivalent of `required` |
| `@maxItems N` | Caps the number of entries |
### Element-shape refinements (applied to `items`)
| Tag | Effect |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@itemsFormat ` | Lifts `format` into `items` (e.g. `uri`, `uri-reference`, `date`, `date-time`, `email`) |
| `@itemsSchemaRef ` | Lifts `$ref` into `items`, including known-shape extras (`format`, `x-allowed-schemes`, `contentMediaType`) — same as `@schemaRef` for scalar props |
### Enums in items
TypeScript union literals on the element type lift into `items.enum` + auto-generated `items.meta:enum` labels. `@enumLabels` on the prop overrides those labels.
```vue
```
### Full example
```vue
```
A complete example covering every shape combination can be seen in [TestMultiValueProps.vue](https://github.com/drunomics/nuxt-component-preview/blob/1.x/playground/components/global/TestMultiValueProps.vue){rel=""nofollow""} and its [generated component-index](https://github.com/drunomics/nuxt-component-preview/blob/1.x/test/fixtures/multivalue-props.component-index.json){rel=""nofollow""}.
# Rendering modes
With Nuxt you can decide what rendering strategy you want to use, if you like even at the route level.
**Please refer
to the Nuxt documentation about [Rendering modes](https://nuxt.com/docs/guide/concepts/rendering){rel=""nofollow""} to learn more.**
The following provides some additional overview about implication of each rendering mode when using it with Lupus Decoupled Drupal:
## Server-side Rendering (SSR) - default
Server-side rendering is used as a default, which is best for SEO, and works the most seamlessly for editors thanks to the
support of Drupal authentication. Nuxt supports server-rendering via regular (Node.js) servers or in a serverless way, e.g. at the [edge](https://nuxt.com/docs/guide/concepts/rendering#edge-side-rendering){rel=""nofollow""}.
## Static-site Generation (SSG)
Static-site generation results in a set of pre-generated files which a regular HTTP server or a CDN can serve, what can help to
make hosting much more simple.
Additionally, by publishing only static files, site security can be improved. However, there is also a drawback:
Editors need to be aware of the pre-rendering step, the frontend is static and no drupal-forms are supported (atm). Nuxt supports a [preview-mode](https://nuxt.com/docs/api/composables/use-preview-mode#usepreviewmode){rel=""nofollow""} which can be implemented and provided to editors though. Read more about it in the Nuxt [Prerendering Guide](https://nuxt.com/docs/getting-started/prerendering){rel=""nofollow""}.
## Client-side Rendering (CSR)
When SEO is not important and a small client-side rendering delay is acceptable to users, client-side rendering can
be a simple alternative to server-side rendering, that does not require any special infrastructure.
Just like with SSG, simply hosting static files is enough. At the moment, no drupal-forms are supported, but Drupal authentication works with the help of the Lupus Decoupled CORS sub-module.
For a zero-infrastructure alternative, see [Providing Themes](lupus-decoupled.org/drupal/themes): [Lupus CSR](https://www.drupal.org/project/lupus_csr){rel=""nofollow""} allows bundling the frontend directly as a Drupal theme, with no separate frontend needed. The frontend is served from Drupal and fetches content from the CE API on the same origin.
# Deployment options
The deployment of Nuxt largely depends on the frontend [rendering mode](lupus-decoupled.org/nuxt/rendering-modes) chosen. Please refer to the
Nuxt [deployment guide](https://nuxt.com/docs/getting-started/deployment){rel=""nofollow""} for additional details.
## Server-rendered
- **Run a Node.js Server** - Useful for deploying Nuxt apps to any Node.js hosting. [Details](https://nuxt.com/docs/getting-started/deployment#nodejs-server){rel=""nofollow""}
- **Edge-Side Rendering** - Run nuxt close to the user, e.g. at edge servers of a CDN. [Details](https://nuxt.com/docs/guide/concepts/rendering#edge-side-rendering){rel=""nofollow""}
Nuxt uses Nitro as its server framework. Thus, also refer to the [Nitro deployment documentation](https://nitro.build/deploy){rel=""nofollow""} for additional details.
For deployments via Docker, an example Dockerfile can be found [here](https://nitro.build/deploy/providers/koyeb#using-a-docker-container){rel=""nofollow""}.
## Static hosting
- **Static Site Generation** prerenders the site during build time. [Details](https://nuxt.com/docs/getting-started/deployment#static-hosting){rel=""nofollow""}
- **Client-side rendering** renders everything the browser. [Details](https://nuxt.com/docs/getting-started/deployment#client-side-only-rendering){rel=""nofollow""}
For serving static files a general web-server is generally good-enough, additionally there are [deployment presets](https://nuxt.com/deploy){rel=""nofollow""} available that make it
easy to trigger the pre-rendering process and serve the resulting build, e.g. with Github or Gitlab pages.
## Supported presets
Nuxt comes with lots of supported presets. Refer to the [Nuxt deployment presets](https://nuxt.com/deploy){rel=""nofollow""}.
# Resources
## Lupus Decoupled Nuxt resources
- [Nuxt Drupal CE Connector](https://github.com/drunomics/nuxtjs-drupal-ce){rel=""nofollow""}
- [Naked demo project](https://github.com/drunomics/lupus-decoupled-nuxt3-demo){rel=""nofollow""}
- [Shad-CN Demo project](https://github.com/drunomics/lupus-decoupled-nuxt3-demo-shadcn){rel=""nofollow""}
### Legacy
- [Nuxt 2 demo project (legacy)](https://github.com/drunomics/lupus-decoupled-nuxt-demo){rel=""nofollow""}
## General Nuxt resources
- [Nuxt website](https://nuxt.com){rel=""nofollow""}
- [Nuxt docs](https://nuxt.com/docs){rel=""nofollow""}
- [Nuxt modules](https://nuxt.com/modules){rel=""nofollow""}
# Deployment Strategies
Lupus Decoupled Drupal supports two fundamentally different deployment approaches: running the frontend as a separate service, or bundling it directly as a Drupal theme.
## Separate Frontend
With a separate frontend, a standalone frontend server (e.g. a Node.js/Nuxt server) handles page rendering and communicates with Drupal via the CE API. This is the default approach for production sites where SSR or SSG is needed.
Within this approach, there are two hosting strategies:
### Separated Deployment
The frontend and Drupal backend are deployed and hosted independently. This offers:
- Faster frontend deployments without dependency on Drupal maintenance windows
- Freedom to choose specialized frontend hosting solutions (e.g. serverless edge deployments)
- Independent scaling and optimization of each component
However, this adds complexity in coordinating multiple environments. For Nuxt frontends, see the [Nuxt deployment docs](lupus-decoupled.org/nuxt/deployment).
### Unified Deployment
Frontend and Drupal backend are hosted together in a single environment. This:
- Requires a hosting environment capable of running both frontend (e.g. Node.js) and Drupal
- Simplifies deployment coordination and environment management
- Enables direct communication between services (e.g. cache or search servers)
A list of unified hosting possibilities can be found [here](lupus-decoupled.org/deployment/unified-hosting).
### Frontend Rendering Modes
Both strategies support multiple rendering modes (SSR, SSG, CSR). By default, Lupus Decoupled uses a Nuxt frontend with server-side rendering. Refer to the [Nuxt rendering modes](lupus-decoupled.org/nuxt/rendering-modes) or your frontend framework's documentation for details.
## Theme-Based Deployment (CSR)
With [Lupus CSR](https://www.drupal.org/project/lupus_csr){rel=""nofollow""}, the pre-built frontend application is bundled directly as a Drupal theme. No separate frontend server is needed:
- Any standard Drupal hosting works
- The frontend application is deployed alongside Drupal as part of the theme
- It fetches content from `/ce-api/...` on the same origin
This is the simplest deployment strategy and the default for [Drupal CMS site templates](lupus-decoupled.org/advanced-topics/site-templates). The trade-off is client-side rendering, which is not ideal for SEO-critical sites.
# Unified Hosting
## Unified hosting options
::note
If you are using
[theme-based deployment](lupus-decoupled.org/deployment/deployment-strategy#theme-based-deployment-csr)
, any standard Drupal hosting works without Node.js support. The options below are for setups with a separate frontend server.
::
The following (in-complete) list of hosting providers offer specialized Drupal hosting while being able to host a decoupled
frontend.
### Amazee.io
Amazee.io provides a managed open source hosting platform based on Kubernetes. It offers [Drupal hosting](https://www.amazee.io/drupal){rel=""nofollow""} and Node.js
support. It provides the flexibility to add backend and frontend in one project, or as separate projects.
### Pantheon.io
Pantheon.io offers [Drupal hosting](https://pantheon.io/product/drupal-hosting){rel=""nofollow""} and optionally a decoupled [frontend site](https://pantheon.io/features/decoupled-cms){rel=""nofollow""}
as well. Still, the frontend is deployed separately from the backend, so each system can be deployed on its own.
See the [Deploy on Pantheon](lupus-decoupled.org/deployment/pantheon) guide for step-by-step instructions.
### upsun
upsun (by platform.sh) is a cloud application platform. It offers [Drupal support](https://devcenter.upsun.com/posts/drupal-and-upsun/){rel=""nofollow""} as well as [Node.js support](https://docs.upsun.com/get-started/here/configure/nodejs.html){rel=""nofollow""}. Depending on the
configuration, frontend and backend can be deployed as a single app, or as two separate apps.
# Deploy on Pantheon
::warning
Pantheon's Next.js hosting is currently in
**invite-only beta**
. You need beta access enabled on your Pantheon workspace before you can create Next.js sites.
::
Pantheon's [Next.js Beta](https://docs.pantheon.io/guides/decoupled/nextjs){rel=""nofollow""} hosting supports deploying **Next.js** frontends with Lupus Decoupled Drupal. Since the platform uses a framework-agnostic Node.js build pipeline under the hood, **Nuxt** frontends also work.
## Prerequisites
- A Pantheon workspace with Next.js Beta access enabled
- A Drupal site with [lupus\_decoupled](https://www.drupal.org/project/lupus_decoupled){rel=""nofollow""} installed and the custom elements API working
- A GitHub account connected to your Pantheon workspace
## Step 1: Create the frontend site on Pantheon
1. Go to Pantheon Dashboard → **Add Site** → **Next.js**
2. Choose **"Create new repository"**
::warning
Do
**not**
use "Connect existing repository" — it fails silently without any error message. Let Pantheon create the repo first, then push your code to it.
::
3. Wait for the initial template deployment to succeed
4. Note the GitHub repo that was created (e.g. `your-org/your-site-name`)
## Step 2: Prepare your frontend
Clone a Lupus Decoupled frontend starter:
::code-group
```bash [Nuxt]
git clone https://github.com/drunomics/lupus-decoupled-nuxt-starter.git my-frontend
```
```bash [Next.js]
git clone https://github.com/drunomics/lupus-decoupled-nextjs-example.git my-frontend
```
::
Configure it to point at your Pantheon Drupal backend. The recommended approach is to set the Drupal base URL as an environment variable on Pantheon via **Dashboard → Settings → Environment Variables** (only available after the first successful deployment):
::code-group
```bash [Nuxt]
NUXT_PUBLIC_DRUPAL_CE_DRUPAL_BASE_URL=https://dev-your-drupal-site.pantheonsite.io
```
```bash [Next.js]
NEXT_PUBLIC_DRUPAL_BASE_URL=https://dev-your-drupal-site.pantheonsite.io
```
::
Alternatively, you can set the base URL directly in the framework config:
::code-group
```ts [Nuxt — nuxt.config.ts]
export default defineNuxtConfig({
modules: ['nuxtjs-drupal-ce'],
drupalCe: {
drupalBaseUrl: 'https://dev-your-drupal-site.pantheonsite.io',
},
})
```
```ts [Next.js — .env.local]
NEXT_PUBLIC_DRUPAL_BASE_URL=https://dev-your-drupal-site.pantheonsite.io
```
::
::note
For Nuxt, all
`nuxtjs-drupal-ce`
options can be overridden at runtime via
`NUXT_PUBLIC_DRUPAL_CE_*`
environment variables. See the
[nuxtjs-drupal-ce documentation](https://www.drupal.org/project/lupus_decoupled/wiki/nuxtjs-drupal-ce){rel=""nofollow""}
for details.
::
## Step 3: Push to Pantheon
Push your frontend code to the GitHub repo Pantheon created:
```bash
cd my-frontend
rm -rf .git
git init && git add -A && git commit -m "Initial deploy"
git remote add origin git@github.com:your-org/your-pantheon-repo.git
git branch -m main
git push --force origin main
```
## Step 4: Wait for the build
Pantheon builds automatically on push. This typically takes 5–10 minutes.
You can check deployment status via the GitHub API:
```bash
gh api repos/your-org/your-repo/deployments --jq '.[0].statuses_url' | xargs gh api --jq '.[0].state'
```
## Step 5: Verify
Visit `https://dev-your-frontend-site.pantheonsite.io/`. You may need to click through Pantheon's sandbox page on first visit. Your Drupal content should render through the frontend.
## Node.js version
Pantheon auto-detects the Node.js version from the `engines` field in `package.json`. If not specified, it defaults to Node.js LTS (currently v22).
```json
{
"engines": {
"node": ">=22"
}
}
```