Handlebars 

Version

main

Language

en

Author

Elias Häußler, coding. powerful. systems. CPS GmbH

Email

e.haeussler@familie-redlich.de

License

This extension documentation is published under the CC BY-NC-SA 4.0 (Creative Commons) license

TYPO3

The content of this document is related to TYPO3 CMS, a GNU/GPL CMS/Framework available from typo3.org.

Community documentation

This documentation is a community documentation for the TYPO3 extension handlebars. It is maintained as part of this third party extension.

Contributing to this manual

If you find an error or something is missing, please create an issue or click on "Edit me on GitHub" on the top right to submit your change request.

Contents 

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

Support 

There are several ways to get support for this extension:

Contributors 

This extension is based on the great work of Digitas Pixelpark GmbH. It was heavily extended and is now maintained by coding. powerful. systems. CPS GmbH.

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

Installation 

Require the extension via Composer (recommended):

composer require cpsit/typo3-handlebars
Copied!

Or download it from TYPO3 extension repository.

Define dependencies 

Each extension that depends on EXT:handlebars needs to explicitly define it as dependency in the appropriate ext_emconf.php file:

# ext_emconf.php

$EM_CONF[$_EXTKEY] = [
    'constraints' => [
        'depends' => [
            'handlebars' => '0.7.0-0.7.99',
        ],
    ],
];
Copied!

Otherwise, template paths are not evaluated in the right order and might get overridden.

Configuration 

Much of the extension configuration takes place via the service configuration using Services.yaml. You should be familiar with the service configuration and Dependency injection in general to be able to implement most of the necessary configurations correctly.

Learn here which configurations are necessary and learn more about how to use the extension:

Cache 

The Handlebars cache is called handlebars and is registered by default when installing and activating the extension. Its cache backend is not configured explicitly and therefore uses the default setting (database cache).

You can specify a different cache backend as follows:

# 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!

Template paths 

There exist several ways to declare template root paths and partial root paths. The most relevant ones are described below.

Configuration via service container 

The easiest way to register your template root paths and partial root paths is by using the Services.yaml file:

# Configuration/Services.yaml

handlebars:
  view:
    templateRootPaths:
      10: EXT:my_extension/Resources/Private/Templates
    partialRootPaths:
      10: EXT:my_extension/Resources/Private/Partials
Copied!

The HandlebarsExtension takes care of all template paths. They will be merged and then added to the service container resulting in the following parameters:

  • %handlebars.templateRootPaths%
  • %handlebars.partialRootPaths%

You can reference those parameters in your custom configuration to use the resolved template paths in your services.

The drawback of this configuration is that it is applied to the whole TYPO3 instance since there exists only one service container for the whole system. In case you need different template paths for specific parts of your installation, take a look at the following configuration method that uses TypoScript.

Configuration via TypoScript 

A more flexible configuration method is the usage of TypoScript. This way you can override the configuration from the service container (as described above) which allows you to define different template root paths and partial root paths for specific parts of the system.

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

Default data 

It may happen that several (or all) templates require recurring, consistent data. This can be, for example, paths to assets or other firmly defined content such as e-mail addresses or names/labels/etc.

The standard HandlebarsRenderer provides the possibility to specify an array $rootContext for this purpose. This data is merged with the concrete render data during each rendering and passed on to the Renderer.

Configuration 

In your Services.yaml file, add the following lines:

# Configuration/Services.yaml

handlebars:
  variables:
    publicPath: /assets
    # ...
Copied!

All data will then be available as service parameter %handlebars.variables% within the service container. So you can use it everywhere you need it in your Services.yaml file.

Overwrite default data 

If in certain cases it is necessary to overwrite a value from the default data, it can simply be passed as an additional value in the Presenter:

# Classes/Presenter/MyCustomPresenter.php

namespace Vendor\Extension\Presenter;

use CPSIT\Typo3Handlebars\Data\Response\ProviderResponse;
use CPSIT\Typo3Handlebars\Presenter\AbstractPresenter;

class MyCustomPresenter extends AbstractPresenter
{
    public function present(ProviderResponse $data): string
    {
        $renderData = [
            // ...
        ];

        // Overwrite default data "publicPath"
        $renderData['publicPath'] = '/custom/path/to/assets';

        return $this->renderer->render('path/to/template', $renderData);
    }
}
Copied!

