How Canvas Headless preview works
Canvas Headless gives a decoupled Drupal site the preview that coupled sites have by default: an editor changes a page in Canvas and sees your front end render the draft. This page explains the two mechanisms behind it, the per-editor preview credential and the page tree your app renders, so you can predict what a preview shows and debug it when it shows something else.
What and why
Section titled “What and why”On a coupled site, the editor and the visitor see the same Drupal-rendered HTML, so preview is free. Once a JavaScript app renders the site, Drupal’s preview shows Drupal’s markup, not the app’s, and the app cannot see drafts because it reads the API as an anonymous visitor. Without Canvas Headless you build that integration yourself: a draft mode in the framework, a privileged API key the app uses to read unpublished content, and a way to map Drupal paths onto your components.
Canvas Headless, an experimental submodule of Drupal Canvas from 1.12.0, supplies both halves. Its APIs and configuration can change between Canvas releases; the release notes list each change. Drupal issues the app a short-lived credential for the editor who is previewing, and returns each page as a tree of components that the app renders with its own code. The Canvas Headless quickstart sets it up.
Mental model
Section titled “Mental model”The preview borrows the editor, never a key. The app holds no secret. Each time the editor opens a page, Drupal signs a single-use assertion that names that editor, and the app’s server exchanges it for an access token that acts as that editor, capped to read-only permissions. A preview shows exactly what the editor may view, nothing more. When the editor logs out of Drupal, the next renewal fails, so the preview stops within the token’s 15-minute lifetime.
Drupal answers “what is at this URL”, the app answers “what does it look like”. The app’s catch-all route sends the requested path to Drupal with fetchPage(). Drupal resolves the path the way it would for a visitor, including redirects and language, and returns the routed page as a tree of elements: each element names a component by machine name and carries its props and slots. <CanvasComponentTree> walks the tree and renders each element with the component of the same machine name in your codebase. JSON:API cannot answer this question: it returns an entity by ID, not the composed page at a path.
How Canvas uses it
Section titled “How Canvas uses it”Starting a preview
Section titled “Starting a preview”- The editor opens a page in Canvas. Canvas asks Drupal for an assertion at
/canvas-headless/assertion. The request uses the editor’s Drupal session, and the editor needs the Access Canvas Headless preview permission. - Drupal returns a JSON Web Token signed with Simple OAuth’s private key. It names the editor’s user ID and the page path, can be redeemed once, and expires after 60 seconds.
- Canvas points the embedded frame at the app’s
/api/draftroute with the assertion in the query string. - The app’s server exchanges the assertion at Drupal’s
/oauth/tokenendpoint for an access token that lasts 15 minutes. The app stores the token in anhttpOnly, partitioned cookie, so page JavaScript never sees it. - Every request the app’s server makes to Drupal for this preview carries the token, so it sees the editor’s drafts.
Before the token expires, Canvas mints a fresh assertion from the editor’s live session and the app renews the token with it. Drupal issues no refresh tokens: every renewal proves again that the editor is logged in and still holds the permission.
Capping the token
Section titled “Capping the token”The token’s permissions are the editor’s permissions intersected with a list of preview-safe view permissions. An editor who can revert revisions or administer URL aliases still gets a token that can only view. Modules declare extra view permissions that previews need with hook_canvas_headless_safe_permissions(). An undeclared permission makes a preview show too little, never too much.
Rendering the page
Section titled “Rendering the page”fetchPage() calls /canvas/content-api?requestUri=<path>. The response contains:
content: the component tree, ornullwhen Canvas does not manage the route.head: the title and meta tags for the page, in a format compatible with Unhead.route: the route name, the negotiated language, links to each translation, and the entity behind the page.
A path that Drupal redirects returns a redirect object instead, with the status code the app should send. To render one entity in a specific view mode, such as a teaser in a listing, fetchEntity() calls /canvas/content-api/entity with the entity type, ID, and view mode.
Registering components
Section titled “Registering components”The app serves its component definitions at /api/canvas/components. When someone with the Administer Canvas Headless frontends permission loads the editor, Canvas calls that endpoint with a fresh preview assertion and registers each component, so editors can place it. Loading the editor without that permission does not sync. Components in a Canvas Headless app covers the rules.
Common misconceptions
Section titled “Common misconceptions”“The front end needs an API key to show drafts.” It needs none. The credential is minted per preview for the current editor and expires in minutes. A leaked token gives read-only access as one editor for at most 15 minutes.
“Canvas Headless replaces JSON:API.” It replaces path resolution and page rendering. Listings, search results, and any query that is not “render the page at this path” still go over JSON:API. Core JSON:API filtered collections exclude unpublished content regardless of permissions, so draft content does not appear in filtered listings during preview.
“Editors can preview anything they can edit.” Preview requires view access to the entity, not only edit access. Access checks that do not use permissions, such as node access grants, follow the editor’s account and are not capped.
“Registering the app is per page or per editor.” The frontend list is site-wide. Once an app is registered, every Canvas editing session previews through the first app in the list.
“The canonical URL always shows the latest draft.” Canonical routes render the default revision. To render a forward revision, pass the latest-version or revision path to fetchPage().
Where to go next
Section titled “Where to go next”- Canvas Headless quickstart connects an app and registers it with Canvas.
- Components in a Canvas Headless app explains component identity and sync.
- Authentication and CORS covers Simple OAuth and consumers, which the preview credential builds on.
- Choose your front end weighs the decoupled architecture as a whole.
Was this page helpful?
Drupal is a registered trademark of Dries Buytaert.