Handlebars 

Extension key

handlebars

Package name

cpsit/typo3-handlebars

Version

main

Language

en

Author

coding. powerful. systems. CPS GmbH

License

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.

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 

  • Full rendering environment for Handlebars templates
  • Native support for custom Handlebars helpers
  • Easy to extend and customize
  • Built on dependency injection for better performance and maintainability
  • Integration with TYPO3's cache framework for compiled templates
  • Compatible with TYPO3 13.4 LTS and 14.3 LTS

Support 

There are several ways to get support for this extension:

License 

This extension is licensed under GNU General Public License 2.0 (or later).

Installation 

Requirements 

  • PHP 8.2 - 8.5
  • TYPO3 13.4 LTS - 14.3 LTS

Installation 

Require the extension via Composer (recommended):

composer require cpsit/typo3-handlebars
Copied!

Or download it from the TYPO3 extension repository.

Site sets 

The extension provides two site sets that can be included in the site configuration or any other site set.

  • cpsit/handlebarsHandlebars 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-elementHandlebars 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.
config/sites/<my-site>/config.yaml
dependencies:
  - cpsit/handlebars
  - cpsit/handlebars-content-element
Copied!

Template paths 

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.

Priority order 

Source Priority Cacheable
Per-content-object TypoScript 100 No
plugin.tx_handlebars.view 50 Yes
Service container (e.g. Services.yaml) 0 Yes

Per-content-object (priority 100) 

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.

tt_content.textmedia = HANDLEBARSTEMPLATE
tt_content.textmedia {
    templateRootPaths {
        10 = EXT:my_extension/Resources/Private/Templates
    }
    partialRootPaths {
        10 = EXT:my_extension/Resources/Private/Partials
    }
}
Copied!

TypoScript (priority 50) 

Global paths for all renderings on the current page can be configured under plugin.tx_handlebars.view:

plugin.tx_handlebars {
    view {
        templateRootPaths {
            10 = EXT:my_extension/Resources/Private/Templates
        }
        partialRootPaths {
            10 = EXT:my_extension/Resources/Private/Partials
        }
    }
}
Copied!

The cpsit/handlebars site set also populates these paths from the site settings {$handlebars.view.templateRootPath} and {$handlebars.view.partialRootPath}.

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.

Configuration/Services.yaml
handlebars:
  view:
    templateRootPaths:
      10: EXT:my_extension/Resources/Private/Templates
    partialRootPaths:
      10: EXT:my_extension/Resources/Private/Partials
Copied!

The HandlebarsExtension DI extension merges all paths declared this way into the container parameters %handlebars.templateRootPaths% and %handlebars.partialRootPaths%.

Variables 

Template variables are available at two scopes: globally for every rendering, and locally for a single content object rendering.

Global variables 

Global variables are merged into every template rendering automatically. They can be defined through TypoScript or the service container.

Via TypoScript 

Use plugin.tx_handlebars.variables to define variables available on every page where the TypoScript is active:

plugin.tx_handlebars {
    variables {
        publicPath = /assets
        siteName = My Site
    }
}
Copied!

Via service container 

Variables can also be defined instance-wide through Services.yaml. These apply regardless of TypoScript configuration:

Configuration/Services.yaml
handlebars:
  variables:
    publicPath: /assets
    apiEndpoint: https://api.example.com
Copied!

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).

Cache 

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!

Debugging 

When TYPO3 frontend debug mode is active, the Handlebars renderer emits additional debug output for individual template tags. This makes it easier to localize rendering errors during development.

Via TypoScript 

# Enable debug mode
config.debug = 1

# Disable debug mode
config.debug = 0
Copied!

Via system configuration 

config/system/additional.php
// Enable debug mode
$GLOBALS['TYPO3_CONF_VARS']['FE']['debug'] = true;

// Disable debug mode
$GLOBALS['TYPO3_CONF_VARS']['FE']['debug'] = false;
Copied!

Quick start 