Debugging 

Rendering of Handlebars templates can be done with additional debugging. This results in individual tags being provided with debug output, which can be used to better localize errors, especially during development, and thus fix them more efficiently.

TypoScript 

# Disable debugging
config.debug = 0

# Enable debugging
config.debug = 1
Copied!

Local configuration 

// Disable debugging
$GLOBALS['TYPO3_CONF_VARS']['FE']['debug'] = false;

// Enable debugging
$GLOBALS['TYPO3_CONF_VARS']['FE']['debug'] = true;
Copied!

Usage 

This section describes basic and advanced functionalities and their possible applications. Here you can learn how to use the individual components of the extension optimally and how to reuse them in your own components.

Basic usage 

This page shows how to create a single module and which components are necessary to process data from TYPO3, prepare it and finally output it in the frontend using a Handlebars template.

The CType header serves as an example.

Example 

  1. Preparations

    Before the actual components are created, some preliminary work is necessary.

    1. Installation

      Install the extension using Composer.

    2. Define dependencies

      Add the extension as dependency to your extension as described here.

    3. Services.yaml

      Create a basic Services.yaml file in your extension. Make sure to read and follow the guidelines described at Dependency injection.

    4. TypoScript configuration

      Create a TypoScript configuration file and include it in your site's root template. Make sure to include static TypoScript from EXT:fluid_styled_content.

  2. Create a new DataProcessor

    Each DataProcessor must implement the CPSIT\Typo3Handlebars\DataProcessing\DataProcessor interface.

    There's already a default DataProcessor in place that provides some basic logic and is required in case you want to develop components like described on this page. Just extend your DataProcessor from CPSIT\Typo3Handlebars\DataProcessing\AbstractDataProcessor and implement the abstract method render():

    # Classes/DataProcessing/HeaderProcessor.php
    
    namespace Vendor\Extension\DataProcessing;
    
    use CPSIT\Typo3Handlebars\DataProcessing\AbstractDataProcessor;
    
    class HeaderProcessor extends AbstractDataProcessor
    {
        protected function render(): string
        {
            $data = $this->provider->get($this->cObj->data);
            return $this->presenter->present($data);
        }
    }
    
    Copied!
  3. Register DataProcessor as service

    The HeaderProcessor must now be registered in the Services.yaml in the next step:

    # Configuration/Services.yaml
    
    services:
      Vendor\Extension\DataProcessing\HeaderProcessor:
        tags: ['handlebars.processor']
    Copied!

    All related components (DataProvider, Presenter) are now automatically assigned to this DataProcessor and registered accordingly.

  4. Create a new DataProvider

    Next, a DataProvider must be created that prepares the module's data and makes it available to the DataProcessor again. Each DataProvider must implement the CPSIT\Typo3Handlebars\Data\DataProvider interface.

    # Classes/Data/HeaderProvider.php
    
    namespace Vendor\Extension\Data;
    
    use CPSIT\Typo3Handlebars\Data\DataProvider;
    use CPSIT\Typo3Handlebars\Data\Response\ProviderResponse;
    use Vendor\Extension\Data\Response\HeaderProviderResponse;
    
    class HeaderProvider implements DataProvider
    {
        public function get(array $data): ProviderResponse
        {
            return (new HeaderProviderResponse($data['header']))
                ->setHeaderLayout((int)$data['header_layout'])
                ->setHeaderLink($data['header_link'])
                ->setSubheader($data['subheader']);
        }
    }
    
    Copied!

    As you can see, the DataProvider returns an instance of a so-called ProviderResponse object. This holds the prepared data for higher-level transfer within the rendering process. Create it in the associated namespace:

    # Classes/Data/Response/HeaderProviderResponse.php
    
    namespace Vendor\Extension\Data\Response;
    
    use CPSIT\Typo3Handlebars\Data\Response\ProviderResponse;
    
    class HeaderProviderResponse implements ProviderResponse
    {
        public const LAYOUT_DEFAULT = 0;
    
        private string $header;
        private int $headerLayout = self::LAYOUT_DEFAULT;
        private string $headerLink = '';
        private string $subheader = '';
    
        public function __construct(string $header)
        {
            $this->header = $header;
            $this->validate();
        }
    
        // Getters and setters...
    
        public function toArray(): array
        {
            return [
                'header' => $this->header,
                'headerLayout' => $this->headerLayout,
                'headerLink' => $this->headerLink,
                'subheader' => $this->subheader,
            ];
        }
    
        private function validate(): void
        {
            if ('' === trim($this->header)) {
                throw new \InvalidArgumentException('Header must not be empty.', 1626108393);
            }
        }
    }
    Copied!
  5. Create a new Presenter

    To complete the rendering process, a new Presenter called HeaderPresenter must be created. It must implement the CPSIT\Typo3Handlebars\Presenter\Presenter interface; furthermore, an CPSIT\Typo3Handlebars\Presenter\AbstractPresenter is already available with the default Renderer already specified as a dependency.

    # Classes/Presenter/HeaderPresenter.php
    
    namespace Vendor\Extension\Presenter;
    
    use CPSIT\Typo3Handlebars\Data\Response\ProviderResponse;
    use CPSIT\Typo3Handlebars\Exception\UnableToPresentException;
    use CPSIT\Typo3Handlebars\Presenter\AbstractPresenter;
    
    class HeaderPresenter extends AbstractPresenter
    {
        public function present(ProviderResponse $data): string
        {
            if (!($data instanceof HeaderProviderResponse)) {
                throw new UnableToPresentException(
                    'Received unexpected response from provider.',
                    1613552315
                );
            }
    
            // Use data from ProviderResponse or implement custom logic
            $renderData = $data->toArray();
    
            return $this->renderer->render(
                'Extensions/FluidStyledContent/Header',
                $renderData
            );
        }
    }
    Copied!
  6. Set up TypoScript configuration

    Finally, you have to configure via TypoScript that instead of the default Fluid rendering the special Handlebars rendering should be executed for all content elements of the CType header.

    For this purpose, each DataProcessor provides a method process(string $content, array $configuration) as entry point.

    # Configuration/TypoScript/setup.typoscript
    
    tt_content.header = USER
    tt_content.header.userFunc = Vendor\Extension\DataProcessing\HeaderProcessor->process
    Copied!
  7. Optional: Create custom Helpers

    If your templates use custom Helpers, you will need to create them additionally. Read Create a custom Helper to learn what options are available for creating your own Helpers.

  8. Flush caches

    Changes were made to the service configuration and also the rendering was overwritten using TypoScript. Therefore, it is now necessary to ensure that the caches are flushed and the service container is reconfigured.

