Gradual migration
Replacing every Fluid template in one step is rarely practical. This page describes an incremental approach that lets Fluid and Handlebars coexist in the same TYPO3 installation — even within the same extension — so you can migrate one content element or controller at a time.
Strategy overview
The recommended path has three lanes that can be worked independently:
- Content elements rendered via
tt_TypoScript: Replace the content object type fromcontent.* FLUIDTEMPLATEtoHANDLEBARSTEMPLATEone CType at a time. - Extbase controllers: Extend
Handlebars, then migrate templates action by action with a Fluid fallback in place.Controller - Shared partials / layout: Migrate the layout shell last, once all templates that depend on it have been converted.
-
Install and configure paths
Install the extension and include the base site set without the content-element set. The content-element set replaces
lib.globally; omitting it keeps all existing content elements on Fluid.content Element config/sites/<site>/config.yamldependencies: - cpsit/handlebars # base set only — does NOT touch lib.contentElementCopied!Configure template and partial root paths for Handlebars independently of the Fluid paths:
plugin.tx_handlebars { view { templateRootPaths { 10 = EXT:my_sitepackage/Resources/Private/Templates/Handlebars } partialRootPaths { 10 = EXT:my_sitepackage/Resources/Private/Partials/Handlebars } } }Copied!Fluid and Handlebars path registrations are completely independent, so there is no risk of one system picking up the other's files.
-
Migrate content elements one at a time
For each content element, convert the TypoScript definition from
FLUIDTEMPLATEtoHANDLEBARSTEMPLATEand create the corresponding.hbsfile.Before (Fluid):
tt_content.tx_myext_teaser = FLUIDTEMPLATE tt_content.tx_myext_teaser { templateName = Teaser templateRootPaths.10 = EXT:my_extension/Resources/Private/Templates partialRootPaths.10 = EXT:my_extension/Resources/Private/Partials layoutRootPaths.10 = EXT:my_extension/Resources/Private/Layouts variables { title = TEXT title.field = header } dataProcessing { 10 = TYPO3\CMS\Frontend\DataProcessing\FilesProcessor 10 { references.fieldName = image as = images } } }Copied!After (Handlebars):
tt_content.tx_myext_teaser = HANDLEBARSTEMPLATE tt_content.tx_myext_teaser { templateName = Teaser templateRootPaths.10 = EXT:my_extension/Resources/Private/Templates/Handlebars partialRootPaths.10 = EXT:my_extension/Resources/Private/Partials/Handlebars variables { title = TEXT title.field = header } dataProcessing { 10 = files 10 { references.fieldName = image as = images } } }Copied!All other
tt_definitions that are not yet migrated continue to use Fluid without any changes.content.* -
Migrate Extbase controllers
For controller-based rendering, the extension provides
Handlebars, which replaces TYPO3's defaultView Factory Fluid. It inspects the TypoScript configuration and returns aView Factory Handlebarswhen Handlebars configuration is present, or falls back to the Fluid view when it is not.View Note
The view factory is globally injected to all Extbase controllers extending
Action, so there's no need to switch the view factory injection.Controller Extend HandlebarsController:
For controllers you own, extend
\CPSIT\instead ofTypo3Handlebars\ Controller\ Handlebars Controller Action. TheController rendermethod renders the current action via Handlebars:View () EXT:my_extension/Classes/Controller/BlogController.phpnamespace Vendor\Extension\Controller; use CPSIT\Typo3Handlebars\Controller\HandlebarsController; use Psr\Http\Message\ResponseInterface; final class BlogController extends HandlebarsController { public function listAction(): ResponseInterface { $this->view->assign('posts', $this->postRepository->findAll()); return $this->htmlResponse($this->renderView()); } }Copied!The view factory resolves the template name automatically from the controller alias and action name (e.g.,
Blog/forlist. hbs list). Configure paths and per-action overrides via theAction handlebarskey in the plugin's TypoScript namespace:plugin.tx_myextension_myplugin { handlebars { default { templateRootPaths.10 = EXT:my_extension/Resources/Private/Templates/Handlebars partialRootPaths.10 = EXT:my_extension/Resources/Private/Partials/Handlebars } } }Copied!See also
Extbase plugins for the full configuration reference, including per-controller and per-action overrides.
Using the Fluid fallback during migration:
When a controller action has not yet been ported, call
delegateon the view to delegate to the underlying Fluid view. This keeps the action working while the template is being migrated:Rendering () public function legacyAction(): ResponseInterface { $this->view->assign('items', $this->repository->findAll()); // Render with Fluid until the .hbs template is ready $content = $this->view instanceof HandlebarsView ? $this->view->delegateRendering() : $this->view->render(); return $this->htmlResponse((string)$content); }Copied! -
Migrate the page layout shell
The layout shell (the outer HTML document, header, navigation, footer) is typically the last thing to migrate because it is shared by every page. Keep the existing Fluid layout in place until all content elements and actions that reference it have been converted to Handlebars partials.
Once all consumers are migrated, convert the Fluid layout to a Handlebars layout partial using the
extend/block/contentpattern described in Layouts and partials. -
Switch the lib.contentElement default
After every content element has been migrated to
HANDLEBARSTEMPLATE, include thecpsit/site set. This setshandlebars- content- element lib.globally so that newly created content elements start from Handlebars automatically:content Element = HANDLEBARSTEMPLATE config/sites/<site>/config.yamldependencies: - cpsit/handlebars - cpsit/handlebars-content-elementCopied!
See also
- Quick start — minimal working setup from scratch
- Layouts and partials — layout and section migration
- Helpers — porting ViewHelpers