This page walks through a minimal working example: rendering the header CType with a Handlebars template.

  1. 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.
  2. Configure template paths

    Declare where your .hbs files are located. The simplest option is TypoScript:

    plugin.tx_handlebars {
        view {
            templateRootPaths {
                10 = EXT:my_sitepackage/Resources/Private/Templates/Handlebars
            }
            partialRootPaths {
                10 = EXT:my_sitepackage/Resources/Private/Partials/Handlebars
            }
        }
    }
    Copied!
  3. Create a Handlebars template

    Create the template file at the path declared above. The filename without the .hbs extension is used as the templateName:

    EXT:my_sitepackage/Resources/Private/Templates/Handlebars/Header.hbs
    <div class="ce-header">
        {{#if header}}
            <h1 class="ce-header__title">{{header}}</h1>
        {{/if}}
        {{#if subheader}}
            <p class="ce-header__subtitle">{{subheader}}</p>
        {{/if}}
    </div>
    Copied!
  4. Configure the content element

    Point the header CType at the template using a HANDLEBARSTEMPLATE content object:

    tt_content.header = HANDLEBARSTEMPLATE
    tt_content.header {
        templateName = Header
    
        variables {
            header = TEXT
            header.field = header
    
            subheader = TEXT
            subheader.field = subheader
        }
    }
    Copied!

    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.

  5. Flush caches

    After editing TypoScript, flush the TYPO3 page cache. After editing Services.yaml, flush and rebuild the service container as well.

Next steps 

HANDLEBARSTEMPLATE content object 

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.

tt_content.header = HANDLEBARSTEMPLATE
tt_content.header {
    templateName = Header
    templateRootPaths.20 = EXT:my_extension/Resources/Private/Templates
}
Copied!

templateName 

Type
string / stdWrap
Description
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.
Example
file = EXT:my_extension/Resources/Private/Templates/Special.hbs
Copied!

templateRootPaths 

Type
array (numeric keys)
Description
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.
Example
templateRootPaths {
    10 = EXT:my_extension/Resources/Private/Templates
    20 = EXT:my_other_extension/Resources/Private/Templates
}
Copied!

partialRootPaths 

Type
array (numeric keys)
Description
Partial root paths for this content object. Same priority and override rules as templateRootPaths.
Example
partialRootPaths {
    10 = EXT:my_extension/Resources/Private/Partials
}
Copied!

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:

  • data — the full content element data array
  • current — the current field value
Example
variables {
    header = TEXT
    header.field = header

    bodytext = TEXT
    bodytext.field = bodytext
    bodytext.parseFunc < lib.parseFunc_RTE

    image = FILES
    image.references.fieldName = image
}
Copied!

settings 

Type
array
Description
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!

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.
Example
assets {
    javaScript {
        my-ext-app {
            source = EXT:my_extension/Resources/Public/JavaScript/app.js
            attributes.defer = 1
            options.useNonce = 1
        }
    }
    css {
        my-ext-styles {
            source = EXT:my_extension/Resources/Public/Css/styles.css
        }
    }
}
Copied!

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.
Example
stdWrap.wrap = <div class="handlebars-content">|</div>
Copied!

Data processors 

The extension provides three data processors that integrate with the standard TypoScript dataProcessing chain inside HANDLEBARSTEMPLATE content objects.


process-variables 

Class: \CPSIT\Typo3Handlebars\DataProcessing\ProcessVariablesProcessor

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.

Standalone usage 

tt_content.my_element = HANDLEBARSTEMPLATE
tt_content.my_element {
    templateName = MyElement

    dataProcessing {
        10 = process-variables
        10 {
            variables {
                header = TEXT
                header.field = header

                teaser = TEXT
                teaser.field = bodytext
                teaser.parseFunc < lib.parseFunc_RTE
            }
        }
    }
}
Copied!

Nested inside another processor 

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.

resolve-markers 

Class: \CPSIT\Typo3Handlebars\DataProcessing\ResolveMarkersProcessor

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 variables
        10 = 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 step
        90 = 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.

unflatten-variable-names 

Class: \CPSIT\Typo3Handlebars\DataProcessing\UnflattenVariableNamesProcessor

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:

{{#each page.nav.mainMenu}}
    <a href="{{link}}">{{title}}</a>
{{/each}}
Copied!

The processor has no configuration properties and operates on the entire processed data array.

Custom helpers 

Handlebars helpers bring custom PHP logic into templates. The extension's Helper interface defines a single method:

public function render(\DevTheorem\Handlebars\HelperOptions $options): mixed;
Copied!

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:

public function render(HelperOptions $options, ?string $name = null): mixed;
Copied!

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()):

public function render(HelperOptions $options, ?RenderingContext $context = null): mixed;
Copied!

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 a helper 

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:

EXT:my_extension/Classes/Renderer/Helper/GreetHelper.php
namespace Vendor\Extension\Renderer\Helper;

use CPSIT\Typo3Handlebars\Attribute\AsHelper;
use CPSIT\Typo3Handlebars\Renderer\Helper\Helper;
use DevTheorem\Handlebars\HelperOptions;

#[AsHelper('greet')]
final readonly class GreetHelper implements Helper
{
    public function render(HelperOptions $options): mixed
    {
        return sprintf('Hello, %s!', $options->hash['name'] ?? 'World');
    }
}
Copied!

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:

EXT:my_extension/Classes/Renderer/Helper/GreetHelper.php
use CPSIT\Typo3Handlebars\Attribute\AsHelper;
use CPSIT\Typo3Handlebars\Renderer\Helper\Helper;
use DevTheorem\Handlebars\HelperOptions;
use Vendor\Extension\Domain\Model\Person;
use Vendor\Extension\Domain\Repository\PersonRepository;

#[AsHelper('greet')]
final readonly class GreetHelper implements Helper
{
    public function __construct(
        private PersonRepository $repository,
    ) {}

    public function render(HelperOptions $options): mixed
    {
        return sprintf('Hello, %s!', $options->hash['name'] ?? 'World');
    }

    #[AsHelper('greetAll')]
    public function greetAll(HelperOptions $options): mixed
    {
        return implode(PHP_EOL, array_map(
            static fn(Person $person) => sprintf('Hello, %s!', $person->getName()),
            $this->repository->findAll(),
        ));
    }
}
Copied!

Use the helper in a template 

Reference the helper by its identifier:

{{greet name="Alice"}}

{{greetAll}}
Copied!

Alternative: Register via Services.yaml 

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:

Configuration/Services.yaml
services:
  Vendor\Extension\Renderer\Helper\GreetHelper:
    tags:
      - name: handlebars.helper
        identifier: 'greetAll'
        method: 'greetAll'
Copied!

Events 

The extension dispatches three PSR-14 events during the rendering pipeline.

BeforeTemplateCompilationEvent 

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:

EXT:my_extension/Classes/EventListener/BeforeTemplateCompilationListener.php
namespace Vendor\Extension\EventListener;

use CPSIT\Typo3Handlebars\Event\BeforeTemplateCompilationEvent;

final readonly class BeforeTemplateCompilationListener
{
    public function __invoke(BeforeTemplateCompilationEvent $event): void
    {
        $context = $event->getContext();   // RenderingContext
        $renderer = $event->getRenderer(); // Renderer
    }
}
Copied!

BeforeRenderingEvent 

Dispatched after variables have been resolved and merged, immediately before the compiled template is executed. The full variable set can be read and modified:

EXT:my_extension/Classes/EventListener/BeforeRenderingListener.php
namespace Vendor\Extension\EventListener;

use CPSIT\Typo3Handlebars\Event\BeforeRenderingEvent;

final readonly class BeforeRenderingListener
{
    public function __invoke(BeforeRenderingEvent $event): void
    {
        // Add a variable
        $event->addVariable('timestamp', time());

        // Replace the entire variable set
        $variables = $event->getVariables();
        $variables['title'] = strtoupper($variables['title'] ?? '');
        $event->setVariables($variables);

        // Remove a variable
        $event->removeVariable('internalFlag');
    }
}
Copied!

Available methods:

  • getVariables() / setVariables(array $variables) — get or replace the full variable set
  • addVariable(string $name, mixed $value) — add or overwrite a single variable
  • removeVariable(string $name) — remove a variable by name
  • getContext() — the RenderingContext (read-only)
  • getRenderer() — the Renderer (read-only)

AfterRenderingEvent 

Dispatched after the template has been fully rendered. The rendered HTML string can be read and replaced:

EXT:my_extension/Classes/EventListener/AfterRenderingListener.php
namespace Vendor\Extension\EventListener;

use CPSIT\Typo3Handlebars\Event\AfterRenderingEvent;

final readonly class AfterRenderingListener
{
    public function __invoke(AfterRenderingEvent $event): void
    {
        $content = $event->getContent();
        $event->setContent(trim($content));
    }
}
Copied!

Available methods:

  • getContent() / setContent(string $content) — get or replace the rendered output
  • getContext() — the RenderingContext (read-only)
  • getRenderer() — the Renderer (read-only)

Asset management 

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.

Asset Types 

The AssetCollector API supports four distinct asset types, all fully supported by this extension:

  1. External JavaScript files — link to external .js files
  2. Inline JavaScript code — embed JavaScript directly in the page
  3. External CSS files — link to external .css files
  4. Inline CSS code — embed styles directly in the page

JavaScript files 

Register external JavaScript files using the javaScript configuration:

10 = HANDLEBARSTEMPLATE
10 {
    templateName = MyTemplate

    assets {
        javaScript {
            my-app-script {
                source = EXT:myext/Resources/Public/JavaScript/app.js
                attributes {
                    async = 1
                    defer = 1
                    crossorigin = anonymous
                }
                options {
                    priority = 1
                    csp = 1
                }
            }
        }
    }
}
Copied!

Inline JavaScript 

Add inline JavaScript code using inlineJavaScript:

assets {
    inlineJavaScript {
        my-inline-script {
            source = console.log('Hello from Handlebars'); initMyApp();
            attributes {
                type = module
            }
            options {
                priority = 1
            }
        }
    }
}
Copied!

CSS files 

Register external stylesheets using the css configuration:

assets {
    css {
        my-styles {
            source = EXT:myext/Resources/Public/Css/styles.css
            attributes {
                media = screen and (max-width: 768px)
            }
            options {
                priority = 1
            }
        }
    }
}
Copied!

Inline CSS 

Add inline styles using inlineCss:

assets {
    inlineCss {
        critical-css {
            source = body { margin: 0; padding: 0; } .container { max-width: 1200px; }
        }
    }
}
Copied!

Configuration reference 

source (required) 

Type
string
Description

Asset source. For external files, use EXT: syntax or absolute paths. For inline assets, provide the code directly as a string value.

Example
# External JavaScript file
source = EXT:myext/Resources/Public/JavaScript/file.js

# External CSS file
source = EXT:myext/Resources/Public/Css/styles.css

# Inline JavaScript code
source = console.log('Hello');

# Inline CSS code
source = body { margin: 0; }
Copied!

attributes 

Type
array
Description
HTML attributes for the generated tag. Boolean attributes ( async, defer, disabled) should be set to 1 to enable them.
JavaScript attributes
  • async (boolean): Load script asynchronously
  • defer (boolean): Defer script execution
  • nomodule (boolean): Fallback for older browsers
  • type (string): Script type (e.g., "module")
  • crossorigin (string): CORS setting (e.g., "anonymous")
  • integrity (string): Subresource Integrity hash
CSS attributes
  • media (string): Media query (e.g., "screen", "print")
  • disabled (boolean): Disable stylesheet
  • title (string): Stylesheet title
  • crossorigin (string): CORS setting
  • integrity (string): Subresource Integrity hash
Example
attributes {
    async = 1
    defer = 1
    type = module
    crossorigin = anonymous
    integrity = sha384-abc123def456
}
Copied!

options 

Type
array
Description
AssetCollector-specific options that control asset rendering behaviour.
Available options
  • priority (boolean): Render before other assets (default: 0)
  • csp (boolean): Add CSP nonce attribute (default: 0). Requires TYPO3 v14+.
  • useNonce (boolean): Add CSP nonce attribute (default: 0). Deprecated since TYPO3 v14 — use csp instead.
Example
options {
    priority = 1
    csp = 1
}
Copied!

Renderer 

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.

interface Renderer
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:

Configuration/Services.yaml
services:
  CPSIT\Typo3Handlebars\Renderer\Renderer:
    alias: Vendor\Extension\Renderer\MyRenderer
Copied!

TemplateResolver 

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 default implementation is \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.

interface TemplateResolver
Fully qualified name
\CPSIT\Typo3Handlebars\Renderer\Template\TemplateResolver
supports ( string $fileExtension)

Return true if this resolver handles the given file extension.

param string $fileExtension

File extension without leading dot.

returntype

bool

resolveTemplatePath ( string $templatePath, ?string $format = null)

Resolve a template name or relative path to its absolute file path.

param string $templatePath

Template name or relative path.

param string|null $format

Optional file extension override.

returntype

string

resolvePartialPath ( string $partialPath, ?string $format = null)

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

Example implementation 

EXT:my_extension/Classes/Renderer/Template/MyTemplateResolver.php
namespace Vendor\Extension\Renderer\Template;

use CPSIT\Typo3Handlebars\Exception;
use CPSIT\Typo3Handlebars\Renderer\Template\BaseTemplateResolver;
use CPSIT\Typo3Handlebars\Renderer\Template\TemplatePaths;

final readonly class MyTemplateResolver extends BaseTemplateResolver
{
    public function __construct(
        private TemplatePaths $templatePaths,
    ) {}

    public function resolveTemplatePath(string $templatePath, ?string $format = null): string
    {
        [$templateRootPaths] = $this->resolveTemplatePaths($this->templatePaths);

        foreach (array_reverse($templateRootPaths) as $rootPath) {
            $filename = $this->resolveFilename($templatePath, $rootPath, $format ?? 'hbs');

            if (is_file($filename)) {
                return $filename;
            }
        }

        throw new Exception\TemplatePathIsNotResolvable($templatePath);
    }

    public function resolvePartialPath(string $partialPath, ?string $format = null): string
    {
        [, $partialRootPaths] = $this->resolveTemplatePaths($this->templatePaths);

        foreach (array_reverse($partialRootPaths) as $rootPath) {
            $filename = $this->resolveFilename($partialPath, $rootPath, $format ?? 'hbs');

            if (is_file($filename)) {
                return $filename;
            }
        }

        throw new Exception\PartialPathIsNotResolvable($partialPath);
    }
}
Copied!

Wiring the implementation 

Register the custom resolver as the implementation of the TemplateResolver interface in your extension's Services.yaml:

Configuration/Services.yaml
services:
  CPSIT\Typo3Handlebars\Renderer\Template\TemplateResolver:
    alias: Vendor\Extension\Renderer\Template\MyTemplateResolver
Copied!

DataSourceAwareProcessor 

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.

interface DataSourceAwareProcessor
Fully qualified name
\CPSIT\Typo3Handlebars\DataProcessing\DataSource\DataSourceAwareProcessor
process ( array $variables, DataSourceCollection $collection, ContentObjectRenderer $contentObjectRenderer)

Process the current variable set and return the (modified) array.

param array $variables

Current template variable set.

param DataSourceCollection $collection

All four data sources for the current rendering.

param ContentObjectRenderer $contentObjectRenderer

Current content element renderer.

returntype

array

Reading from DataSourceCollection 

DataSourceCollection::resolve() searches the data sources in priority order and returns the first match. Pass a specific DataSource case to restrict the lookup:

use CPSIT\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
  • DataSource::ContentObjectConfiguration — top-level HANDLEBARSTEMPLATE config

Example implementation 

EXT:my_extension/Classes/DataProcessing/MyPreProcessor.php
namespace Vendor\Extension\DataProcessing;

use CPSIT\Typo3Handlebars\DataProcessing\DataSource\DataSource;
use CPSIT\Typo3Handlebars\DataProcessing\DataSource\DataSourceAwareProcessor;
use CPSIT\Typo3Handlebars\DataProcessing\DataSource\DataSourceCollection;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;

final readonly class MyPreProcessor implements DataSourceAwareProcessor
{
    public function process(
        array $variables,
        DataSourceCollection $collection,
        ContentObjectRenderer $contentObjectRenderer,
    ): array {
        $table = $collection->resolve('table', DataSource::ProcessorConfiguration, 'tt_content');
        $variables['tableName'] = $table;

        return $variables;
    }
}
Copied!

Registering the processor 

Reference the processor class by its fully qualified class name in preProcessing or postProcessing:

10 = process-variables
10 {
    variables {
        header = TEXT
        header.field = header
    }

    preProcessing {
        10 = Vendor\Extension\DataProcessing\MyPreProcessor
    }
}
Copied!

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.

PathProvider 

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).

interface PathProvider
Fully qualified name
\CPSIT\Typo3Handlebars\Renderer\Template\Path\PathProvider
getTemplateRootPaths ( )

Return an array of template root paths, keyed by integer priority.

returntype

array

getPartialRootPaths ( )

Return an array of partial root paths, keyed by integer priority.

returntype

array

isCacheable ( )

Return true if the paths provided by this provider may be cached. Return false for request-dependent paths.

returntype

bool

getPriority ( )

Return the priority of this provider. Higher values win.

returntype

int

EXT:my_extension/Classes/Renderer/Template/Path/SitePathProvider.php
namespace Vendor\Extension\Renderer\Template\Path;

use CPSIT\Typo3Handlebars\Renderer\Template\Path\PathProvider;
use TYPO3\CMS\Core\Site\SiteFinder;

final readonly class SitePathProvider implements PathProvider
{
    public function __construct(
        private SiteFinder $siteFinder,
    ) {}

    public function getTemplateRootPaths(): array
    {
        return [];
    }

    public function getPartialRootPaths(): array
    {
        return [];
    }

    public function isCacheable(): bool
    {
        return false;
    }

    public static function getPriority(): int
    {
        return 25;
    }
}
Copied!

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.

interface VariableProvider
Fully qualified name
\CPSIT\Typo3Handlebars\Renderer\Variables\VariableProvider
get ( )

Return the variables contributed by this provider.

returntype

array

getPriority ( )

Return the priority of this provider. Higher values win.

returntype

int

EXT:my_extension/Classes/Renderer/Variables/SiteVariableProvider.php
namespace Vendor\Extension\Renderer\Variables;

use CPSIT\Typo3Handlebars\Renderer\Variables\VariableProvider;
use TYPO3\CMS\Core\Site\SiteFinder;

final readonly class SiteVariableProvider implements VariableProvider
{
    public function __construct(
        private SiteFinder $siteFinder,
    ) {}

    public function get(): array
    {
        return [
            'siteName' => $this->siteFinder->getSiteByPageId(0)->getIdentifier(),
        ];
    }

    public function offsetExists(mixed $offset): bool
    {
        return array_key_exists($offset, $this->get());
    }

    public function offsetGet(mixed $offset): mixed
    {
        return $this->get()[$offset] ?? null;
    }

    public function offsetSet(mixed $offset, mixed $value): never
    {
        throw new \LogicException('Variables are read-only.', 1781693633);
    }

    public function offsetUnset(mixed $offset): never
    {
        throw new \LogicException('Variables are read-only.', 1781693639);
    }

    public static function getPriority(): int
    {
        return 10;
    }
}
Copied!

Like PathProvider, the class is picked up automatically via #[AutoconfigureTag('handlebars.variable_provider')] on the interface.

Migration 

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:

  • \CPSIT\Typo3Handlebars\DataProcessing\DataProcessor (interface)
  • \CPSIT\Typo3Handlebars\DataProcessing\AbstractDataProcessor
  • \CPSIT\Typo3Handlebars\Data\DataProvider (interface)
  • \CPSIT\Typo3Handlebars\Data\Response\ProviderResponse (interface)
  • \CPSIT\Typo3Handlebars\Presenter\Presenter (interface)
  • \CPSIT\Typo3Handlebars\Presenter\AbstractPresenter
  • \CPSIT\Typo3Handlebars\DataProcessing\SimpleProcessor

The handlebars.processor service tag has also been removed.

TypoScript entry point 

Before: Each content element was routed to a DataProcessor class via a USER content object:

tt_content.header = USER
tt_content.header.userFunc = Vendor\Extension\DataProcessing\HeaderProcessor->process
Copied!

After: Use HANDLEBARSTEMPLATE directly:

tt_content.header = HANDLEBARSTEMPLATE
tt_content.header {
    templateName = Header

    variables {
        header = TEXT
        header.field = header

        subheader = TEXT
        subheader.field = subheader
    }
}
Copied!

Data preparation (DataProvider → variables / dataProcessing) 

Before: Data was prepared in a DataProvider class and returned as a ProviderResponse object, which the Presenter then passed to the renderer.

After: Data is prepared entirely in TypoScript:

  • Simple field values: use variables with content objects such as TEXT, FILES, etc.
  • Database relations and menus: use standard TYPO3 dataProcessing processors (e.g., database-query, menu).
  • Per-record variable processing inside a loop: use process-variables.

Template selection (Presenter → templateName) 

Before: The Presenter called $this->renderer->render('path/to/template', $data).

After: The template is declared in TypoScript:

tt_content.my_element {
    templateName = MyElement
}
Copied!

For conditional template selection, use stdWrap on templateName:

tt_content.my_element {
    templateName = MyElement
    templateName.override.if {
        isTrue.field = tx_myext_variant
        value = special
    }
    templateName.override = MyElementSpecial
}
Copied!

Helper registration 

Before: Helpers were registered via Services.yaml tags:

Configuration/Services.yaml
services:
  Vendor\Extension\Renderer\Helper\GreetHelper:
    tags:
      - name: handlebars.helper
        identifier: 'greet'
        method: 'greetById'
Copied!

After: Use the #[AsHelper] attribute directly on the class or method:

EXT:my_extension/Classes/Renderer/Helper/GreetHelper.php
use CPSIT\Typo3Handlebars\Attribute\AsHelper;
use CPSIT\Typo3Handlebars\Renderer\Helper\Helper;
use DevTheorem\Handlebars\HelperOptions;

#[AsHelper('greet')]
final readonly class GreetHelper implements Helper
{
    public function render(HelperOptions $options): string { /* ... */ }
}
Copied!

The Services.yaml tag approach still works and can be used if you cannot modify the helper class (e.g., a third-party class).

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:

tt_content.textmedia = HANDLEBARSTEMPLATE
tt_content.textmedia {
    templateRootPaths.10 = EXT:my_extension/Resources/Private/Templates
    partialRootPaths.10 = EXT:my_extension/Resources/Private/Partials
}
Copied!

Contributing 

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.

Create an issue first 

Before you start working on the extension, please create an issue on GitHub: https://github.com/CPS-IT/handlebars/issues

Also, please check if there is already an issue on the topic you want to address.

Contribution workflow 

Preparation 

Clone the repository first:

git clone https://github.com/CPS-IT/handlebars.git
cd handlebars
Copied!

Now install all Composer dependencies:

composer install
Copied!

Analyze code 

# All analyzers
composer analyze

# Specific analyzers
composer analyze:dependencies
Copied!

Check code quality 

# All linters
composer lint

# Specific linters
composer lint:composer
composer lint:editorconfig
composer lint:php
composer lint:typoscript

# Fix all CGL issues
composer fix

# Fix specific CGL issues
composer fix:composer
composer fix:editorconfig
composer fix:php
composer fix:typoscript

# All static code analyzers
composer sca

# Specific static code analyzers
composer sca:php
Copied!

Run tests 

# All tests
composer test

# Specific tests
composer test:functional
composer test:unit

# All tests with code coverage
composer test:coverage

# Specific tests with code coverage
composer test:coverage:functional
composer test:coverage:unit

# Merge code coverage of all test suites
composer test:coverage:merge
Copied!

Code coverage reports are written to .Build/coverage. You can open the last merged HTML report like follows:

open .Build/coverage/html/_merged/index.html
Copied!

Build documentation 

# Rebuild and open documentation
composer docs

# Build documentation (from cache)
composer docs:build

# Open rendered documentation
composer docs:open
Copied!

The built docs will be stored in .Build/docs.

Pull Request 

Once you have finished your work, please submit a pull request and describe what you've done: https://github.com/CPS-IT/handlebars/pulls

Ideally, your PR references an issue describing the problem you're trying to solve. All described code quality tools are automatically executed on each pull request for all currently supported PHP versions and TYPO3 versions.

Sitemap