Sources 

Extended usage 

Several advanced use cases are explained on the following pages. These are primarily aimed at specifically extending the standard components.

SimpleProcessor 

If it is not necessary to further process the data transferred by TypoScript, a SimpleProcessor is available. This passes the transferred data directly to the Renderer without any further interaction.

Usage 

tt_content.tx_myextension_mymodule = USER
tt_content.tx_myextension_mymodule {
    userFunc = CPSIT\Typo3Handlebars\DataProcessing\SimpleProcessor->process
    userFunc.templatePath = Extensions/FluidStyledContent/MyModule
}
Copied!

As you can see, the SimpleProcessor is directly addressed as userFunc. It already provides the necessary functionality, but can of course also be extended with your own requirements.

Furthermore it is necessary to indicate a template path with (this normally happens in the Presenter). The SimpleProcessor throws an exception, if the template path is not set or invalid.

Sources 

Using the ContentObjectRenderer 

In some cases, the presence of the current ContentObjectRenderer may be necessary in the DataProvider. For this case a CPSIT\Typo3Handlebars\ContentObjectRendererAware interface is provided, which can be used in combination with the trait CPSIT\Typo3Handlebars\Traits\ContentObjectRendererAwareTrait.

Usage 

  1. Transfer of the ContentObjectRenderer

    If the rendering process is triggered via TypoScript, the DataProcessor is automatically assigned the current instance of the ContentObjectRenderer (via the cObj property). It can then pass this to the DataProvider:

    # Classes/DataProcessing/CustomProcessor.php
    
    namespace Vendor\Extension\DataProcessing;
    
    use CPSIT\Typo3Handlebars\DataProcessing\AbstractDataProcessor;
    
    class CustomProcessor extends AbstractDataProcessor
    {
        protected function render(): string
        {
            $this->provider->setContentObjectRenderer($this->cObj);
            // ...
        }
    }
    Copied!
  2. Assure ContentObjectRenderer is available

    In the DataProvider, the existence of the ContentObjectRenderer can be easily checked if the associated trait is used:

    # Classes/Data/CustomProvider.php
    
    namespace Vendor\Extension\Data;
    
    use CPSIT\Typo3Handlebars\ContentObjectRendererAware;
    use CPSIT\Typo3Handlebars\Data\DataProvider;
    use CPSIT\Typo3Handlebars\Data\Response\ProviderResponse;
    use CPSIT\Typo3Handlebars\Traits\ContentObjectRendererAwareTrait;
    
    class CustomProvider implements DataProvider, ContentObjectRendererAware
    {
        use ContentObjectRendererAwareTrait;
    
        public function get(array $data): ProviderResponse
        {
            $this->assertContentObjectRendererIsAvailable();
            // ...
        }
    }
    Copied!
  3. Use the ContentObjectRenderer

    If successful, the ContentObjectRenderer can then be used, for example, to parse database content generated using RTE:

     # Classes/Data/CustomProvider.php
    
     namespace Vendor\Extension\Data;
    
     use CPSIT\Typo3Handlebars\ContentObjectRendererAware;
     use CPSIT\Typo3Handlebars\Data\DataProvider;
     use CPSIT\Typo3Handlebars\Data\Response\ProviderResponse;
     use CPSIT\Typo3Handlebars\Traits\ContentObjectRendererAwareTrait;
    +use Vendor\Extension\Data\Response\CustomProviderResponse;
    
     class CustomProvider implements DataProvider, ContentObjectRendererAware
     {
         use ContentObjectRendererAwareTrait;
    
         public function get(array $data): ProviderResponse
         {
             $this->assertContentObjectRendererIsAvailable();
    -        // ...
    +
    +        $text = $this->parseText($data);
    +
    +        return new CustomProviderResponse($text);
        }
    +
    +    private function parseText(string $plaintext): string
    +    {
    +        return $this->contentObjectRenderer->parseFunc($plaintext, [], '< lib.parseFunc_RTE');
    +    }
     }
    Copied!

