
When I rebuilt hoja.in on TYPO3 14.3, the old site package was full of flux page templates, a long FLUIDTEMPLATE block and a sys_template record with static includes. None of it came along. The new package is small, and most of the wiring is done by conventions that arrived in TYPO3 13 and are the normal way in 14.
This post builds a minimal site package from zero with exactly those conventions. Copy it, rename the vendor, and you have a working page.
What was wrong with the old way
The classic setup had three places that needed to agree with each other: a sys_template record in the database with static includes, TypoScript that resolved the template name from the backend layout with a long stdWrap chain, and three separate root paths for templates, layouts and partials. Change one and forget the other, and you get a white page with an exception. I have debugged that more times than I want to count.
Two features in TYPO3 13 removed most of this:
- Site sets (Feature #103437, with TypoScript and TSconfig providers in #103439 and #103522). A set is a folder with a
config.yaml. Files calledsetup.typoscript,constants.typoscriptandpage.tsconfignext to it are loaded automatically. - PAGEVIEW (Feature #103504). A content object that picks the Fluid template from the page’s backend layout and finds layouts and partials by folder convention.
Step 1: The package skeleton
This is the whole tree of the example. Nothing is hidden.
packages/my-site/
├── composer.json
├── Configuration/
│ └── Sets/
│ └── MySite/
│ ├── config.yaml
│ ├── setup.typoscript
│ └── page.tsconfig
└── Resources/
└── Private/
└── PageView/
├── Layouts/
│ └── Page.html
├── Pages/
│ └── Default.html
└── Partials/
└── Navigation.htmlThe composer.json needs the extension key and the core packages you depend on:
{
"name": "myvendor/my-site",
"type": "typo3-cms-extension",
"license": "GPL-2.0-or-later",
"require": {
"typo3/cms-core": "^14.3",
"typo3/cms-fluid-styled-content": "^14.3"
},
"extra": {
"typo3/cms": {
"extension-key": "my_site"
}
}
}In the project root, add the folder as a path repository and require the package: composer config repositories.local path 'packages/*' and composer require myvendor/my-site:@dev.
Step 2: The set definition
The config.yaml of the set gives it a name and lists other sets it depends on. Dependencies are loaded first and de-duplicated, which replaces the old static includes.
name: myvendor/my-site
label: 'My site'
dependencies:
- typo3/fluid-styled-contentOn hoja.in the set also depends on typo3/form and typo3/seo-sitemap, because the contact page uses the form framework and the sitemap should be there without extra TypoScript.
Step 3: TypoScript with PAGEVIEW
setup.typoscript next to config.yaml is picked up without any include. This is the full file:
page = PAGE
page {
meta.viewport = width=device-width, initial-scale=1
10 = PAGEVIEW
10 {
paths.10 = EXT:my_site/Resources/Private/PageView/
dataProcessing {
10 = menu
10 {
levels = 1
as = mainMenu
}
}
}
}Our hero here is paths. PAGEVIEW appends Pages/, Layouts/ and Partials/ to each path itself. Several paths are possible (paths.10, paths.20) and higher numbers win, so another package can override single templates.
In the template you get some variables for free: page, site, language and settings. Since TYPO3 14.2 there is also content, which holds the content areas of the current backend layout (Feature #104974). These default names are reserved; trying to override them throws an exception.
Step 4: A backend layout that maps to a template
page.tsconfig in the set folder is loaded as page TSconfig for every page of a site that uses the set. One layout with one column is enough to start:
mod.web_layout.BackendLayouts {
Default {
title = Default page
config {
backend_layout {
colCount = 1
rowCount = 1
rows {
1 {
columns {
1 {
name = Main content
colPos = 0
identifier = main
}
}
}
}
}
}
}
}Select it in the page properties of the root page, as “Backend Layout” and “for subpages”. The stored value is pagets__Default; PAGEVIEW strips the prefix and renders Pages/Default.html. When no layout is selected at all, the resolver falls back to the name default, and Fluid in v14 tries the given spelling first and the capitalised one after it, so Pages/Default.html still matches. A second layout called Legal would render Pages/Legal.html, which is exactly how the legal pages on hoja.in get their own template.
The identifier = main is important. It becomes the key of the content area in Fluid.
Step 5: The Fluid files
The page template only picks a layout and fills a section:
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<f:layout name="Page" />
<f:section name="Main">
<f:render.contentArea contentArea="{content.main}" />
</f:section>
</html>The layout holds the frame around it:
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<header>
<a href="{site.base}">{site.configuration.websiteTitle}</a>
<f:render partial="Navigation" arguments="{mainMenu: mainMenu}" />
</header>
<main id="content">
<f:render section="Main" />
</main>
</html>And the partial renders the menu from the data processor:
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<nav aria-label="Main">
<ul>
<f:for each="{mainMenu}" as="item">
<li><a href="{item.link}" aria-current="{f:if(condition: item.current, then: 'page', else: 'false')}">{item.title}</a></li>
</f:for>
</ul>
</nav>
</html>f:render.contentArea is new in TYPO3 14.2 (Feature #108726). In 13.4 you would loop over the records yourself or use a CONTENT object; the ViewHelper is nicer because other extensions can hook into the output with the ModifyRenderedContentAreaEvent. If your package must also run on v13, keep the old way for now.
A small note on file names: Fluid 5 in v14 also accepts Default.fluid.html (Feature #108166). It is optional. I kept plain .html.
Step 6: Let the site depend on the set
The last piece is the site configuration. Instead of a sys_template record, the site lists the set:
rootPageId: 1
base: 'https://example.ddev.site/'
dependencies:
- myvendor/my-site
languages:
-
title: English
enabled: true
languageId: 0
base: /
locale: en_US.UTF-8You can also choose the set in the backend in the Sites module. A site that uses sets acts as its own TypoScript root, so there is no “clear” and “root” flag to forget. A sys_template record still works and is applied after the site TypoScript, but for a fresh project I do not create one at all.
Flush the caches with vendor/bin/typo3 cache:flush, open the frontend, and the page renders with your layout, your menu and the content of the main column.
What I learned
- Folder names matter more than TypoScript now.
Pages,Layouts,Partials, with exactly this spelling. - The backend layout identifier is the template name. Rename one and you must rename the other.
- Put everything that belongs to the site design in the set: TypoScript, TSconfig, form configuration. Then a second site is just one more line in its
config.yaml.
If this post helped you to start your package without the old boilerplate, I will be really happy.