This extension documentation is published under the
CC BY-NC-SA 4.0
(Creative Commons) license.
An extension for TYPO3 CMS that provides an entire rendering environment for
Handlebars templates. It is seamlessly integrated into TYPO3 and offers
extensive configuration options to get all the power out of your templates.
To meet everyone's needs, it is easily extensible using TYPO3 on-board tools.
Introduction
A quick overview about the main features provided by this extension.
Installation
Instructions on how to install this extension, and which TYPO3 and PHP versions
are currently supported.
Configuration
Learn how to configure the extension in various ways. This includes extension
configuration, site configuration and TypoScript configuration.
Usage
This section describes how to use this extension in various ways, and which
additional components exist.
Developer corner
A quick overview about all relevant classes provided by this extension.
Guides
Task-oriented guides for common integration and migration scenarios,
including a step-by-step path for migrating from Fluid to Handlebars.
Migration
Required migration steps when upgrading the extension to a new major version.
Introduction
What does it do?
The extension provides a full rendering environment for Handlebars templates
within TYPO3 CMS. All core features of
Handlebars.js are supported by the usage of the third-party library
PHP Handlebars.
Its main use is to seamlessly integrate Handlebars templates into TYPO3 without
the need to modify these templates again for output in TYPO3.
Features
Templating engine: Full Handlebars rendering environment for TYPO3
Custom Helpers: Custom helpers with auto-registration via PHP attributes
Extbase support: Controller-based rendering via
HandlebarsView
Events: PSR-14 hooks into the full rendering pipeline
DI integration: Built on dependency injection for better performance and maintainability
Caching: Integration with TYPO3's cache framework for compiled templates
Extensibility: Easy to extend and customize
Compatibility: Compatible with TYPO3 13.4 LTS and 14.3 LTS
Support
There are several ways to get support for this extension:
The extension provides two site sets that can be included in the site
configuration or any other site set.
cpsit/handlebars — Handlebars base
Wires
plugin.tx_handlebars.view.templateRootPaths and
plugin.tx_handlebars.view.partialRootPaths from the site settings
handlebars.view.templateRootPath and
handlebars.view.partialRootPath.
Include this set for every site that renders Handlebars templates.
cpsit/handlebars-content-element — Handlebars content elements
Sets
lib.contentElement = HANDLEBARSTEMPLATE, replacing the
default Fluid base object used by EXT:fluid_styled_content.
Include this set when all content elements should use Handlebars rendering by default.
The extension is configured through TypoScript, the Symfony service
container, and TYPO3's extension configuration. This section covers all
available options.
Template and partial root paths are collected from various sources, each
with a distinct priority. Higher-priority sources win over lower-priority ones.
Within a single source, higher numeric keys override lower ones.
Template and partial root paths can be set directly inside a
HANDLEBARSTEMPLATE content object. These paths apply only to
that specific rendering, including any nested partial lookups triggered by it.
The
cpsit/handlebars site set also populates these paths
from the site settings
{$handlebars.view.templateRootPath} and
{$handlebars.view.partialRootPath}.
Note
When multiple extensions declare paths under the same numeric key, the last
one loaded wins. Use distinct keys (e.g., 10, 20, 30) to ensure all paths
are registered.
Service container (priority 0)
The lowest-priority source is the service container. Paths registered here
apply instance-wide, regardless of the current page or content object, and
serve as the global fallback.
The
HandlebarsExtension DI extension merges all paths declared this
way into the container parameters
%handlebars.templateRootPaths% and
%handlebars.partialRootPaths%.
Template name resolution
Once the root paths are collected, the active
TemplateResolver turns a template
or partial name into an absolute file path. The default resolver supports two
addressing styles: directory-relative paths and flat @-prefixed names.
Flat template names
The default template resolver lets you reference any template or partial by
its bare filename rather than a directory-relative path. Prefix the name with
@ to use this addressing:
The resolver scans all configured root paths and finds the file by name
regardless of its subdirectory. If the same filename exists in multiple root
paths, the higher-priority root path wins. This matches the
Fractal template
resolution convention, making it straightforward to use a Fractal component
library as the template source.
Appending --<variant> selects a named variant and falls back to the base
name automatically if no dedicated file exists:
{{> @card--highlighted}}{{!-- falls back to @card if not found --}}
Copied!
Names without the @ prefix are resolved as directory-relative paths in
the usual way.
See also
TemplateResolver for implementation details and
how to replace the resolver entirely.
Variables
Template variables are available at two scopes: globally for every rendering,
and locally for a single content object rendering.
When the same key is defined in both sources, the TypoScript value takes
precedence.
Per-rendering variables
Variables scoped to a single rendering are declared in the
variables property of a
HANDLEBARSTEMPLATE
content object. Each entry is processed as a standard content object
against the current record's data:
tt_content.header = HANDLEBARSTEMPLATE
tt_content.header {
templateName = Header
variables {
header = TEXT
header.field = header
subheader = TEXT
subheader.field = subheader
link = TEXT
link.typolink.parameter.field = header_link
}
}
Copied!
Entries with no sub-configuration are treated as simple variables and
passed to the template as-is, without invoking
ContentObjectRenderer:
variables {
# Content object — field value is rendered via cObjGetSingle
header = TEXT
header.field = header
# Simple variables — values are passed through directly
cssClass = my-element
theme = dark
}
Copied!
Two variables are always injected automatically and cannot be overridden
(this reflects the same behavior as in
FLUIDTEMPLATE):
data
The full data array of the current content element record.
current
The value of the current field (
$cObj->currentValKey).
Warning
Declaring
data or
current in
variables is not allowed and raises an exception.
The extension registers a cache named handlebars that stores compiled
Handlebars templates. The cache is registered automatically on extension
activation; no manual setup is required.
The default backend is the TYPO3 database cache. To use a different backend,
add an override to your extension's ext_localconf.php file:
ext_localconf.php
if (!isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['handlebars']['backend'])) {
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['handlebars']['backend']
= \TYPO3\CMS\Core\Cache\Backend\SimpleFileBackend::class;
}
Copied!
See also
Caching in TYPO3 for all
available backends and their configuration options.
Extension configuration
The extension provides configuration options via TYPO3's extension
configuration. These are mapped onto typed configuration classes and
are available for autowiring as
\CPSIT\Typo3Handlebars\Configuration\HandlebarsConfiguration.
Enables strict mode for the Handlebars renderer. In strict mode, the
compiled template throws an exception as soon as it encounters a
variable, property, or path that does not exist, instead of silently
rendering an empty string. This is useful during development to catch
typos in variable names and template paths early.
See also
Debug mode for context on why this setting replaces
the previous, debug-mode-triggered strict compilation.
Usage
This section describes how to use the extension to render Handlebars templates
from TYPO3 content elements and page objects.
This page walks through a minimal working example: rendering the header
CType with a Handlebars template.
Include site sets
The extension ships two site sets. Include them in your site's
configuration via the site module or config/sites/<site>/sets.yaml.
cpsit/handlebars (required)
Wires
plugin.tx_handlebars.view paths from the site
settings
handlebars.view.templateRootPath and
handlebars.view.partialRootPath. Required for every site
that renders Handlebars templates.
cpsit/handlebars-content-element (optional)
Sets
lib.contentElement = HANDLEBARSTEMPLATE, replacing
the default Fluid base object. Include this set when all content
elements should use Handlebars rendering by default.
Configure template paths
Declare where your .hbs files are located. The simplest option is
TypoScript:
Each entry in
variables is processed as a TYPO3 content
object against the current content element record. The resulting values
are passed to the template alongside the automatically injected
data and
current variables.
Flush caches
After editing TypoScript, flush the TYPO3 page cache. After editing
Services.yaml, flush and rebuild the service container as well.
HANDLEBARSTEMPLATE is a custom content object type provided by
this extension. It compiles and renders a Handlebars template, resolving
template paths, processing variables, and registering assets — all from
TypoScript configuration.
Name of the template to render. The value is resolved as a filename
(without the .hbs extension) relative to the configured template
root paths. Exactly one of
templateName,
template,
or
file must be set.
Example
templateName = Header
# With stdWrap
templateName.field = tx_myext_template_name
Copied!
template
Type
string / stdWrap
Description
Inline Handlebars source used directly as the template. Useful for short
or dynamically constructed templates. Cannot be used together with
templateName or
file.
Example
template = <h1>{{header}}</h1>
Copied!
file
Type
string / stdWrap
Description
Absolute or
EXT:-relative path to a Handlebars template
file. Cannot be used together with
templateName or
template.
Template root paths for this content object. These are added to the
content object path provider with the highest priority (100), overriding
any TypoScript or service container paths for this rendering. Higher
numeric keys take precedence over lower ones.
The shorthand
partialRootPath (singular) sets a single
path at key 0.
variables
Type
array
Description
Variables passed to the template. Each entry is processed as a content
object against the current content element's data record. Simple
string values are passed through as-is; entries with a sub-array are
rendered via
ContentObjectRenderer::cObjGetSingle().
Two variable names are reserved and always available automatically:
Arbitrary key-value pairs passed to the template as the
settings
variable. Unlike
variables, entries are not processed as
content objects — values are used as plain strings.
Example
settings {
showDate = 1
dateFormat = d.m.Y
}
Copied!
In the template:
{{#if settings.showDate}}<time>{{formatDate date settings.dateFormat}}</time>{{/if}}
Copied!
dataProcessing
Type
array
Description
Standard data processors, executed after
variables are resolved.
Processors receive and return the
$processedData array. Any key added
by a processor is available as a template variable.
The extension provides three additional processors:
process-variables,
resolve-markers, and
unflatten-variable-names.
Example
dataProcessing {
10 = database-query
10 {
table = tx_myext_domain_model_item
as = items
}
20 = process-variables
20 {
as = items
merge = 1
variables {
label = TEXT
label.field = title
}
}
}
Copied!
See also
Data processors for documentation of the extension-specific
processors.
preProcessing
Type
array
Description
Data source aware processors executed before
variables
are processed. These can read from multiple data sources (content element
record, processed data, processor configuration) and modify the variable
set before content object rendering begins.
postProcessing
Type
array
Description
Data source aware processors executed after
variables have
been resolved and data processors have run, but before the template is
rendered.
assets
Type
array
Description
Registers JavaScript and CSS assets via TYPO3's AssetCollector API. Supports
four sub-keys:
javaScript,
inlineJavaScript,
css,
inlineCss.
Asset management for the complete assets configuration reference.
headerAssets
Type
content object
Description
Adds arbitrary markup to the page
<head>. The value is evaluated
as a content object and the result is passed to
PageRenderer::addHeaderData().
Example
headerAssets = TEXT
headerAssets.value = <link rel="stylesheet" href="/assets/styles.css">
Copied!
footerAssets
Type
content object
Description
Adds arbitrary markup before the closing
</body> tag. The value
is evaluated as a content object and the result is passed to
PageRenderer::addFooterData().
Example
footerAssets = TEXT
footerAssets.value = <script src="/assets/app.js"></script>
Copied!
stdWrap
Type
stdWrap
Description
Standard TYPO3 stdWrap processing applied to the final rendered output.
HandlebarsViewFactory integrates Handlebars rendering into the Extbase
MVC stack. It implements
\TYPO3\CMS\Core\View\ViewFactoryInterface and
is wired globally, so every Extbase controller that goes through the standard
view factory mechanism automatically benefits from it without any code changes.
When the factory detects an Extbase request it reads the
handlebars key from the plugin's TypoScript configuration and
returns a
HandlebarsView. If no
handlebars key is present
and the controller does not extend
HandlebarsController, the factory
falls back to the Fluid view.
The controller alias matches the value registered in
ExtensionUtility::configurePlugin(). For the example above,
BlogController would typically have alias
Blog.
Properties
Each resolution key accepts the same properties as a
HANDLEBARSTEMPLATE content object:
Property
Type
Description
templateName
string
Template name or @-prefixed flat name.
Defaults to
<ControllerAlias>/<action>.
format
string
File extension. Defaults to hbs.
templateRootPaths
array
Additional template root paths.
partialRootPaths
array
Additional partial root paths.
variables
array
Extra variables passed to the template.
Default template name
When no
templateName is configured, the factory derives one
automatically from the controller alias and action name:
<ControllerAlias>/<actionName>
Copied!
For
BlogController::listAction() with alias
Blog this
resolves to Blog/list.hbs under the configured template root paths.
HandlebarsController
For controllers you own, extend
\CPSIT\Typo3Handlebars\Controller\HandlebarsController instead of
ActionController. This guarantees Handlebars rendering even when no
handlebars TypoScript key is present — the factory always returns
a
HandlebarsView for these controllers:
During an incremental migration you may need to keep some actions on Fluid
while others are already on Handlebars. Call
delegateRendering() on the
view to hand off to the underlying Fluid view for that action:
Template paths — how template and partial root paths are
collected and prioritised
Data processors
The extension provides three data processors that integrate with the standard
TypoScript
dataProcessing chain inside
HANDLEBARSTEMPLATE
content objects.
Processes a
variables configuration block — exactly like the
top-level
variables of
HANDLEBARSTEMPLATE — within
a data processor chain. This is most useful when combined with other processors
such as
database-query, allowing per-record variable processing.
Data sources
When resolving configuration values, the processor draws from four data
sources, tried in the order listed:
Data source identifier
Contains
contentObjectRenderer
Current record's field values
contentObjectConfiguration
Top-level
HANDLEBARSTEMPLATE config
processedData
Accumulated output from previous processors
processorConfiguration
This processor's own config block
This is why options like
table and
as can be set
by an outer processor and automatically picked up by a nested
process-variables without being repeated explicitly.
The
preProcessing and
postProcessing hooks receive
the same collection, so they have access to all four sources as well.
dataProcessing {
10 = database-query
10 {
table = tx_myext_domain_model_item
as = items
dataProcessing {
10 = process-variables
10 {
table = tx_myext_domain_model_item
as = item
variables {
title = TEXT
title.field = title
body = TEXT
body.field = bodytext
body.parseFunc < lib.parseFunc_RTE
}
}
}
}
}
Copied!
Properties
variables
Variables to process. Same syntax as the top-level
variables in
HANDLEBARSTEMPLATE.
table
Database table of the record to use as the data source for field
lookups. Defaults to the current content element table.
as
Target key in the processed data array. When set, the processed
variables are stored under this key. When omitted, the processed
variables replace (or merge into) the root of the processed data.
merge
Boolean. When
1 and
as is omitted,
the processed variables are merged into the existing processed data
rather than replacing it. When
as is set and the key
already holds an array, the processed variables are merged into that
array. Default:
0.
if
Standard TypoScript
if condition. When the condition
evaluates to false, the processor is skipped and the processed data
is returned unchanged.
preProcessing
Data source aware processors run before
variables are
processed.
postProcessing
Data source aware processors run after
variables are
processed.
Replaces marker-style keys (e.g.,
###NAV_ITEMS###) in the
processed data with the values stored under those keys. The typical pattern is
to use a marker as a named placeholder early in the chain — either as the
as target of a preceding processor or directly in a
variables entry — and then resolve all markers to clean variable
names in a final step. This keeps intermediate processors decoupled from the
variable names the template expects.
tt_content.my_element = HANDLEBARSTEMPLATE
tt_content.my_element {
templateName = MyElement
dataProcessing {
# Declare the expected output slots early using markers — these# names will become the final template variables10 = menu
10 {
as = ###mainNavigation###
levels = 2
}
20 = menu
20 {
as = ###footerLinks###
special = directory
special.value = 42
}
# Resolve all markers to clean variable names in one final step90 = resolve-markers
90.removeNonMatchingMarkers = 1
}
}
Copied!
After processing, the template receives
mainNavigation and
footerLinks as clean variable names. The markers at the top of
the chain serve as upfront documentation of what the template expects, while
the processors that follow fill those slots independently.
Properties
pattern
Regular expression used to identify marker keys. The first capture
group becomes the resolved variable name.
Default:
###(.*?)###
removeNonMatchingMarkers
Boolean. When
1, variable keys that still match the
marker pattern after resolution (i.e., no value was found for them)
are removed from the processed data. Default:
0.
Converts dot-separated flat variable names into nested arrays. This is useful
when other processors set their
as key to a dotted path,
representing the intended position in a nested data structure.
dataProcessing {
10 = menu
10 {
as = page.nav.mainMenu
}
20 = menu
20 {
as = page.nav.footerLinks
special = directory
special.value = 42
}
90 = unflatten-variable-names
}
Copied!
After processing, the template receives a nested
page object:
Named arguments passed from the template (e.g., {{greet name="Alice"}})
are available as
$options->hash['name']. Positional arguments
(e.g., {{greet "Alice"}}) must be declared as additional parameters
in the method signature after
$options:
The
RenderingContext can also be injected by type-hint — declare it
anywhere in the method signature before positional arguments and it is provided
automatically. It gives access to the current PSR-7 request
(
$context->getRequest()) and the full set of template variables
(
$context->getVariables()):
The current template scope is accessible via
$options->scope, and block
helpers can call
$options->fn() and
$options->inverse() to render
their inner blocks.
Implement the
\CPSIT\Typo3Handlebars\Renderer\Helper\Helper interface and
place the
#[AsHelper] attribute on the class. Because the class implements
the
Helper interface, the attribute automatically resolves to the
render
method:
The attribute can also be placed directly on a method, in which case the method
name is inferred automatically — no
method parameter needed. Dependency
injection works normally for all
#[AsHelper]-annotated classes:
There's no need to implement the
Helper interface, it only serves as a
low-barrier tool to easily get started with custom helper implementations. You
can also just do the following:
If you cannot use the attribute (e.g., for a third-party class), register the
helper explicitly in Services.yaml. Both
identifier and
method are required:
Dispatched immediately before a template is compiled. The event provides
read-only access to the rendering context and the renderer. Use it to
inspect the context (e.g., to log which template is about to be rendered)
or to trigger additional rendering via the renderer:
Dispatched after variables have been resolved and merged, immediately before
the compiled template is executed. The full variable set can be read and
modified:
The Handlebars extension integrates with TYPO3's Asset collector
to manage JavaScript and CSS assets in your frontend rendering. Assets are registered
directly through the
assets configuration of a
HANDLEBARSTEMPLATE
content object.
See also
Asset collector in the TYPO3 Core API reference —
covers best practices, CSP/nonce usage, priority, and general troubleshooting.
useNonce (boolean): Add CSP nonce attribute (default: 0).
Deprecated since TYPO3 v14 — use
csp instead.
Example
options {
priority = 1
csp = 1
}
Copied!
Developer corner
This section documents the extension points available to developers who need
to go beyond what TypoScript configuration alone provides. Each page covers
one or more interfaces: when to implement them, the contract they define, and
how to wire the implementation into the service container.
Implement the \CPSIT\Typo3Handlebars\Renderer\Renderer
interface to replace the entire rendering stack — for example to use a
different template engine, add a pre-render transformation, or wrap the
compiled output. The default implementation is
\CPSIT\Typo3Handlebars\Renderer\HandlebarsRenderer.
interfaceRenderer
Fully qualified name
\CPSIT\Typo3Handlebars\Renderer\Renderer
renderTemplate(RenderingContext $context)
Compile and render a template. The
RenderingContext carries the
template path or inline source, the current variable set, and the PSR-7
request.
param RenderingContext $context
The current rendering context.
returntype
string
renderPartial(RenderingContext $context)
Compile and render a partial.
param RenderingContext $context
The current rendering context.
returntype
string
Wiring the implementation
Register the custom renderer as the implementation of the
Renderer interface in your extension's Services.yaml:
Implement the
\CPSIT\Typo3Handlebars\Renderer\Template\TemplateResolver
interface to change how template and partial names are resolved to absolute file
paths — for example to support a different directory layout, an additional file
extension, or a database-driven path lookup.
The extension ships two implementations:
\CPSIT\Typo3Handlebars\Renderer\Template\FlatTemplateResolver (the
default) and
\CPSIT\Typo3Handlebars\Renderer\Template\HandlebarsTemplateResolver.
\CPSIT\Typo3Handlebars\Renderer\Template\BaseTemplateResolver implements
supports() and provides protected helpers for normalizing root paths and
resolving filenames with
EXT: syntax. Extending it keeps
implementations concise.
Resolve a partial name or relative path to its absolute file path.
param string $partialPath
Partial name or relative path.
param string|null $format
Optional file extension override.
returntype
string
FlatTemplateResolver
FlatTemplateResolver is the default implementation. It scans all
configured root paths recursively and builds an in-memory map of every
template file, keyed by its bare filename (without directory). A lookup
therefore succeeds regardless of where in the directory tree the file lives.
Template and partial names must be prefixed with @ to trigger flat
resolution. A name without the prefix is passed directly to
HandlebarsTemplateResolver (see below).
Referencing a flat partial in a Handlebars template
{{> @card}}
Copied!
Variant separator
Appending --<variant> to an @-prefixed name selects a variant
of a component. If no file with that exact name exists, the resolver automatically
falls back to the base name:
{{> @card--highlighted}}{{!-- falls back to @card if not found --}}
When the same filename exists under multiple root paths, the higher-priority
root path wins (see Template paths). Within a single root path, files
are sorted by name and the first occurrence is used, matching Fractal's
uniqueness guarantee.
HandlebarsTemplateResolver
HandlebarsTemplateResolver resolves template and partial names as
paths relative to the configured root paths. Given the name Blog/List,
it searches each root path (highest priority first) for a matching file —
for example Blog/List.hbs.
This resolver is used as the fallback inside
FlatTemplateResolver
for any name that does not start with @, so both resolution strategies
are active at the same time.
Implement the
\CPSIT\Typo3Handlebars\DataProcessing\DataSource\DataSourceAwareProcessor
interface to run custom PHP logic during the
preProcessing or
postProcessing stages of a
process-variables
processor (or of
HANDLEBARSTEMPLATE directly). Unlike standard
TYPO3 data processors, implementations receive a
DataSourceCollection
that gives structured access to all four data sources available at that point
in the pipeline.
DataSourceCollection::resolve() searches the data sources in priority
order and returns the first match. Pass a specific
DataSource case to
restrict the lookup:
useCPSIT\Typo3Handlebars\DataProcessing\DataSource\DataSource;
// Search all sources (highest priority first)
$table = $collection->resolve('table');
// Search only the processor configuration
$table = $collection->resolve('table', DataSource::ProcessorConfiguration);
// Search two specific sources, in the given order
$table = $collection->resolve('table', [
DataSource::ProcessorConfiguration,
DataSource::ContentObjectConfiguration,
]);
Copied!
The four
DataSource cases are:
DataSource::ProcessorConfiguration — this processor's own config block
DataSource::ProcessedData — accumulated output from previous processors
DataSource::ContentObjectRenderer — current record's field values
The numeric keys control execution order when multiple processors are
registered. The class is instantiated via
GeneralUtility::makeInstance(), so constructor injection works
as normal.
PathProvider & VariableProvider
Two further interfaces allow contributing template paths and global variables
from PHP rather than from TypoScript or Services.yaml configuration.
Both are auto-registered via
#[AutoconfigureTag] and both use a
priority integer to control merge order.
Implement the
\CPSIT\Typo3Handlebars\Renderer\Template\Path\PathProvider
interface to contribute template and partial root paths programmatically — for
example when paths depend on the current site configuration or a value not
available at container compile time.
Higher priority values are merged last and therefore take precedence over lower
ones. The three built-in providers use 0 (
GlobalPathProvider), 50
(
TypoScriptPathProvider), and 100 (
ContentObjectPathProvider).
The class is picked up automatically because
PathProvider carries
#[AutoconfigureTag('handlebars.template_path_provider')]. No extra
Services.yaml entry is needed beyond standard autowiring.
VariableProvider
Implement the
\CPSIT\Typo3Handlebars\Renderer\Variables\VariableProvider
interface to inject variables into every template rendering without repeating
them in TypoScript. Common uses include a site-wide locale string, feature
flags, or shared navigation data.
Providers are merged in ascending priority order; a higher-priority provider
can overwrite keys from a lower-priority one. The built-in
GlobalVariableProvider uses priority 0.
This chapter is aimed at developers who are familiar with TYPO3's built-in Fluid
templating engine and want to adopt Handlebars for new or existing projects. It
explains the conceptual differences, maps Fluid constructs to their Handlebars
equivalents, and provides a step-by-step path for migrating an existing project
incrementally without a big-bang rewrite.
Fluid and Handlebars share the same goal — separating presentation from logic —
but take different philosophical stances on how much the template language
itself should do. Understanding this difference up front makes the rest of the
migration straightforward.
Fluid templates can contain complex expressions: inline ViewHelper chaining,
boolean operators, type-coercing comparisons, and dynamic dispatch to arbitrary
PHP classes. This power comes at the cost of templates that are hard to read
outside of a PHP context.
Handlebars is explicitly "logic-less". Templates may only output values, iterate
over arrays, and branch on truthiness. All other logic must live in a named
helper — a PHP callable registered with the renderer. This constraint keeps
templates readable by designers and front-end developers who do not know PHP,
and it shifts complexity to a layer that can be unit-tested cleanly.
Concept-by-concept mapping
The table below maps every major Fluid concept to its Handlebars equivalent.
Detailed examples for each row are given in the linked pages.
Some Fluid features have no direct equivalent and require a different approach:
Inline ViewHelper chains
Fluid allows
{value -> f:format.trim() -> f:format.upper()}. In
Handlebars, compose the same logic in a single helper that applies
both transformations.
Arithmetic and boolean operators in templates
Fluid supports
{a + b} and
{a && b} in some contexts. In
Handlebars, compute the result in a data processor or a helper and expose it
as a plain variable.
Type-aware comparisons
<f:if condition="{count} > 0"> works in Fluid because it parses the
expression. Handlebars {{#if count}} only tests truthiness —
0 and the empty string are falsy, everything else is truthy. Write a
helper if you need a numeric comparison.
Named format strings in the template
Fluid's
<f:format.*> ViewHelpers apply a PHP function to a value.
Replace each one with a small helper whose name describes the transformation
(e.g., {{formatDate date format="d.m.Y"}}).
Note
The extension ships a viewHelper bridge helper that lets you
call any registered Fluid ViewHelper from a Handlebars template without
writing a wrapper. This is intended as a temporary aid during migration;
see Fluid ViewHelper bridge for details.
Template syntax
This page provides side-by-side examples of the most common Fluid constructs
and their Handlebars counterparts. The examples assume a content element with
the variables
header,
bodytext,
items,
and
image.
Handlebars HTML-escapes every {{...}} expression by default.
Fluid:
{header}
Copied!
Handlebars:
{{header}}
Copied!
Raw / unescaped output
Use triple braces to output a value without HTML escaping. Reserve this for
content that has already been sanitized (e.g., a
parseFunc-processed
RTE field).
Fluid:
{bodytext -> f:format.raw()}
Copied!
Handlebars:
{{{bodytext}}}
Copied!
Conditionals
{{#if}} is truthy: empty strings,
0, empty arrays, and
null are all falsy. For numeric comparisons, write a helper (see
Helpers).
Inside {{#each}}, {{this}} refers to the current
item and @index holds the zero-based iteration counter.
@first and @last are boolean flags for the
boundary items.
{{#with}} sets a new scope root, similar to assigning a sub-object
and then using it directly. Inside the block, properties of the given object are
accessible without a prefix.
Handlebars partials are resolved relative to the configured partial root paths
in the same way as templates. The partial name is the filename without the
.hbs extension.
To pass the entire current context to the partial (as Fluid does with the
arguments="{_all}" attribute), just omit any arguments:
{{> Teaser}}
Copied!
To pass a completely different context object, provide it as a positional
argument before any hash arguments:
{{> Card item}}
Copied!
Dynamic property access
Handlebars dot-path notation resolves nested public properties:
{{user.address.city}}. For getter resolution (using Extbase's
ObjectAccess) and dynamic key lookups (where the key itself is a variable),
use the built-in
get helper:
Handlebars comments are stripped from the rendered output and never appear in
the HTML source. Use them for template-internal notes.
Fluid:
<!-- this comment appears in HTML source -->
Copied!
Handlebars:
{{!-- this comment is stripped from the output --}}
Copied!
Escaping Handlebars delimiters
To output a literal {{ in the rendered HTML, use the raw
block syntax:
{{{raw}}}}
This {{{will not be}}} parsed as Handlebars.
{{{{/raw}}}}
Copied!
Layouts and partials
Fluid's layout system —
<f:layout>,
<f:section>, and
<f:render section="..."> — is one of the first things developers look for
when switching template engines. EXT:handlebars ships an equivalent mechanism
implemented as the extend, block, and content helpers, modelled
after the handlebars-layouts
convention.
Fluid uses a push model: a child template declares which layout it inherits
and then pushes named sections into the layout's named slots. Handlebars uses
the same model, but the building blocks are ordinary helpers rather than
dedicated language constructs.
Fluid
Handlebars
<f:layout name="Default" />
{{#extend "default"}} … {{/extend}}
<f:render section="Main"> (in layout)
{{#block "main"}} … {{/block}}
<f:section name="Main"> (in template)
{{#content "main"}} … {{/content}}
The content helper supports an optional mode hash argument
(replace / append / prepend) that controls
how the child's content is merged with the layout block's default. replace
is the default and matches Fluid's behaviour.
Handlebars layouts are resolved as partials, so the layout file must be
placed in one of the configured partial root paths, not in the template root
paths. By convention, default.hbs lives under
Resources/Private/Partials/.
Default content in blocks
A {{#block}} can hold default markup that is used verbatim when
the child provides no matching {{#content}} for that slot. This is
the equivalent of Fluid's
optional="true" on
<f:render section="...">
combined with a fallback in the layout.
A child template that does not declare a {{#content "footer"}} block
will render the default copyright line automatically.
Appending and prepending content
The mode argument lets a child add to a block rather than replace
it. This has no direct Fluid equivalent and is often used for accumulating
<script> or
<link> tags:
Not every template needs a full layout. Reusable snippets that were Fluid
partials map directly to Handlebars partials with no extra ceremony — just
create a .hbs file in the partial root path and include it with
{{> Name}}.
Fluid ViewHelpers and Handlebars helpers serve the same role: they bring PHP
logic into templates. The implementation model is different enough to warrant a
dedicated page.
A Fluid ViewHelper is a PHP class that implements
\TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper . Arguments are
declared via
initializeArguments() and the class is resolved by its
namespace prefix (e.g.,
f:,
myext:).
A Handlebars helper is any PHP callable registered via the :php: #[AsHelper]
attribute. Arguments reach the callable either as named hash arguments
(name=value pairs after the helper name) or as positional arguments
(bare values in order). There is no argument declaration step — the method
signature is the contract.
Fluid ViewHelper
Handlebars helper
Class extending
AbstractViewHelper
Any class / method / callable
Namespace prefix in template
Plain identifier string
initializeArguments()
Method parameters
renderChildren()
$options->fn($options->scope)
Registered via namespace import
#[AsHelper('name')] attribute
Porting an inline ViewHelper
Inline ViewHelpers that transform a single value (like
<f:format.date>)
are the most common case. The Handlebars equivalent is a helper that receives the
value as a positional argument and returns the formatted string.
ViewHelpers that wrap inner content (block ViewHelpers) correspond to
Handlebars block helpers. The inner content is rendered via
$options->fn($options->scope) and the inverse ({{else}})
branch via
$options->inverse($options->scope).
The table below lists frequently used Fluid ViewHelpers and how to handle them
in Handlebars templates.
Fluid ViewHelper
Handlebars approach
f:if
Built-in {{#if}} / {{#unless}}
f:for
Built-in {{#each}}
f:alias
Built-in {{#with}}
f:format.raw
Triple-stash {{{variable}}}
f:format.htmlspecialchars
Default {{variable}} (always escapes)
f:format.date
Custom formatDate helper
f:format.number
Custom formatNumber helper
f:translate
Custom translate helper (use TYPO3 API inside)
f:uri.page,
f:link.*
Custom URI helper (use
UriBuilder inside)
f:image
Custom image helper (use TYPO3 image API inside)
f:render partial="…"
{{> PartialName}} or {{render "PartialName"}}
f:debug
Built-in {{debug}} helper
Fluid ViewHelper bridge
As a temporary migration aid, the extension ships a viewHelper
helper that invokes any registered Fluid ViewHelper directly from a Handlebars
template. This lets you use existing ViewHelpers without writing a wrapper immediately.
The viewHelper helper is intended as a short-term escape
hatch during migration, not as a permanent pattern. It carries the overhead
of bootstrapping a Fluid rendering context for every invocation and relies on
internal Fluid APIs that may change. Replace it with a proper Handlebars
helper once the migration for the affected template is complete.
See also
Custom helpers — full reference for implementing and registering
Handlebars helpers.
Gradual migration
Replacing every Fluid template in one step is rarely practical. This page
describes an incremental approach that lets Fluid and Handlebars coexist in
the same TYPO3 installation — even within the same extension — so you can
migrate one content element or controller at a time.
Strategy overview
The recommended path has three lanes that can be worked independently:
Content elements rendered via
tt_content.* TypoScript: Replace
the content object type from
FLUIDTEMPLATE to
HANDLEBARSTEMPLATE one CType at a time.
Extbase controllers: Extend
HandlebarsController, then migrate templates
action by action with a Fluid fallback in place.
Shared partials / layout: Migrate the layout shell last, once all
templates that depend on it have been converted.
Install and configure paths
Install the extension and include the base site set without the
content-element set. The content-element set replaces
lib.contentElement globally; omitting it keeps all existing content
elements on Fluid.
config/sites/<site>/config.yaml
dependencies:-cpsit/handlebars# base set only — does NOT touch lib.contentElement
Copied!
Configure template and partial root paths for Handlebars independently of
the Fluid paths:
All other
tt_content.* definitions that are not yet migrated
continue to use Fluid without any changes.
Migrate Extbase controllers
For controller-based rendering, the extension provides
HandlebarsViewFactory, which replaces TYPO3's default
FluidViewFactory. It inspects the TypoScript configuration and returns
a
HandlebarsView when Handlebars configuration is present, or falls
back to the Fluid view when it is not.
Note
The view factory is globally injected to all Extbase controllers extending
ActionController, so there's no need to switch the view factory injection.
Extend HandlebarsController:
For controllers you own, extend
\CPSIT\Typo3Handlebars\Controller\HandlebarsController instead of
ActionController. The
renderView() method renders the current
action via Handlebars:
The view factory resolves the template name automatically from the
controller alias and action name (e.g., Blog/list.hbs for
listAction). Configure paths and per-action overrides via the
handlebars key in the plugin's TypoScript namespace:
Extbase plugins for the full configuration reference, including
per-controller and per-action overrides.
Using the Fluid fallback during migration:
When a controller action has not yet been ported, call
delegateRendering() on the view to delegate to the underlying Fluid
view. This keeps the action working while the template is being migrated:
publicfunctionlegacyAction(): ResponseInterface{
$this->view->assign('items', $this->repository->findAll());
// Render with Fluid until the .hbs template is ready
$content = $this->view instanceof HandlebarsView
? $this->view->delegateRendering()
: $this->view->render();
return$this->htmlResponse((string)$content);
}
Copied!
Migrate the page layout shell
The layout shell (the outer HTML document, header, navigation, footer) is
typically the last thing to migrate because it is shared by every page.
Keep the existing Fluid layout in place until all content elements and
actions that reference it have been converted to Handlebars partials.
Once all consumers are migrated, convert the Fluid layout to a Handlebars
layout partial using the extend / block /
content pattern described in Layouts and partials.
Switch the lib.contentElement default
After every content element has been migrated to
HANDLEBARSTEMPLATE,
include the
cpsit/handlebars-content-element site set. This sets
lib.contentElement = HANDLEBARSTEMPLATE globally so that
newly created content elements start from Handlebars automatically:
This page lists required migration steps when upgrading to a new major version
of the extension.
Version 1.0.0
Version 1.0.0 replaces the previous PHP-class rendering model (DataProcessor /
DataProvider / Presenter) with the
HANDLEBARSTEMPLATE content
object. All rendering configuration moves to TypoScript; custom PHP classes are
no longer the entry point.
Removed classes and interfaces
The following classes and interfaces have been removed and have no replacement:
The Services.yaml tag approach still works and can be used if you
cannot modify the helper class (e.g., a third-party class).
Debug mode
The debug mode built into
HandlebarsRenderer has been removed. Previously,
when TYPO3's
config.debug flag or
$GLOBALS['TYPO3_CONF_VARS']['FE']['debug'] was enabled, the renderer
automatically bypassed the template cache and switched the Handlebars compiler
to strict mode (throwing on missing variables instead of silently returning
empty strings).
There is no automatic replacement tied to a debug flag. Use the following
alternatives instead:
Cache bypass: Disable caching explicitly via
TYPO3's caching framework configuration or by
setting
config.no_cache = 1 during development.
Strict compilation: Enable
rendering.strictMode
in the extension configuration. Unlike the previous behavior, this is a
persistent setting rather than one tied to TYPO3's debug flags.
Strict template validation: Use the shipped {{debug}}
helper inside templates to inspect variable values at render time. For
programmatic checks, call
Handlebars::precompile() directly to
inspect the generated PHP code.
Template path configuration
Template path configuration via Services.yaml and TypoScript remains
unchanged. In addition, paths can now also be set per-content-object directly
in
HANDLEBARSTEMPLATE:
Thanks for considering contributing to this extension! Since it is
an open source product, its successful further development depends
largely on improving and optimizing it together.
The development of this extension follows the official
TYPO3 coding standards.
To ensure the stability and cleanliness of the code, various code
quality tools are used and most components are covered with test
cases. In addition, we use DDEV
for local development. Make sure to set it up as described below. For
continuous integration, we use GitHub Actions.
Use the following commands to normalize and format the code base:
# Apply all automatic fixes
composer fix
# Apply specific fixes
composer fix:composer
composer fix:editorconfig
composer fix:php
Copied!
Run checks
Use
composer check to run the full code quality pipeline locally.
This command bundles dependency analysis, static analysis, coding style checks,
and Rector in dry-run mode so that potential refactorings can be reviewed
without changing files.
# Run all checks
composer check
# Run specific checks
composer check:deps
composer check:refactor
composer check:static
composer check:style
# Run specific style checks
composer check:style:composer
composer check:style:editorconfig
composer check:style:php
composer check:style:typoscript
Copied!
Run refactorings
Refactorings are intentionally separated from regular checks because they may
change the code base.
# Run all configured refactorings
composer refactor
# Run specific refactorings
composer refactor:php
Copied!
Run tests
Run the full test suite before opening a pull request:
# Run all tests
ddev composer test
ddev composer test:coverage
# Run functional tests
ddev composer test:functional
ddev composer test:functional:coverage
# Run unit tests
ddev composer test:unit
ddev composer test:unit:coverage
# Merge coverage reports
ddev composer test:merge-coverage
Copied!
Coverage reports
Code coverage reports are written to Build/tests/coverage. Open the latest merge
HTML report with:
open Build/tests/coverage/html/_merged/index.html
Copied!
Pull requests
Once the changes are ready, please
submit a pull request
and describe what was changed and why. Ideally, the pull request references an
issue that describes the problem being solved.
All documented code quality tools are executed automatically for pull requests
across the currently supported PHP versions. For details, refer to the GitHub
Actions workflows.
Sitemap
Reference to the headline
Copy and freely share the link
This link target has no permanent anchor assigned.The link below can be used, but is prone to change if the page gets moved.