Sources 

Shared components 

It is not always necessary or desired to automatically register all related components based on the registration of a DataProcessor. In some cases, for example, it may be necessary to use a component more than once, e.g. if several modules use the same template and thus one Presenter can be used for all those modules.

To be able to cover this special case, it is possible to specify a concrete DataProvider or Presenter for individual DataProcessors in the Services.yaml file.

Example 1: Shared Presenter 

Assume that there are two modules Highlight Box and Highlight Text, which are both rendered using the same Handlebars template. The data provision is still done via two separate DataProviders.

In the Services.yaml file, we register both DataProcessors, but specify a concrete method call setPresenter(). This is normally called automatically if it is not set manually.

# Configuration/Services.yaml

services:
  Vendor\Extension\DataProcessing\HighlightBoxProcessor:
    tags: ['handlebars.processor']
    calls:
      - setPresenter: ['@Vendor\Extension\Presenter\HighlightPresenter']
  Vendor\Extension\DataProcessing\HighlightTextProcessor:
    tags: ['handlebars.processor']
    calls:
      - setPresenter: ['@Vendor\Extension\Presenter\HighlightPresenter']
Copied!

Both DataProcessors are now injected with the same Presenter, while all other components continue to act independently.

Example 2: Shared DataProvider 

The same procedure can be used if a common DataProvider is to be used instead of a common Presenter. In this case the method call must be setProvider():

# Configuration/Services.yaml

services:
  Vendor\Extension\DataProcessing\HighlightBoxProcessor:
    tags: ['handlebars.processor']
    calls:
      - setProvider: ['@Vendor\Extension\Data\HighlightProvider']
  Vendor\Extension\DataProcessing\HighlightTextProcessor:
    tags: ['handlebars.processor']
    calls:
      - setProvider: ['@Vendor\Extension\Data\HighlightProvider']
Copied!

Sources 

Custom rendering components 

All components are described using interfaces. This makes it easy to exchange individual components. The following illustrates how such a use case can look.

Custom Renderer 

The CPSIT\Typo3Handlebars\Renderer\Renderer interface describes a Renderer. A distinction must be made as to whether the custom Renderer is to be used for all components or only for individual variants.

