Quickstart
Goal: have the Drupal Canvas editor render its preview through your own frontend app, with your app’s components placeable on a page by an editor.
What you’ll have when you’re done
Section titled “What you’ll have when you’re done”- A frontend app scaffolded from the Canvas Headless template (or an app you already have, wired with the same SDK), running locally and serving the two routes Canvas requires of it.
- That app registered on your site’s
Headless frontendslist, showing statusReady. - The Canvas editor rendering its preview from your app instead of from Drupal, unpublished content included.
- Your app’s own components in the Canvas component library, and one of them placed on a page.
Prerequisites
Section titled “Prerequisites”- A Source CMS site you can administer: the
Editbutton (top right on any page of the site) opens the Canvas visual editor. (No site? A self-serve Cloud Platform free trial gets you building on Acquia the same day, though its Drupal 11 application is the Cloud Platform path, not a Source CMS site; for one of those, request a Source demo.) - The
Administer Canvas Headless frontendsandAccess Canvas Headless previewpermissions on your account. Both are restricted permissions granted per role. - Node.js 22 or later; npm ships with it.
- A Chromium-based browser while you develop over plain HTTP. The editor embeds your app cross-origin and the preview session rides on a partitioned cookie, so Firefox needs HTTPS and Safari needs a version with CHIPS support.
Check Canvas Headless is on
Section titled “Check Canvas Headless is on”Canvas Headless is on when Headless frontends sits in the icon rail down the left edge of the Canvas editor; seeing it needs the Administer Canvas Headless frontends permission from the prerequisites. On a Source CMS site, where Acquia manages the codebase, that is the whole check.
Pick your framework. The tab you choose stays selected across pages.
-
Scaffold the frontend app
Section titled “Scaffold the frontend app”The scaffolder writes
CANVAS_SITE_URLinto the project’s.envfrom--site-url, so point it at your site. To connect an app you already have, Wire an app you already have replaces this step and step 2; pick back up at step 3.Terminal window npx @drupal-canvas/create@latest my-canvas-frontend \--template nextjs \--site-url https://your-site.example.com \--agents noneThe Next.js template wires the adapter in
next.config.tswithwithCanvas(), and mounts the Canvas routes itself as four files underapp/api/.Terminal window npx @drupal-canvas/create@latest my-canvas-frontend \--template astro \--site-url https://your-site.example.com \--agents noneThe Astro template wires the adapter in
astro.config.mjsas thecanvas()integration, which injects the Canvas routes for you. Draft preview is per-request, so the project runs with an SSR adapter (@astrojs/node).Terminal window npx @drupal-canvas/create@latest my-canvas-frontend \--template nuxt \--site-url https://your-site.example.com \--agents noneThe Nuxt template wires the adapter by listing
@drupal-canvas/headless-nuxtinmodules, and the module mounts the Canvas routes as Nitro server handlers.Terminal window npx @drupal-canvas/create@latest my-canvas-frontend \--template tanstack-start \--site-url https://your-site.example.com \--agents noneThe TanStack Start template wires the adapter in
vite.config.tswith thecanvas()plugin, and mounts the Canvas routes itself as five route files undersrc/routes/api/, the fifth serving the editor’s isolated component preview.┌ Drupal Canvas create│◇ Fetched template│● Created .env from .env.example.│● No additional agents selected. Skipping compatibility setup.│◇ Installed dependencies with npm│◇ Initialized Git repository on main with initial commit│◇ Get started ─────────────────────────────╮│ ││ Created project in ./my-canvas-frontend ││ ││ Next steps: ││ cd my-canvas-frontend ││ npm run dev ││ │├───────────────────────────────────────────╯│└ Canvas project created successfully.--agents noneskips generating per-agent rule files for Cursor, Copilot, and similar.Have your coding agent drive the scaffold. Drop
--agents noneso the scaffolder also generates rule files for your agent (Cursor, Copilot, and similar), and hand it the goal:Scaffold a Drupal Canvas headless frontend withnpx @drupal-canvas/create@latest my-canvas-frontend --template <nextjs|astro|nuxt|tanstack-start> --site-url https://<my site>(keep the agent rule files it generates), start the dev server, and confirmGET /api/canvas/components answers 401 with WWW-Authenticate: Bearer.This page’s Markdown export (the
View as Markdowncontrol at the top) gives your agent the complete instructions and captured output. With that run done, the app is up and answering; continue at register the app with Canvas.Whatever the framework, the template gives you the same four things: a catch-all route that resolves any Drupal path with
fetchPage()and renders the result with<CanvasComponentTree>, the draft session routes, the component metadata endpoint at/api/canvas/components, and a directory of ready-made components.CANVAS_SITE_URLis the entire configuration. There is no client secret, because the app never holds a standing credential. -
Start the app and check the Canvas routes
Section titled “Start the app and check the Canvas routes”Terminal window cd my-canvas-frontendnpm run devThe dev server prints its local URL:
http://localhost:3000for the Next.js, Nuxt, and TanStack Start templates,http://localhost:4321for Astro. Confirm the app is serving the Canvas contract, against the URL yours printed:Terminal window 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."}A
401here is the success case: the route exists and is refusing a caller that brought no credential. This is the exact response Canvas looks for when it decides whether an app is connected. Theframe-ancestorsheader is the adapter’s doing; it keeps the app un-embeddable until a live preview session names the editor’s origin. The TanStack Start template’s response carries every header shown exceptcontent-security-policy: its adapter currently sets that header on rendered pages, not on this route. -
Register the app with Canvas
Section titled “Register the app with Canvas”Open Canvas and pick
Headless frontendsfrom the icon rail down the left edge. ClickAdd frontend, put the URL your dev server printed into the dialog’s oneFrontend URLfield, and confirm. The URL takes no trailing slash, no query, and no fragment.Your browser, not the Drupal server, probes each registered app and labels the row:
Ready: the app answered at/api/canvas/componentsthe way the adapter does.Setup needed: something answered at that URL, but not a Canvas adapter.Unreachable: nothing answered.
Failed rows are re-probed every few seconds, so a row that starts wrong turns green on its own once you fix the app. Canvas previews through the first app in the list; drag rows to reorder.
-
Open a page in the Canvas editor
Section titled “Open a page in the Canvas editor”Open any page of your site and click
Edit. The editor no longer draws the preview itself: it embeds your app in a frame, and what fills the canvas is your frontend rendering that page, drafts included. The toolbar names the active app’s host and lets you switch between registered apps, and a status line above the frame readsDraft session active — renews automatically around <time>.This is site-wide, not per page. Once a frontend is registered, every Canvas editing session on the site previews through the active app.
The same load synchronizes components. Canvas reads your app’s component metadata endpoint and registers every component it finds, so the component library now lists the components in your codebase. There is nothing to push.
-
Place one of your components
Section titled “Place one of your components”Open the component library and drag one of your app’s components onto the page: the template ships a set of them, including
Hero,Card, andAccordion. Canvas records the placement and your app renders it in the frame, with the props declared in that component’scomponent.ymlnow editable in the right-hand panel. Publish the page and the same composition renders on the app’s own URL, this time as ordinary application markup: the selection markers only exist while a draft session is live. -
Add a component of your own
Section titled “Add a component of your own”From the project root:
Terminal window npx canvas scaffold --name my-hero┌ Drupal Canvas scaffold│◇ Created component││ Created: my-hero│ Directory: components/my-hero│ Component metadata: components/my-hero/component.yml│ Source file: components/my-hero/index.jsx│ CSS file: components/my-hero/index.css│└ Scaffold completedThe directory comes from
componentDirincanvas.config.json:componentsin the Next.js template,src/componentsin Astro and TanStack Start,app/componentsin Nuxt.Open the new
component.ymland change two lines.machineNamearrives as the placeholderhello-world, the same for every component the scaffolder writes, so the second one you scaffold would collide with the first. It is the component’s identity in Canvas and the key your app’s generated registry renders by; it does not have to match the directory name, but a name that does is easier to live with.statusarrives asfalse, which registers the component without offering it for placement:name: My HeromachineName: my-herostatus: trueReload the Canvas editor.
My Herois now in the component library, ready to drag onto a page. Drupal will not let anyone rename it or flip its status there: an external component’s identity belongs to the app that implements it. ChangemachineNamelater and Canvas treats it as a different component, registering the new name and deactivating the old entry.Ask your agent for the component; the Components page’s Markdown export gives it the identity and status rules:
Scaffold a Canvas component with npx canvas scaffold --name my-hero, then in itscomponent.yml set machineName: my-hero and status: true.Reload the Canvas editor afterwards; components synchronize when it loads.
What just happened
Section titled “What just happened”Two halves met. On the app side, CANVAS_SITE_URL is the whole configuration, because the credential is minted per preview rather than stored. When you opened the editor, Drupal signed a single-use, short-lived assertion and pointed the embedded frame at your app’s /api/draft, which exchanged it at your site’s /oauth/token for an access token bound to your own user account. Unpublished content appears in the preview because you are allowed to see it, not because the app holds a privileged key, and the token’s reach is capped to a read-only preview ceiling.
On the rendering side, the catch-all route hands the requested path to fetchPage(), which asks Drupal to resolve it the way it would for a visitor and hand back the routed entity as a tree of elements rather than as finished HTML. <CanvasComponentTree> walks that tree and renders each element with the component of the same machine name in your codebase. That is the part plain JSON:API cannot do: JSON:API answers “give me entity X”, not “what does this URL resolve to, and which components compose it”. Listings and detail queries still go over JSON:API, through the same client the direct-fetch guide covers.
Canvas Headless first shipped in Drupal Canvas 1.9.0, released 29 July 2026. Its SDK packages are all pre-1.0, so treat their APIs as movable.
When something goes wrong
Section titled “When something goes wrong”Error: CANVAS_SITE_URL must be set. See .env.example., and the dev server never starts
the app has no .env, or the file is not in the project root next to package.json. The scaffolder writes it from --site-url; if you scaffolded without that flag, copy .env.example to .env and fill in your site’s base URL with no trailing slash and no path.
The frontend row shows Setup needed
a server answered at that URL, but not with the adapter’s 401. Run the curl check from step 2 against the exact URL you registered. The usual causes are a wrong port and an app whose adapter never reached the build: withCanvas() in next.config.ts, canvas() in astro.config.mjs’s integrations, @drupal-canvas/headless-nuxt in nuxt.config.ts’s modules, or canvas() in vite.config.ts’s plugins for TanStack Start.
The frontend row shows Unreachable
nothing answered. Canvas probes each app from your browser rather than from the Drupal server, so a localhost dev server is fine, but the URL has to be one your browser can open, and the dev server has to still be running.
The preview never starts, or starts and stays blank, in Firefox or Safari
the preview session depends on a cookie set cross-origin, in a partition. Chromium-based browsers work over HTTPS, and also against a plain-HTTP localhost dev server. Firefox needs HTTPS, plus a per-site exception for the Drupal site if you block all third-party cookies. Safari follows CHIPS availability, which it lacks in versions 18.5 through 26.1.
Headless frontends is missing from Canvas
your account does not have the Administer Canvas Headless frontends permission. Access Canvas Headless preview is the separate permission editors need to preview through an app, and a role can hold either one without the other.
One entity fails to open in preview while others work
previewing needs view access to the entity, not only edit access. An entity with no canonical URL, or one whose path your app does not serve, cannot be previewed either.
A component you added is not in the component library
check its component.yml. status has to be true; a component left at the scaffolder’s status: false does synchronize, and Drupal records it, but it is not offered for placement. Check machineName too: left at the scaffolder’s hello-world, a second component collides with the first. Reload the editor afterwards, since components synchronize when the editor loads.
Next steps
Section titled “Next steps”- Components: where this app’s components live, what gives one its identity, and how they stay in sync with the site.
- First fetch: the advanced path. Add direct JSON:API reads to this app with the Drupal API Client; the steps apply to this app as written.
- Canvas components quickstart: props, slots, and the component format your app’s component directory already uses.
- Canvas CLI reference:
canvas pullbrings components that already exist on your site into the app’s codebase. - Fetch content directly: where direct fetches belong, caching, and the JSON:API queries that sit alongside
fetchPage().
Was this page helpful?
What went wrong?
Still stuck? Contact Acquia Support (opens in a new tab)