ContentBlocks ContentBlocks 2.x Templating
ContentBlocks turns canvas data into front-end HTML by pairing each block and layout definition (JSON) with a template file. The definition describes inputs and structure; the template decides how saved values are rendered.
The parsed output is saved to the principal; typically that is the resource content, so getting the output from ContentBlocks to your front-end just means putting [[*content]] in your template.
See also: Parser implementation for how templates are resolved and how to register custom parsers.
Also in this section:
Table of Contents
How definitions and templates connect
Each block or layout lives in your config directory (see contentblocks.config_directories) and generally has either a .twig or .tpl file beside it with the same name.
config/
blocks/
text.json ← definition (inputs, plugins, etc.)
text.twig ← matching template
layouts/
full-width.json
full-width.twig
By convention, a template sits next to its definition and shares the same base name. For a block at blocks/structure/heading.json, ContentBlocks looks for blocks/structure/heading.{ext}.
You can however override the template path in the definition:
{
"title": "Text",
"template": "blocks/shared/wrapper.twig",
"inputs": { ... }
}
Override rules:
- A path with
/is relative to the config base (e.g.blocks/shared/wrapper.twig). - A filename only (e.g.
alternate.tpl) resolves next to the definition (e.g.blocks/foo/alternate.tplforblocks/foo/bar.json). - Paths must stay inside the config directory;
..segments are rejected.
Choosing .twig or .tpl
ContentBlocks ships with two built-in parsers:
| Extension | Parser | Best for |
|---|---|---|
.twig |
Twig | Logic, loops, includes, filters; preferred when both exist |
.tpl |
MODX placeholders | Simple output, output modifiers. |
When no extension is given in a template override or no template is set, registered extensions are tried in registration order. Twig is registered before .tpl, so blocks/text.twig wins over blocks/text.tpl when both exist.
You can only use one template file per element.
Template values
Each input stores its data under the input’s definition key, using one or more data keys declared by the input type. Simple inputs use a single value key, so a text input named text is stored as { "text": { "value": "..." } } and rendered with {{ text.value }} or [[+text.value]].
More complex inputs may have multiple data keys, these are listed in their documentation.
Templates also receive a reserved _meta variable with definition config and runtime context like block/layout keys, sanitized definitions, and position info.
Input data key reference at a glance
| Input type | Data keys | Template example (assuming input key fld) |
|---|---|---|
| text, textarea, number, select, richtext, color, hr | value |
{{ fld.value }} / [[+fld.value]] |
| snippet | snippet_call, name, snippet, properties, uncached |
{{ fld.snippet_call }}, {{ fld.name }} |
| heading | value, level |
{{ fld.value }}, {{ fld.level }} |
| image | value, source, relative_url |
{{ fld.value }}, {{ fld.relative_url }} |
| icon | value, size |
{{ icon.value }}, {{ fld.size }} |
| repeater | rows |
{% for row in items.rows %} … row.data |
| tabs | (flat input keys) | {{ fld.title.value }} — same as block inputs |
| label, spacer, error | (none saved) | — |
Label and spacer inputs do not persist data and need no template placeholders.
The _meta variable (RenderContext)
_meta is a system-provided object available in every block and layout template. Keys starting with _ signal that the variable is managed by ContentBlocks, not editor input data. You should not create blocks starting with an _.
Block templates:
| Key | Description |
|---|---|
_meta.type |
Always "block" |
_meta.key |
Block key (e.g. code-multiple) |
_meta.config |
Sanitized block definition (title, inputs, drawer, modal, plugins, etc., but not keys starting with _) |
_meta.title |
Block definition title |
_meta.layout |
Parent layout info when rendered inside a layout (key, title, config) |
_meta.column |
Column reference (e.g. main) |
_meta.idx |
1-based index of the block within its column |
_meta.uniqueIdx |
1-based canvas-wide render counter |
Layout templates:
| Key | Description |
|---|---|
_meta.type |
Always "layout" |
_meta.key |
Layout key |
_meta.config |
Sanitized layout definition (title, columns, modal, plugins, etc., but not keys starting with _) |
_meta.title |
Instance title from the canvas, or definition title as fallback |
_meta.idx |
1-based index of the layout on the canvas |
_meta.uniqueIdx |
1-based canvas-wide render counter |
_meta.columns |
Column definitions from the layout config |
Internal definition keys (such as _configPath) are stripped from _meta.config. When a block is rendered in isolation (outside a layout render), _meta may only include type, key, config, and title.
Twig example:
<section class="cb-block cb-block--{{ _meta.key }}" data-idx="{{ _meta.idx }}">
<h3>{{ _meta.config.title }}</h3>
{{ text.value }}
</section>
.tpl example:
<section class="cb-block cb-block--[[+_meta.key]]" data-idx="[[+_meta.idx]]">
<h3>[[+_meta.config.title]]</h3>
[[+text.value]]
</section>
Blocks
The template receives the block’s saved data keyed by inputKey. Each input is an object containing that input type’s data keys.
Twig:
<p class="cb-text">{{ text.value }}</p>
.tpl:
<p class="cb-text">[[+text.value]]</p>
Multi-key inputs use dotted paths in .tpl, and property access in Twig:
<img src="{{ image.value }}" alt="">
<{{ heading.level|default('h2') }}>{{ heading.value }}</{{ heading.level|default('h2') }}>
<img src="[[+image.value]]" alt="">
Layouts
Layout templates receive:
- Each column’s rendered HTML under the column
key(e.g.main,sidebar); in Twig already marked as safe HTML. - A
columnsarray mapping column keys to rendered strings. - Layout settings from the layout instance
dataobject (modal inputs), using the same{ inputKey: { value: ... } }shape as blocks.
Twig:
<div class="cb-layout{% if cssClass.value %} {{ cssClass.value }}{% endif %}">
<div class="row">
<div class="large-8 columns">{{ main }}</div>
<div class="large-4 columns">{{ sidebar }}</div>
</div>
</div>
.tpl:
<div class="cb-layout[[+cssClass.value:notempty=` [[+cssClass.value]]`]]">
<div class="row">
<div class="large-8 columns">[[+main]]</div>
<div class="large-4 columns">[[+sidebar]]</div>
</div>
</div>
Column-only example (no layout settings):
Twig:
<div class="row">
<div class="large-8 columns">{{ main }}</div>
<div class="large-4 columns">{{ sidebar }}</div>
</div>
.tpl:
<div class="row">
<div class="large-8 columns">[[+main]]</div>
<div class="large-4 columns">[[+sidebar]]</div>
</div>
Common examples
Simple text block
Definition (blocks/text.json):
{
"title": "Text",
"inputs": {
"text": { "type": "text" }
}
}
Twig (blocks/text.twig):
<p class="cb-text">{{ text.value }}</p>
.tpl (blocks/text.tpl):
<p class="cb-text">[[+text.value]]</p>
Blocks in a modal window
These are the equivalent to field settings in v1.
Modal inputs use the same data shape as inputs and drawer. Values are stored in the block data from initialization (including input defaults), so templates can use them even if the editor never opened the settings modal.
The value updates the block’s store in real time when making changes.
Definition excerpt:
{
"modal": {
"style": {
"type": "select",
"properties": {
"options": [
{ "label": "Info", "value": "info" },
{ "label": "Warning", "value": "warning" }
]
}
}
}
}
Twig:
<div class="alert alert-{{ style.value|default('info') }}">{{ text.value }}</div>
.tpl:
<div class="alert alert-[[+style.value:default=`info`]]">[[+text.value]]</div>
Blocks in a drawer
The drawer appears when focusing on a block.
Definition excerpt:
{
"drawer": {
"style": {
"type": "select",
"properties": {
"options": [
{ "label": "Info", "value": "info" },
{ "label": "Warning", "value": "warning" }
]
}
}
}
}
Twig:
<div class="alert alert-{{ style.value|default('info') }}">{{ text.value }}</div>
.tpl:
<div class="alert alert-[[+style.value:default=`info`]]">[[+text.value]]</div>
Repeater list
Repeater inputs store rows under {inputKey}.rows. Each row has id, block, and data — where data holds the repeater row’s inputs using the same { inputKey: { value: ... } } shape.
Twig:
<ul class="cb-list">
{% for row in items.rows %}
{% set item = row.data %}
<li>
<strong>{{ item.title.value }}</strong>
{{ item.text.value|nl2br }}
</li>
{% endfor %}
</ul>
For repeaters, use Twig, because it supports loops natively.
Tabs inputs
Tabs inputs store nested field values flat by input name on the tabs input key. There is no tabs wrapper or activeTab in saved data. See Tabs input type.
Twig (e.g. code-multiple block with a languages tabs input):
{{ languages.javascript.value }}
{{ languages.php.value }}
.tpl:
[[+languages.javascript.value]]
[[+languages.php.value]]
Quote block with multiple fields
Twig:
<blockquote class="cb-quote">
<p>{{ quote.value }}</p>
{% if author.value %}
<footer>— {{ author.value }}{% if origin.value %}, <cite>{{ origin.value }}</cite>{% endif %}</footer>
{% endif %}
</blockquote>
.tpl:
<blockquote class="cb-quote">
<p>[[+quote.value]]</p>
[[+author.value:notempty=`<footer>— [[+author.value]][[+origin.value:notempty=`, <cite>[[+origin.value]]</cite>`]]</footer>`]]
</blockquote>
Full-width layout
Definition (layouts/full-width.json):
{
"title": "Full-width layout",
"columns": [{ "key": "main", "title": "Main" }]
}
Twig (layouts/full-width.twig):
<div class="row">
<div class="large-12 columns">{{ main }}</div>
</div>
MODX tags in .tpl templates
During block and layout rendering, ContentBlocks uses a restricted parser (cbParser) that only processes:
- Placeholders —
[[+key]], including dotted keys like[[+text.value]]and[[+image.relative_url]] - Resource fields —
[[*pagetitle]],[[*id]], etc.
Chunks, snippets, and other MODX tags are not processed in this pass, so they can be rendered by the standard MODX parser, when the page is requested.
Simple placeholders without output filters are substituted before MODX parsing; remaining [[+...]] tags are processed through the restricted parser.
Twig environment
Twig templates use this resolution order for the Twig environment:
- Twig for MODX 3 package —
twigparserservice, when installed - External environment — if set via
TwigEnvironmentFactory::setExternalEnvironment() - Bundled Twig — shipped under
core/components/contentblocks/dependencies/twig/as fallback if no Twig is available already.
The filesystem loader is rooted at your config’s base path, so templates can use {% include 'blocks/partials/wrapper.twig' %} for shared partials within the config tree.
ContentBlocks registers an escape_modx filter on that environment (including the Twig for MODX environment, when that extra is installed). Use it when a value should be shown as text and MODX tags inside it must not be parsed. See Code.
Render errors
If a template file is missing or parsing fails, ContentBlocks outputs an HTML error block (cb-render-error) for that element and continues rendering siblings. Check the MODX error log for details ([ContentBlocks] prefix).
Missing templates list the path searched and extensions tried (e.g. twig, tpl).
Further reading
- Parser implementation — architecture, custom parsers, registration
- Blocks — block and layout definition structure