Global replacement 

If the custom Renderer is to be used equally for all components, it can simply be registered as a global replacement for the default Renderer in the Services.yaml file.

# Configuration/Services.yaml

services:
  CPSIT\Typo3Handlebars\Renderer\Renderer:
    alias: 'Vendor\Extension\Renderer\AlternativeRenderer'
Copied!

Single replacement 

A custom Renderer can also be used only for specific modules. In this case, it replaces the default Renderer for the concrete Presenters.

# Configuration/Services.yaml

services:
  Vendor\Extension\Presenter\MyCustomPresenter:
    arguments:
      $renderer: ['@Vendor\Extension\Renderer\AlternativeRenderer']
Copied!

Custom TemplateResolver 

A standard TemplateResolver exists for resolving template paths for templates and partials. This is used in the default Renderer, but a custom TemplateResolver can also be used for specific purposes.

To use a custom TemplateResolver, a corresponding class is created that implements the CPSIT\Typo3Handlebars\Renderer\Template\TemplateResolver interface:

# Classes/Renderer/Template/AlternativeTemplateResolver.php

namespace Vendor\Extension\Renderer\Template;

use CPSIT\Typo3Handlebars\Renderer\Template\TemplateResolver;

class AlternativeTemplateResolver implements TemplateResolver
{
    /**
     * @var list<string>
     */
    private array $supportedFileExtensions = ['hbs', 'hbs.html'];

    public function getSupportedFileExtensions(): array
    {
        return $this->supportedFileExtensions;
    }

    public function supports(string $fileExtension): bool
    {
        return in_array(strtolower($fileExtension), $this->supportedFileExtensions, true);
    }

    public function resolveTemplatePath(string $templatePath): string
    {
        // ...
    }
}
Copied!

This is then used in the Services.yaml file instead of the standard TemplateResolver:

# Configuration/Services.yaml

services:
  CPSIT\Typo3Handlebars\Renderer\Template\TemplateResolver:
    alias: 'Vendor\Extension\Renderer\Template\AlternativeTemplateResolver'
Copied!

Sources 

Create a custom Helper 

Any Helper implemented in the frontend via JavaScript and used in Handlebars templates must also be replicated in PHP. For this purpose, the extension provides a CPSIT\Typo3Handlebars\Renderer\Helper\Helper interface.

Implementation options 

There are several ways to implement Helpers. To understand how Helpers are resolved in the Renderer, it is worth taking a look at the responsible Trait.

Basically every registered Helper must be callable. This means that both globally defined functions and invokable classes as well as class methods are possible. See what options are available in the following examples.

Global function 

Any globally registered function can be used as a Helper, provided that it is recognized by the registered PHP autoloader.

function greet(array $context): string
{
    return sprintf('Hello, %s!', $context['hash']['name']);
}
Copied!

Invokable class 

Invokable classes can also be used as Helpers. For this it is necessary that they implement the method __invoke().

# Classes/Renderer/Helper/GreetHelper.php

namespace Vendor\Extension\Renderer\Helper;

use CPSIT\Typo3Handlebars\Renderer\Helper\Helper;

class GreetHelper implements Helper
{
    public function __invoke(array $context): string
    {
        return sprintf('Hello, %s!', $context['hash']['name']);
    }
}
Copied!

Registration 

Helpers can be registered either via configuration in the Services.yaml file or directly via the Renderer (if the default Renderer is used).

Manual registration 

In addition to automatic registration, Helpers can also be registered manually at any time. For this purpose it is necessary to initialize the Renderer beforehand. Then a Helper can be registered with the registerHelper() method and thus made available in the Renderer:

$renderer->registerHelper(
    'greet',
    \Vendor\Extension\Renderer\Helper\GreetHelper::class . '::greetById'
);
Copied!

Sources 

Events 

There are several events available that allow to influence the rendering. Event listeners must be registered via the service configuration. More information can be found in the official TYPO3 documentation.

BeforeRenderingEvent 

This event is triggered directly before the compiled template is rendered along with the provided data. This allows the data to be manipulated once again before it is passed to the Renderer.

Example:

# Classes/EventListener/BeforeRenderingListener.php

namespace Vendor\Extension\EventListener;

use CPSIT\Typo3Handlebars\Event\BeforeRenderingEvent;

