feat(Headless): #3591861 Add a Canvas-owned routed content endpoint for headless apps
Adds a Canvas-owned endpoint for retrieving routed content:
GET /canvas/content-api?requestUri={requestUri}
The endpoint routes the supplied request URI through Drupal. It returns Canvas content only when Canvas manages the complete component tree. The response also contains an Unhead-compatible document head and normalized route information.
Matched routes that Canvas does not render still return a successful response with content set to null. Route-not-found and access-denied responses use RFC 9457 Problem Details.
Headless SDK support for the new endpoint is added in a separate MR: !1437
Comparison with /ce-api
| Behavior | Lupus /ce-api/{path} |
Canvas /canvas/content-api?requestUri={requestUri} |
|---|---|---|
| Ownership | Provided by lupus_decoupled_ce_api and lupus_ce_renderer |
Provided by Canvas Headless using Custom Elements |
| Request mapping | Removes the /ce-api prefix and routes the remaining path through Drupal |
Requires requestUri to be a path inside the Drupal site with no #fragment, then routes it through Drupal |
| Supported routes | Supports Drupal routes that Custom Elements can render | Routes accessible Drupal paths, but returns content only for canonical content entities with a complete Canvas tree or an enabled full content template |
| Unsupported routes | Returns a Custom Elements error page, normally with status 406; admin routes may redirect to Drupal | Returns content: null with available head and route data when Canvas cannot render the matched route; route and access failures use RFC 9457 Problem Details |
| Content formats | Supports Custom Elements as JSON or markup | Always returns the Custom Elements JSON shape, even when the site's global Custom Elements setting uses another format |
| Content roots | Canonical entity routes return one entity element; other routes may return a list when a top-level renderless-container is removed |
Returns one element or null; multiple roots stay inside a transparent renderless-container wrapper |
| Content response | Returns title, breadcrumbs, metatags, content_format, content, page_layout, local_tasks, and messages |
Returns content, head, and route |
| Document metadata | Returns the title and Metatag module data in Lupus-specific response fields | Returns title, meta, non-canonical, non-stylesheet links, and JSON-LD entries in a head object compatible with the Unhead library |
| Route information | Does not include route or entity details in a standard shape | Returns the route name, requested URI, route parameters, and rendered entity details when available |
| Redirects | Returns redirect and optional Drupal messages as JSON |
Returns the redirect URL, original status code, and external URL flag as JSON |
| Error format | Renders Drupal error routes through the Custom Elements renderer | Returns application/problem+json responses |
| Partial responses | Supports _select=content |
Returns the complete endpoint response, support for include param will be added later |
| Response identification | Adds X-Drupal-CE: page or X-Drupal-CE: redirect |
Uses standard JSON and Problem Details response types |
| Draft previews | By default doesn't support Canvas auto-save data | Selects entity and content-template auto-save data for preview requests and checks entity access again after loading it |
| Extension points | Supports response overrides and hook_lupus_ce_renderer_response_alter() |
Provides a focused API response owned by Canvas |
Content response
{
"content": {
"element": "js-example-component"
},
"head": {
"title": "Example page",
"meta": [
{
"name": "description",
"content": "Example description"
},
{
"property": "og:title",
"content": "Example page"
}
],
"script": [
{
"type": "application/ld+json",
"textContent": {
"@context": "https://schema.org",
"@type": "WebPage",
"name": "Example page"
}
}
]
},
"route": {
"name": "entity.canvas_page.canonical",
"requestUri": "/page/1",
"params": {
"canvas_page": "1"
},
"entity": {
"entityType": "canvas_page",
"bundle": "canvas_page",
"id": "1",
"uuid": "773942c6-3660-4c50-9a8d-e25966a69bff",
"langcode": "en"
}
}
}content contains one structured root when Canvas manages the complete component tree. Multiple roots use a transparent renderless-container with the ordered roots in its default slot. When Canvas does not render the matched route, content is null.
head is compatible with the Unhead library. It always contains title and may also contain meta, link, and script. Canonical links are omitted because the frontend owns its public URLs.
Redirect response
{
"redirect": {
"external": false,
"url": "/new-path",
"statusCode": 301
}
}Redirect results use HTTP 200. statusCode is the status the frontend should use for the browser redirect.
Error response
Route-not-found, access-denied, and invalid requests use RFC 9457 Problem Details with the application/problem+json media type:
{
"type": "about:blank",
"title": "Not Found",
"status": 404
}detail is included when an additional explanation is available, such as when requestUri is missing or invalid.
AI-Generated: Yes (Used AI coding agent to work on implementation).