Managing content in Drupal CMS via JSON:API. Covers listing, fetching, creating, updating, and deleting pages and content entities using npm run content commands. Includes page component structure (uuid, component_id, inputs, parent_uuid, slot), image uploading and media target_id handling, formatted HTML text fields, UUID generation, input format reference mapping component.yml prop types to JSON formats, menu item listing (menu_items--main, menu_items--footer, menu_items--social-media are read-only via API — must use admin UI for create/update/delete), page path alias management, and common pitfalls like wrong media IDs, link props as objects instead of strings, plain strings for HTML fields, target_id as integer instead of string, langcode errors, and JSON:API read-only mode. Use when composing pages, adding components to pages, uploading images, or interacting with the Drupal CMS content API.
From drupal-canvasnpx claudepluginhub ajv009/drupal-devkit --plugin drupal-canvasThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Guides agent creation for Claude Code plugins with file templates, frontmatter specs (name, description, model), triggering examples, system prompts, and best practices.
This skill provides tools for managing content in Drupal CMS via JSON:API. Use these commands to list, fetch, update, and create pages and other content entities.
All commands use a single entrypoint:
npm run content -- <command> [args...]
List content items of a specific type:
npm run content -- list <type>
npm run content -- list --types # Discover available types
Fetch one or more content items and save them locally:
npm run content -- get <type> <uuid> [<uuid>...]
npm run content -- get <type> <uuid> --include <relationships>
Examples:
npm run content -- get page abc-123-def
npm run content -- get page abc-123 def-456 ghi-789
npm run content -- get media--image uuid1 uuid2 uuid3
npm run content -- get page abc-123-def --include image,owner
Saves content to content/<type>/<uuid>.json. For media--image, automatically includes the file relationship and displays the thumbnail URL and target_id.
Create a new content item from a local file:
npm run content -- create <file-path>
After creation, the temporary file is removed and the full entity is fetched and saved with the UUID returned by the API.
Push changes from a local JSON file back to the API:
npm run content -- update <file-path>
The file must contain valid JSON:API data with data.type and data.id fields.
Delete one or more content items:
npm run content -- delete <type> <uuid> [<uuid>...]
Examples:
npm run content -- delete page abc-123-def
npm run content -- delete media--image uuid1 uuid2 uuid3
Also removes the local JSON file if it exists.
Upload an image and create a media entity:
npm run content -- upload <image-path> [alt-text]
Example:
npm run content -- upload src/stories/assets/photo.jpg "Photo description"
Output includes:
target_id for use in componentsGenerate random UUIDs for new components:
npm run content -- uuid # Generate 1 UUID
npm run content -- uuid 5 # Generate 5 UUIDs
Content is stored in the /content directory (gitignored):
content/
page/
abc-123-def-456.json
media--image/
img-123-456.json
Pages in Drupal CMS contain these key attributes:
title - Page titlestatus - Published status (true/false)path - URL path configurationcomponents - Array of canvas componentsmetatags - SEO metadatainclude_in_search - Whether to index for searchComponents are stored in data.attributes.components as a flat array. Nesting is defined via parent_uuid and slot fields:
{
"data": {
"type": "page",
"attributes": {
"title": "My Page",
"status": true,
"components": [
{
"uuid": "comp-001",
"component_id": "js.section",
"inputs": { "width": "Normal" },
"parent_uuid": null,
"slot": null
},
{
"uuid": "comp-002",
"component_id": "js.text_block",
"inputs": { "text": { "value": "<p>Hello world</p>", "format": "canvas_html_block" } },
"parent_uuid": "comp-001",
"slot": "content"
}
]
}
}
}
Note: inputs are automatically parsed to objects when fetched and stringified when sent back to the API.
| Field | Description |
|---|---|
uuid | Unique identifier for this component instance |
component_id | Component type (e.g., js.text_block, js.image_banner) |
component_version | Version hash of the component definition |
inputs | Object containing prop values |
parent_uuid | UUID of parent component (null for root-level) |
slot | Slot name in parent (null for root-level, e.g., "content") |
Images must be uploaded to the media library before referencing in components:
npm run content -- upload image.jpg "Alt text"
Output:
Uploading: image.jpg
Uploaded: image.jpg
UUID: 98eabd02-c52c-493b-8ca9-cb9d0fe70ceb
File: /var/www/html/pages/media--image/98eabd02-...json
Thumbnail: https://...
target_id: 31
When working with images, there are two different internal IDs:
| Entity Type | ID Location | Usage |
|---|---|---|
| File | drupal_internal__target_id in relationships | Internal file reference |
| Media | resourceVersion=id%3AXX in self link URL | Use this in components |
The target_id shown in command output is the correct media internal ID.
Standard Drupal CMS uses field_media_image (not media_image). The upload script auto-detects the correct field name.
Components that accept images use a target_id reference:
{
"component_id": "js.image_banner",
"inputs": {
"heading": "Welcome",
"image": { "target_id": "31" },
"text": { "value": "<p>Content</p>", "format": "canvas_html_block" }
}
}
Important: The target_id must be the media entity's internal ID, not the file's internal ID.
Rich text fields use a specific format:
{
"text": {
"value": "<p>HTML content with <a href=\"/page\">links</a>.</p>",
"format": "canvas_html_block"
}
}
npm run content -- list page
npm run content -- get page abc-123-def
# Edit content/page/abc-123-def.json
npm run content -- update content/page/abc-123-def.json
Upload images:
npm run content -- upload image1.jpg "Description"
# Note target_id: 31
Generate UUIDs:
npm run content -- uuid 3
Create page JSON at content/page/new-my-page.json:
{
"data": {
"type": "page",
"attributes": {
"title": "My Page",
"status": true,
"components": [
{
"uuid": "generated-uuid",
"component_id": "js.image_banner",
"inputs": {
"heading": "Welcome",
"image": { "target_id": "31" }
},
"parent_uuid": null,
"slot": null
}
],
"path": { "alias": "/my-page" },
"include_in_search": true
}
}
}
Create the page:
npm run content -- create content/page/new-my-page.json
Always read component.yml before composing inputs. The prop type in component.yml determines how the value must be formatted in the page JSON.
| component.yml Type | JSON Input Format | Example |
|---|---|---|
type: string (plain) | Plain string | "heading": "Hello" |
type: string with contentMediaType: text/html | Formatted text object | "text": { "value": "<p>Hello</p>", "format": "canvas_html_block" } |
type: string with format: uri-reference | Plain string (path or URL) | "link": "/about" |
type: object with $ref: .../image | target_id object (string) | "image": { "target_id": "31" } |
type: string with enum | Exact enum value string | "layout": "Left aligned" |
type: boolean | Boolean | "visible": true |
type: integer / type: number | Number | "count": 3 |
JSON:API read-only mode: If all write operations return HTTP 405, JSON:API is in read-only mode. Enable with: ddev drush config:set jsonapi.settings read_only false -y
Wrong ID type: Using file's drupal_internal__target_id instead of media's internal ID causes "image.src NULL value found" errors.
Missing langcode permission: When creating pages, omit the langcode field as the API may reject it with permission errors.
Patching limitations: PATCH requests may not work for all fields. Create a new page with a different alias if updates fail.
Link props are plain strings, not objects: Link/URL props (format: uri-reference) must be plain strings like "/about". Do NOT use Drupal's link field format { "uri": "/about", "options": [] } — that is for Drupal entity fields, not Canvas component inputs.
// Correct
"link": "/services"
// Wrong — will cause errors
"link": { "uri": "/services", "options": [] }
Formatted text fields need the wrapper object: Any prop with contentMediaType: text/html in component.yml must use the formatted text object, not a plain string. Omitting the wrapper may cause rendering failures or empty output.
// Correct
"text": { "value": "<p>Content here</p>", "format": "canvas_html_block" }
// Wrong — plain string for an HTML field
"text": "Content here"
target_id must be a string: Image references use "target_id": "31" (a string), not "target_id": 31 (an integer). Using an integer may cause type errors.
Drupal CMS exposes menu items via JSON:API, but the endpoints are read-only. Menu items can be listed but not created, updated, or deleted via JSON:API (POST/PATCH/DELETE return 405 Method Not Allowed, individual GET returns 404).
Available menu types for listing:
menu_items--main — Main navigation menumenu_items--footer — Footer linksmenu_items--social-media — Social media linksnpm run content -- list menu_items--main
npm run content -- list menu_items--footer
npm run content -- list menu_items--social-media
Menu items must be managed through the Drupal admin UI, not the JSON:API. Use browser automation to navigate the admin pages:
<CMS_URL>/admin/structure/menu/manage/<menu-name>/add (e.g., .../manage/main/add)<CMS_URL>/admin/structure/menu/manage/<menu-name> then click edit on individual itemsMenu names for admin URLs: main, footer, social-media.
Menu item fields in the admin UI:
| Field | Description |
|---|---|
| Menu link title | Display text for the menu link |
| Link | Target URL (/path for internal, full URL for external) |
| Weight | Sort order (lower = higher in menu), or drag to reorder |
| Enabled | Whether the item is visible (checkbox) |
| Parent link | Parent menu item for nested menus (dropdown) |
When creating pages, set the path alias to control the URL:
{
"data": {
"type": "page",
"attributes": {
"title": "About",
"path": { "alias": "/about" },
"status": true,
"components": []
}
}
}
Important: Verify aliases at <CMS_URL>/admin/config/search/path. Standard Drupal admin paths apply: use /admin/config/system/site-information for site name, /admin/appearance/settings for theme logo/favicon.
After creating or updating a page, verify the result in the Canvas Page Editor at <CMS_URL>/canvas/editor/canvas_page/<id>. This visual editor shows the component tree and allows you to inspect how components are nested and composed. It is especially useful for debugging slot/parent relationships and verifying that all sections appear in the correct order.
For individual component issues, use the Component Code Editor at <CMS_URL>/canvas/code-editor/component/<name> to verify the deployed component's props, slots, and live preview.
When working with the Drupal CMS content API, consult the official documentation for platform-specific behavior, API capabilities, and known limitations. Use the canvas-docs-explorer skill:
/canvas-docs-explorer JSON API content management menus
Key docs to consult:
enabling-api and using-api — API setup and usage patternsjsonapi-query-builder — query syntax and filteringmenus and adding-links-menu — menu management (read-only via API, admin UI required for writes)content-workflows — publishing states and revision behaviorfields and field-types — available field types and their API representationsAlways check the docs before assuming standard Drupal JSON:API behavior — Drupal CMS may have platform-specific differences.
The JSON:API spec can be regenerated from the live site if the openapi module is installed. Use ddev drush pm:list --type=module --status=enabled --field=name | grep openapi to check availability, then access the spec at <CMS_URL>/openapi/jsonapi?_format=json.
Required in .env:
CANVAS_SITE_URL - Base URL of Drupal CMS siteCANVAS_JSONAPI_PREFIX - API prefix (default: "jsonapi")CANVAS_CLIENT_ID - OAuth client IDCANVAS_CLIENT_SECRET - OAuth client secret