class BeforeRenderingListener
{
    public function modifyRenderData(BeforeRenderingEvent $event): void
    {
        $data = $event->getData();

        // Do anything...

        $event->setData($data);
    }
}
Copied!

AfterRenderingEvent 

After the Renderer has completely rendered the template using the provided data, the AfterRenderingEvent is triggered. This can be used to subsequently influence the rendering result.

Example:

# Classes/EventListener/AfterRenderingListener.php

namespace Vendor\Extension\EventListener;

use CPSIT\Typo3Handlebars\Event\AfterRenderingEvent;

class AfterRenderingListener
{
    public function modifyRenderedContent(AfterRenderingEvent $event): void
    {
        $content = $event->getContent();

        // Do anything...

        $event->setContent($content);
    }
}
Copied!

Sources 

Asset Management 

The Handlebars extension integrates with TYPO3's modern AssetCollector API to manage JavaScript and CSS assets in your frontend rendering. This feature allows you to register external files and inline code directly through TypoScript configuration.

Overview 

TYPO3 13+ provides the AssetCollector service as the recommended way to register frontend assets. The Handlebars extension fully supports this API through the assets configuration in HANDLEBARSTEMPLATE content objects.

Benefits 

  • Deduplication: Assets with the same identifier are automatically merged across the page
  • Priority Control: Control rendering order with priority options
  • CSP Support: Automatic nonce injection for Content Security Policy compliance
  • Modern API: Uses TYPO3's recommended approach (not deprecated PageRenderer methods)

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
                    useNonce = 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 behavior.
Available Options
  • priority (boolean): Render before other assets (default: 0)
  • useNonce (boolean): Add CSP nonce attribute (default: 0)
Example
options {
    priority = 1
    useNonce = 1
}
Copied!

Complete Examples 

Basic Example 

page.20 = HANDLEBARSTEMPLATE
page.20 {
    templateName = MyPage

    variables {
        title = TEXT
        title.data = page:title
    }

    assets {
        # External JavaScript
        javaScript {
            app-script {
                source = EXT:myext/Resources/Public/JavaScript/app.js
                attributes {
                    defer = 1
                }
            }
        }

        # External CSS
        css {
            main-styles {
                source = EXT:myext/Resources/Public/Css/main.css
            }
        }
    }
}
Copied!

Advanced Example with Priority 

page.20 = HANDLEBARSTEMPLATE
page.20 {
    templateName = MyPage

    assets {
        # High-priority critical CSS
        css {
            critical-styles {
                source = EXT:myext/Resources/Public/Css/critical.css
                options {
                    priority = 1
                }
            }
        }

        # Regular stylesheet with media query
        css {
            responsive-styles {
                source = EXT:myext/Resources/Public/Css/responsive.css
                attributes {
                    media = screen and (min-width: 768px)
                }
            }
        }

        # Inline critical CSS (highest priority)
        inlineCss {
            above-the-fold {
                source = body { font-family: sans-serif; } h1 { font-size: 2em; }
                options {
                    priority = 1
                }
            }
        }

        # Modern JavaScript module
        javaScript {
            app-module {
                source = EXT:myext/Resources/Public/JavaScript/app.js
                attributes {
                    type = module
                }
                options {
                    useNonce = 1
                }
            }
        }

        # Legacy fallback for older browsers
        javaScript {
            app-legacy {
                source = EXT:myext/Resources/Public/JavaScript/app.legacy.js
                attributes {
                    nomodule = 1
                    defer = 1
                }
            }
        }

        # Inline initialization code
        inlineJavaScript {
            app-init {
                source = window.APP_CONFIG = { apiUrl: '/api' };
                options {
                    priority = 1
                }
            }
        }
    }
}
Copied!

CDN Example with Security 

assets {
    javaScript {
        cdn-library {
            source = https://cdn.example.com/library.js
            attributes {
                crossorigin = anonymous
                integrity = sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC
                defer = 1
            }
        }
    }

    css {
        cdn-styles {
            source = https://cdn.example.com/styles.css
            attributes {
                crossorigin = anonymous
                integrity = sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN
            }
        }
    }
}
Copied!

Migration from Legacy Methods 

If you're using the legacy headerAssets or footerAssets configuration, consider migrating to the modern assets approach:

