ContentBlocks ContentBlocks 2.x Developer Parser and Rendering Parser
ContentBlocks 2 renders front-end output through a custom parser. Each element gets a template file, using a supported extension that maps to a parser class.
To learn more about the built-in parsing, see Templating.
Table of Contents
Architecture overview
Canvas
└── Layout (template + layout data + column HTML)
└── Column
└── Block (template + block data)
| Component | Role |
|---|---|
RenderService |
Walks the canvas tree; resolves templates; calls parsers |
TemplateResolver |
Maps a definition to a template file and parser by extension |
ParserRegistry |
Holds extension → parser mappings (twig, tpl, custom) |
ParserInterface |
Contract for rendering files and inline strings |
ModxParser |
.tpl — MODX placeholders and resource fields |
TwigParser |
.twig — Twig templates via TwigEnvironmentFactory |
Initialization is lazy: the stack is built on first use of getRenderService(), getParserRegistry(), or registerParser() on the ContentBlocks service.
Default registration
On initialization, ContentBlocks registers:
twig→TwigParsertpl→ModxParser
Then it fires the ContentBlocks_RegisterParsers system event so extras can add or replace parsers before RenderService is constructed.
Extension registration order matters for template resolution when no extension is present in the path: the resolver tries each registered extension in order until a file exists. So the TwigParser always takes precedence over the ModxParser, which takes precedence over custom registered parsers.
Template resolution
TemplateResolver resolves templates relative to the config base path (first valid directory from contentblocks.config_directories).
Convention-based paths
For a block with config path blocks/text:
- Start from
blocks/text(no extension). - We check for
blocks/text.twig, thenblocks/text.tpl(order follows registry). - Return the first existing file with a registered parser.
Nested definitions follow the same rule: blocks/structure/heading → blocks/structure/heading.twig.
Explicit override
A definition may set an explicit "template":
{
"title": "Text",
"template": "blocks/shared/wrapper.twig"
}
If the override includes an extension, only that extension is used. If it has no extension, extensions are tried in registry order.
For security reasons, paths are sanitized and must be within the configured config directory.
Resolved result
A successful resolve returns a ResolvedTemplate with:
relativePath— path under config baseabsolutePath— full filesystem pathextension— e.g.twigparser— theParserInterfaceinstance to use
Built-in parsers
TwigParser (.twig)
- Delegates to
TwigEnvironmentFactory. - Passes block or layout data as Twig variables, including the reserved
_metaobject. - Wraps Twig errors with template path and line number.
Twig environment resolution:
- MODX
twigparserservice (from the Twig for MODX extra), with the configured config path added to its loader TwigEnvironmentFactory::setExternalEnvironment()allows an extension to explicitly tell ContentBlocks what Twig Environment to use- Local bundled Twig from
dependencies/twig/as fallback
Auto-escape is enabled (html) by default.
Layout column HTML is wrapped as Twig\Markup in RenderService, so those placeholders like {{ main }} work right away to insert parsed HTML.
When you want to insert raw HTML, use the |raw filter (e.g. richtext).
Use the |escape_modx filter when MODX tags should be escaped, for example to show code samples containing MODX tags. That filter escapes HTML and square brackets. See Code.
ModxParser (.tpl)
- Reads template file content (or treats the argument as inline string).
- Flattens nested data arrays to dotted placeholder keys (
text.value,image.relative_url). - Replaces scalar
[[+key]]placeholders directly. - Remaining tags are processed with
cbParser, an extensions of the MODX parser, which is limited to only replace placeholder (+) and resource field (*) tokens. This leaves chunks and snippets alone, so they can be rendered dynamically when the page is requested.
Use .tpl for straightforward placeholder substitution and resource field access.
ParserInterface
Custom parsers implement:
namespace modmore\ContentBlocks\Parser;
interface ParserInterface
{
public function render(string $template, array $values): string;
public function renderString(string $string, array $values): string;
}
render—$templateis the absolute filesystem path when resolving from disk; implementations should read the file whenis_file($template)is true.renderString— render an inline template string (used for string-based workflows).$values— associative array of template variables (block data, layout columns,_meta, etc.).
Throw \RuntimeException (or let exceptions bubble) on failure; RenderService catches errors and emits cb-render-error HTML for that element without stopping the rest of the canvas.
Registering a custom parser
Option 1: ContentBlocks service (bootstrap)
In your component’s bootstrap.php (loaded during MODX namespace init):
/** @var modX $modx */
$corePath = $modx->getOption('contentblocks.core_path', null, $modx->getOption('core_path') . 'components/contentblocks/');
$contentBlocks = $modx->getService('contentblocks', 'ContentBlocks', $corePath . 'model/contentblocks/');
$contentBlocks->registerParser('mustache', new MyMustacheParser());
Call registerParser() before ContentBlocks renders output. Registering after initializeParsers() has run still works because registerParser() calls initializeParsers() first, but registering during bootstrap or the event below is safest.
Option 2: System event (recommended for extras)
Listen to ContentBlocks_RegisterParsers:
<?php
switch ($modx->event->name) {
case 'ContentBlocks_RegisterParsers':
/** @var modmore\ContentBlocks\Parser\ParserRegistry $registry */
$registry = $modx->event->params['registry'];
$registry->register('mustache', new MyMustacheParser());
// Optional: replace the default Twig parser
// $registry->register('twig', new MyTwigParser($factory));
break;
}
Event parameters:
| Key | Type | Description |
|---|---|---|
registry |
ParserRegistry |
Register parsers here |
contentBlocks |
ContentBlocks |
ContentBlocks service instance |
Option 3: Replace bundled Twig
For a site-wide custom Twig setup without a new extension:
use modmore\ContentBlocks\Parser\TwigEnvironmentFactory;
use Twig\Environment;
TwigEnvironmentFactory::setExternalEnvironment($myCustomTwigEnvironment);
Do this early in bootstrap, before any ContentBlocks render.
Example: minimal custom parser
<?php
namespace MyVendor\ContentBlocks;
use modmore\ContentBlocks\Parser\ParserInterface;
class UppercaseParser implements ParserInterface
{
public function render(string $template, array $values): string
{
$content = is_file($template)
? (string) file_get_contents($template)
: $template;
return $this->renderString($content, $values);
}
public function renderString(string $string, array $values): string
{
$output = $string;
foreach ($values as $key => $value) {
if (is_scalar($value) || $value === null) {
$output = str_replace('{{' . $key . '}}', (string) $value, $output);
}
}
return strtoupper($output);
}
}
Register with extension uc and add blocks/banner.uc next to your definition.
Tips:
- Register a unique extension (letters/numbers only in the resolved path).
- Keep parsers stateless when possible; the same instance may render many blocks per request.
RenderService behavior
Blocks
- Load block definition and config path (
_configPathorblocks/{key}). - Resolve template via
TemplateResolver. - Merge
$block->getData()with a_metacontext object and pass to the parser.
Layouts
- Render each column’s blocks to HTML strings.
- Merge layout data with
columns(map of key → HTML) and spread column keys at the top level (main,sidebar, …). Column HTML is wrapped asTwig\Markupso layout templates can use{{ main }}without|raweven when autoescape is enabled. - Add
_metawith layout definition and runtime context. - Resolve layout template and render.
Canvas
Renders each layout and joins output with the implode string from contentblocks.implode_string (default: two newlines).
Error output
RenderErrorOutput generates commented, accessible HTML for:
- Missing templates (lists searched path and extensions)
- Parser exceptions (message + detail)
Other blocks and layouts on the same canvas continue to render.
Legacy parser swap (loadParser / restoreParser)
ContentBlocks 1 used loadParser() to temporarily swap MODX’s global parser for cbParser during resource save. This remains for backward compatibility but is deprecated in v2. New code should rely on ModxParser inside the render stack instead of mutating $modx->parser.
Related settings
| Setting | Effect |
|---|---|
contentblocks.config_directories |
Config base path for definitions and templates |
contentblocks.implode_string |
Separator between rendered layouts on a canvas |
contentblocks.core_path |
Fallback when config path cannot be resolved |
See also
- Building Content in PHP — programmatic canvas construction with Elements classes vs database persistence
- Templating — template variables,
.twigvs.tplexamples - Blocks — block and layout JSON structure
core/components/contentblocks/bootstrap.php— bootstrap registration example