Canvas Headless quickstart
This tutorial connects a JavaScript front end to a Drupal CMS site through Canvas Headless. Editors keep the Canvas visual editor, place your app’s components on pages, and see every draft rendered by your app instead of by Drupal.
Canvas Headless is the recommended way to build a decoupled Drupal CMS site. If you only need to read content over an API and editors never preview in your front end, the plain Next.js on Drupal quickstart is enough.
What you’ll build
Section titled “What you’ll build”A front-end app scaffolded from the Canvas Headless template, running locally and registered with your Drupal CMS site. When an editor opens a page in Canvas, the editor embeds your app and your app renders the page, drafts included. You finish by adding a component of your own and placing it on a page.
Prerequisites
Section titled “Prerequisites”- A Drupal CMS 2 site installed per Install Drupal CMS in a DDEV project. You need an account with the administrator role.
- Node.js 22.19 or later, or 24.5 or later. Node 23 is not supported.
- Canvas 1.12 or later, which step 1 installs. Canvas Headless is an experimental submodule: its APIs, hooks, and configuration can change between Canvas releases, so read the Canvas release notes before each update.
- A Chromium-based browser such as Chrome or Edge. The editor embeds your app from another origin and keeps the preview session in a partitioned cookie. Over plain HTTP on
localhost, only Chromium browsers accept it.
Step 1: Add the Canvas Headless dependencies
Section titled “Step 1: Add the Canvas Headless dependencies”Drupal CMS ships Drupal Canvas. Canvas Headless is a submodule of Canvas from version 1.12.0. It builds on Simple OAuth and on Custom Elements, which Drupal CMS does not include. Require them together, and make sure Canvas is at least 1.12:
ddev composer require 'drupal/canvas:^1.12' 'drupal/simple_oauth:^6.1' 'drupal/custom_elements:^3.4'ddev drush pm:install canvas_headless -yDrush also installs simple_oauth, consumers, and custom_elements. Canvas Headless creates the OAuth consumer and scope it needs, so there is nothing to configure under Configuration > Web services > Consumers.
Step 2: Generate the signing keys
Section titled “Step 2: Generate the signing keys”Canvas Headless signs every preview credential with Simple OAuth’s RSA key pair, so the keys must exist before the first preview.
- Go to Configuration > People > Simple OAuth
/admin/config/people/simple_oauth. - Select Generate keys. In Directory for the keys, enter a path outside the web root, such as
../keys, and select Generate. - The Public Key and Private Key fields now hold the new file paths. Select Save configuration.
Generate a separate key pair in every environment. With shared keys, a preview credential minted on one copy of the site is accepted by another.
Step 3: Grant the Canvas Headless permissions
Section titled “Step 3: Grant the Canvas Headless permissions”Canvas Headless adds two restricted permissions. Go to People > Permissions and grant them to the roles that need them:
- Administer Canvas Headless frontends lets a role add, reorder, and remove the front-end apps Canvas connects to. Give it to developers and site administrators.
- Access Canvas Headless preview lets a role preview drafts through the app. Give it to every role that edits pages in Canvas.
The administrator role already has both.
Step 4: Scaffold the front-end app
Section titled “Step 4: Scaffold the front-end app”Create the app with @drupal-canvas/create. The --site-url value is your Drupal CMS site’s URL, which ddev describe prints:
npx @drupal-canvas/create@latest my-canvas-frontend \ --template nextjs \ --site-url https://my-site.ddev.site \ --agents noneUse --template astro, nuxt, or tanstack-start for the other frameworks this tutorial covers; the rest of this tutorial is identical. --agents none skips the rule files the scaffolder otherwise writes for AI coding agents.
Every template contains the same four pieces:
- A catch-all route that sends each requested path to Drupal with
fetchPage()and renders the answer with<CanvasComponentTree>. - The draft session routes that the editor uses to start a preview.
- A component metadata endpoint at
/api/canvas/components, which tells Canvas which components the app provides. - A component directory with ready-made components such as Hero, Card, and Accordion.
The scaffolder writes CANVAS_SITE_URL to .env. That is the app’s entire configuration. The app holds no client secret, because Drupal issues a fresh credential for every preview.
Step 5: Start the app and check the Canvas routes
Section titled “Step 5: Start the app and check the Canvas routes”The app’s server calls your DDEV site over HTTPS, and Node.js does not trust DDEV’s local certificate until you point it at mkcert’s root certificate. Set that in the terminal that runs the dev server, then start it:
cd my-canvas-frontendexport NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem"npm run devThe Next.js, Nuxt, and TanStack Start templates serve on http://localhost:3000; Astro serves on http://localhost:4321. Request the component metadata endpoint without a credential:
curl -i http://localhost:3000/api/canvas/componentsHTTP/1.1 401 Unauthorizedcontent-security-policy: frame-ancestors 'self'cache-control: no-storecontent-type: application/jsonwww-authenticate: Bearer
{"error":"missing_assertion","message":"Provide a Drupal preview assertion as a Bearer token. Assertions are single-use; mint a fresh one per request."}The 401 is the expected answer: the route exists and refuses a caller without a credential. Canvas checks for exactly this response when it decides whether an app is connected. The frame-ancestors header keeps the app out of other sites’ frames until a preview session names the editor’s origin; the TanStack Start adapter sets it on rendered pages only.
Step 6: Register the app with Canvas
Section titled “Step 6: Register the app with Canvas”- In Canvas, select Headless frontends in the icon rail.
- Select Add frontend, enter
http://localhost:3000in Frontend URL, and confirm. Enter the URL without a trailing slash, query, or fragment.
Your browser, not the Drupal server, checks each registered app and labels it:
| Status | Meaning |
|---|---|
| Ready | The app answered at /api/canvas/components the way the Canvas adapter does. |
| Setup needed | Something answered at that URL, but not a Canvas adapter. |
| Unreachable | Nothing answered. |
Canvas checks failing rows again every few seconds, so a row turns Ready on its own once you fix the app. Canvas previews through the first app in the list; drag the rows to change it.
Step 7: Edit a page through your app
Section titled “Step 7: Edit a page through your app”Open any page on the site and select Edit. The editor now embeds your app in a frame, and your app renders the page, including unsaved changes. A status line above the frame reads Draft session active, with the time the session renews.
The setting is site-wide: once an app is registered, every Canvas editing session previews through it.
Opening the editor as an administrator also syncs components. Canvas reads your app’s component metadata endpoint and registers each component it finds, so the component library lists the components in your codebase. There is nothing to push.
Drag the Hero component from the library onto the page. Your app renders it in the frame, and the props declared in its component.yml appear in the right-hand panel. Publish the page, copy its path from the browser’s address bar (for example /page/1, or its URL alias), and open that path on http://localhost:3000: your app serves it as ordinary markup.
Step 8: Add a component of your own
Section titled “Step 8: Add a component of your own”From the app’s root directory, scaffold a component:
npx canvas scaffold --name my-heroThe CLI creates the component in the directory set by componentDir in canvas.config.json: components/my-hero/ in the Next.js template. Open components/my-hero/component.yml and change two lines:
name: My HeromachineName: my-herostatus: truemachineName arrives as hello-world for every scaffolded component, so a second component would collide with the first. status arrives as false, which registers the component but hides it from the library. Components in a Canvas Headless app explains both fields.
Reload the Canvas editor.
Troubleshooting
Section titled “Troubleshooting”drush pm:install stops on a missing dependency
Section titled “drush pm:install stops on a missing dependency”The Composer step did not run, or it ran without drupal/simple_oauth or drupal/custom_elements. Run the composer require command from step 1, then install the module again.
The dev server never starts and prints CANVAS_SITE_URL must be set
Section titled “The dev server never starts and prints CANVAS_SITE_URL must be set”The app has no .env file in its root directory. Copy .env.example to .env and set CANVAS_SITE_URL to the site URL, with no trailing slash and no path.
The frontend row shows Setup needed
Section titled “The frontend row shows Setup needed”A server answered, but not with the adapter’s 401. Run the curl check from step 5 against the exact URL you registered. The usual causes are a wrong port, or an adapter missing from the framework configuration: withCanvas() in next.config.ts, canvas() in the integrations of astro.config.mjs, @drupal-canvas/headless-nuxt in the modules of nuxt.config.ts, or canvas() in the plugins of vite.config.ts for TanStack Start.
The preview fails with “fetch failed” or “unable to verify the first certificate”
Section titled “The preview fails with “fetch failed” or “unable to verify the first certificate””Node.js does not trust DDEV’s local certificate, so the app’s server cannot exchange the preview assertion. Stop the dev server, run export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem" in the same terminal, and start npm run dev again.
The frontend row shows Unreachable
Section titled “The frontend row shows Unreachable”Nothing answered at the URL. Canvas checks from your browser, so a localhost URL works, but the dev server must be running and the URL must open in the same browser.
Headless frontends is missing from the editor
Section titled “Headless frontends is missing from the editor”Your role lacks Administer Canvas Headless frontends. Access Canvas Headless preview is a separate permission and does not show the panel.
The preview stays blank in Firefox or Safari
Section titled “The preview stays blank in Firefox or Safari”The preview session uses a partitioned cross-origin cookie. Firefox accepts it only over HTTPS, and needs a per-site exception for the Drupal site if you block all third-party cookies. Safari 18.5 through 26.1 does not support partitioned cookies. Develop in a Chromium browser, or serve the app over HTTPS.
One page fails to open in the preview
Section titled “One page fails to open in the preview”The editor needs view access to the entity, not only edit access. Pages without a canonical URL, and paths your app does not route to its catch-all handler, cannot be previewed.
Next steps
Section titled “Next steps”- How Canvas Headless preview works explains the credential exchange and the page tree your app renders.
- Components in a Canvas Headless app covers component identity, syncing, and pulling existing components into the app.
- JSON:API basics covers listings and queries, which your app still fetches over JSON:API.
- Authentication and CORS explains Simple OAuth and consumers, which Canvas Headless builds on.
Was this page helpful?
Drupal is a registered trademark of Dries Buytaert.