Before (Legacy) 

10 = HANDLEBARSTEMPLATE
10 {
    templateName = MyTemplate

    headerAssets = TEXT
    headerAssets.value = <script src="path/to/script.js"></script>

    footerAssets = TEXT
    footerAssets.value = <script>console.log('footer');</script>
}
Copied!

After (Modern) 

10 = HANDLEBARSTEMPLATE
10 {
    templateName = MyTemplate

    assets {
        javaScript {
            my-script {
                source = EXT:myext/Resources/Public/JavaScript/script.js
            }
        }

        inlineJavaScript {
            footer-script {
                source = console.log('footer');
            }
        }
    }
}
Copied!

Benefits of Migration 

  • Automatic asset deduplication across multiple content elements
  • Better control over rendering order via priority
  • Content Security Policy nonce support
  • Type-safe attribute handling
  • Future-proof implementation using TYPO3's recommended API

Best Practices 

Unique Identifiers 

Always use unique, namespaced identifiers across your entire page to avoid conflicts:

# Good: Namespaced identifier
my-extension-app-script {
    source = ...
}

# Bad: Generic identifier (may conflict with other extensions)
app {
    source = ...
}
Copied!

Priority Management 

Use priority = 1 for assets that must load early:

# Critical inline CSS should have priority
inlineCss {
    critical {
        source = ...
        options.priority = 1
    }
}

# Regular stylesheets can have normal priority
css {
    theme {
        source = ...
        # No priority option = loaded after priority assets
    }
}
Copied!

Content Security Policy 

Enable useNonce = 1 for inline scripts when using CSP:

inlineJavaScript {
    inline-config {
        source = window.config = { /* ... */ };
        options.useNonce = 1
    }
}
Copied!

Module vs Classic Scripts 

Use type = module for ES6 modules and provide nomodule fallback for older browsers:

# Modern browsers
javaScript {
    app-module {
        source = EXT:myext/Resources/Public/JavaScript/app.module.js
        attributes.type = module
    }
}

# Legacy browsers
javaScript {
    app-legacy {
        source = EXT:myext/Resources/Public/JavaScript/app.legacy.js
        attributes.nomodule = 1
    }
}
Copied!

Troubleshooting 

Assets Not Appearing 

If assets don't appear in the rendered page:

  1. Check identifiers are unique - Duplicate identifiers will cause the last one to win
  2. Verify source is not empty - Empty sources will throw an exception
  3. Check for exceptions - Configuration errors will halt rendering and display an error
  4. Ensure AssetCollector is initialized - Only works in frontend context

Duplicate Assets 

If assets appear multiple times, check for:

  • Duplicate identifiers in different content objects
  • Multiple HANDLEBARSTEMPLATE objects registering the same asset

Use unique, namespaced identifiers to prevent conflicts:

# Use extension prefix to avoid conflicts
my-ext-my-script {
    source = ...
}
Copied!

Configuration Errors 

The extension throws InvalidAssetConfigurationException for:

  • Unknown asset type (valid types: javaScript, inlineJavaScript, css, inlineCss)
  • Invalid asset type configuration (must be an array)
  • Invalid or empty identifier
  • Invalid asset configuration (must be an array)
  • Missing source parameter
  • Empty source value (after trimming whitespace)

These errors will halt page rendering and display an exception message. Check your TypoScript configuration to resolve the issue.

Boolean Attributes Not Working 

Boolean attributes require the value 1 (or any truthy value) to be enabled:

# Correct
attributes {
    async = 1        # Will output: async="async"
    defer = 1        # Will output: defer="defer"
}

# Incorrect - will not be output
attributes {
    async = 0        # Omitted from output
    defer =          # Omitted from output
}
Copied!

See Also 

Rendering concept 

Rendering chart

The following components are involved in the rendering process and assume different roles in the MVC pattern:

DataProvider (Model) 

DataProviders are used to provide relevant data for processing by a DataProcessor. The data source is irrelevant: data can be provided both from local sources such as the TYPO3 database and from external sources such as APIs.

Thus, DataProviders fulfill the part of the Model in the MVC pattern. The data supplied is not necessarily only applicable to a specific template, but serves the general usability of all components involved in the rendering process of a parent module.

DataProcessor (Controller) 

