Skip to content

Goal: make components render your CMS content. A content template renders every entity of one content type through a component composition, with props bound to entity fields instead of values an editor types in.

Until now, props held static values. But an article’s title shouldn’t be typed into a heading component per page; it should flow from the title field of each article. That binding is what a content template holds.

  • A content template for the article content type, versioned in your project as content-templates/node.article.full.json.
  • A component prop bound to an entity field, using the binding expressions your site actually exposes.
  • Every published article rendering through your template at its own URL.
  • A component project connected to your site with working Canvas CLI credentials (Canvas quickstart)
  • At least one content type with content on the site (this guide uses article)

To create a content type, see Creating a new content type on docs.acquia.com; to add or change its fields, see Managing fields. Templates bind to fields that already exist.

  1. Templates are created in the editor, then maintained as code. In Drupal Canvas, open the Templates panel in the left toolbar, then Add new template. Pick the content type (Article) and the view mode (Full content), and confirm. The editor opens the new, empty template at /canvas/template/node/article/full/.

    There is no admin page for this; the Templates panel is the only place templates are created or deleted.

  2. Terminal window
    npx canvas pull

    The scaffold’s canvas.config.json syncs content templates by default (sync.contentTemplates: true), into contentTemplatesDir (content-templates/). The new template arrives empty:

    content-templates/node.article.full.json
    {
    "label": "Article content items — Full content view",
    "entityType": "node",
    "bundle": "article",
    "viewMode": "full",
    "elements": {}
    }

    elements uses the same shape as page specs: keys are element UUIDs, each element has a type (js.<machineName>), props, and optional slots.

  3. Which fields can feed which props is site-specific. The CLI pulls the authoritative list:

    Terminal window
    npx canvas agents-context

    (That is the @drupal-canvas/cli 0.20.1 form, the version the scaffold pins; from 0.21 on, the command takes a provider argument, and npx canvas agents-context --all runs every default provider.) The context lands in .agents/drupal-canvas/ (built for AI coding agents, equally readable by you; the directory is .gitignored), two files of it relevant here:

    • prop-sources.json: for each entity bundle and component, every bindable prop and the exact binding expression for each candidate field.
    • view-modes.json: the view modes each bundle exposes (and therefore which templates can exist).

    An excerpt for the heading component on node/article:

    "js.heading": {
    "heading": [
    {
    "label": "Title",
    "source": {
    "sourceType": "entity-field",
    "expression": "ℹ︎␜entity:node:article␝title␞␟value"
    }
    }
    ]
    }

    The expression string is an opaque field pointer (the separators are control-picture characters). Never compose one by hand; copy it verbatim from prop-sources.json.

  4. Add elements to the template’s elements map. A prop’s value is either a literal (a plain string, number, boolean, or object) or a prop-source object copied from prop-sources.json:

    {
    "label": "Article content items — Full content view",
    "entityType": "node",
    "bundle": "article",
    "viewMode": "full",
    "elements": {
    "a1b2c3d4-0000-4000-8000-000000000001": {
    "type": "js.heading",
    "props": {
    "heading": {
    "sourceType": "entity-field",
    "expression": "ℹ︎␜entity:node:article␝title␞␟value"
    },
    "headingElement": "h1",
    "headingSize": "extra_large",
    "textColor": "dark",
    "layout": "left_aligned"
    }
    },
    "a1b2c3d4-0000-4000-8000-000000000002": {
    "type": "js.text",
    "props": {
    "text": {
    "sourceType": "entity-field",
    "expression": "ℹ︎␜entity:node:article␝body␞␟processed"
    },
    "textSize": "normal",
    "textColor": "dark"
    }
    }
    }
    }

    Two details that break a template:

    • Fill in every prop the component defines, not just the bound ones. The server rejects a template element that omits a prop key, even one the component schema doesn’t mark required ('textColor' is a required key.). Give unbound props literal values.
    • For rich-text fields, bind the processed property (rendered HTML), not value (raw stored text); prop-sources.json labels them (Body vs. raw variants).
  5. Terminal window
    npx canvas validate
    npx canvas push

    validate checks templates alongside components (Content templates / Valid: node.article.full); push uploads the template with the canvas:content_template scope. Command flags for both are in the Canvas CLI reference.

  6. Open any published article at its own URL. The title renders through your heading element, the body through your text element; every article on the site now flows through the same template. Editing the template and pushing again restyles them all at once.

[layout.0.inputs] 'textColor' is a required key. (from canvas push)

a template element omits a prop the component defines. Add the missing keys with literal values (step 4).

A bound rich-text prop renders empty, but the field has content

the entity’s stored text format doesn’t exist on the site, so the processed output is empty. Check the entity’s body field format against the site’s configured formats.

The template pulls but your content type isn’t in prop-sources.json

re-run npx canvas agents-context after any field or content-type change on the site; the files are a snapshot, not live.

You want a template for a view mode that isn’t offered

the Add new template dialog only lists view modes the bundle exposes (see view-modes.json). A view mode has to exist on the bundle before a template can target it; enable it for the bundle on the site, then re-run npx canvas agents-context so view-modes.json picks it up.

Was this page helpful?