DataProcessors are the entry point into the entire rendering process. They fetch the data from the DataProvider, process it and pass it on to the Presenter.

This is where the entire processing logic takes place. Thus, DataProcessors fulfill the part of the Controller in the MVC pattern. They are usually addressed directly via TypoScript.

Presenter (View transition) 

In Presenter, the supplied data is prepared for rendering a specific Handlebars template. Dependent templates can also be selected based on the supplied data if multiple template variants are possible.

In the MVC pattern, the Presenter takes on a transitional role between the DataProcessor (Controller) and the Renderer (View).

Renderer (View) 

The template is finally rendered in the Renderer. For this purpose, the template is compiled and filled with data from the Presenter. The resulting output is returned and completes the rendering process.

The Renderer is thus responsible for the View in the context of the MVC pattern. The compiled templates used for this are usually cached.

Helper (optional) 

Helpers describe a simple way to bring custom PHP functionality to Handlebars templates. They are similar to ViewHelpers used in Fluid templates.

The default Renderer is able to handle various Helpers. There are few limitations to the successful use of Helpers:

  • The associated callable (class method/function) must be publicly callable
  • If the callable is a class method, it must be loadable by the registered PHP class autoloader

Helpers play a rather subordinate role in the MVC pattern, since they are not explicitly involved. However, since they are implicitly involved in the output of a template, they most likely take the role of the View.

TemplateResolver 

Whenever a template is rendered by the Renderer, it must first be resolved, e.g. by looking it up in all defined template root paths. It is necessary to define a TemplateResolver for each Renderer, because the Renderer itself is not able to resolve template paths.

The TemplateResolver is also used for resolving partials. However, since partials do not necessarily have to be used, defining a TemplateResolver for them is optional.

Extbase controllers 

In order to increase compatibility with standard extbase plugins, the extension supports the rendering of extbase controller actions. For this purpose, a compatibility layer was introduced, with which own DataProcessors can be triggered using the HandlebarsViewResolver.

Configuration 

Tag each DataProcessor with handlebars.compatibility_layer within your Services.yaml file and provide additional information about the target extbase controller and actions supported by it.

# Configuration/Services.yaml

services:
  Vendor\Extension\DataProcessing\MyProcessor:
    tags:
      - name: handlebars.processor
      - name: handlebars.compatibility_layer
        type: 'extbase_controller'
        controller: 'Vendor\Extension\Controller\MyController'
        actions: 'dummy'
Copied!

The action configuration can be either empty (= NULL) or set to a comma-separated list of action names that are supported by the configured DataProcessor. If you leave it empty, the DataProcessor is used for all controller actions.

Usage 

Once the HandlebarsViewResolver is triggered to render a specific "view", it creates an array of information and passes it to the configured DataProcessor. You can then take further steps based on the provided configuration.

When accessing the $configuration property inside your DataProcessor, you should see the following properties:

$configuration = [
    'extbaseViewConfiguration' => [
        'controller' => '<controller class>',
        'action' => '<controller action>',
        'request' => '<original extbase request>',
        'variables' => '<template variables>',
    ],
];
Copied!

Sources 

Extbase repositories 

When Extbase repositories are used to fetch data via the DataProvider, it may be necessary to perform the necessary bootstrapping for Extbase repositories. This is the case whenever the rendering process is executed outside the Extbase context and fields such as tt_content.pages or tt_content.recursive are to be accessed in the repository to determine the storage PIDs.

To execute the necessary bootstrapping or to reset the underlying ConfigurationManager and to fill it with the current ContentObjectRenderer, the method initializeConfigurationManager() must be executed in the DataProcessor.

Usage 

 # Classes/DataProcessing/HeaderProcessor.php

 namespace Vendor\Extension\DataProcessing;

 use CPSIT\Typo3Handlebars\DataProcessing\AbstractDataProcessor;

 class HeaderProcessor extends AbstractDataProcessor
 {
     protected function render(): string
     {
+        $this->initializeConfigurationManager();
         $data = $this->provider->get($this->cObj->data);
         return $this->presenter->present($data);
     }
 }
Copied!

Sources 

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!
# All analyzers
composer analyze

# Specific analyzers
composer analyze:dependencies
Copied!

Check code quality 

# Run all linters
composer lint

# Run 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

# Run all static code analyzers
composer sca

# Run 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