Configuration
Site configuration
Headless is enabled per site in config/:
dependencies:
- friendsoftypo3/headless
headless: 1
dependencies — pick one site set:
friendsoftypo3/— trimmed default response (new projects).headless friendsoftypo3/— full 4.x-compatible response (upgrades).headless- legacy friendsoftypo3/— JSON only for requests sent with exactlyheadless- mixed Accept: application/; everything else renders your own HTML page. The JSON it serves is the full 4.x-shaped (legacy) response, not the trimmed default one.json
headless — run mode: 0 off, 1 always JSON, 2 mixed (Accept-driven —
pair it with the mixed set). In mixed mode the first Accept header
value must match exactly: Accept: application/
(the axios/fetch default) or application/ renders
HTML. A site using sets must not carry a root
sys_template record: its "Clear" flags wipe all set-provided TypoScript.
Sites not using sets can select the equivalent sys_template statics instead
("Headless", "Headless Legacy (4.x)", "Headless - Mixed mode JSON response").
Headless 4.x ships sets as well (TYPO3 v13): friendsoftypo3/
(there: the full response) and friendsoftypo3/. On TYPO3
v12 include the headless static template in the root TypoScript record
instead.
URLs pointing at the frontend domain (frontend, frontend,
per-language variants) are covered in Multi-Site & URL Configuration.
Automatic integrations
Headless detects installed core extensions and integrates them without
further setup: EXT: (JSON form definitions, form editor additions),
EXT: (JSON login plugin), EXT: (JSON redirect
envelopes, frontend-aware backend modules) and EXT:
(canonical/meta-tag managers, hreflang rewriting). Workspace preview needs
no setup either — core rendering plus the backend preview-URL rewrite
cover it. Whether a request gets the headless behaviour follows the
site's mode: always with headless: 1, only for exact
Accept: application/ requests with headless: 2.
Feature flags
Set flags in settings./additional.:
$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['<flag>'] = true;
headless.storageProxy — serve processed files through the site's
frontend/frontend instead of the TYPO3 host.
headless.elementBodyResponse — on POST/PUT/DELETE requests return only
the element matching response from the request body — clean
plugin output for SPA form handling:
POST https://example.tld/path-to-form-plugin
Content-Type: application/x-www-form-urlencoded
responseElementId=#ELEMENT_ID#&tx_form_formframework[email]=email&...
Set response to match a nested (child) element.
On a mixed-mode site (headless: 2) the POST itself must carry the exact
Accept: application/ header, or the middleware does not act.
headless.overrideFluidTemplates — swap the core View
for Headless: Fluid views render JSON templates, raw-PHP
templates (Headless) are supported too.
To render an Extbase plugin through a raw-PHP template, set the plugin's
request format and place the template at
<template (highest-keyed
root path wins):
plugin.tx_myext.view.templateRootPaths.100 = EXT:site_package/Resources/Private/Templates/
plugin.tx_myext_pi1.format = php
An explicit $view->render from your own code resolves
Path/ against the same root paths.
Template selection is not user-controllable: format is a regular Extbase
request parameter, but Headless serves the raw-PHP view only
when the dispatched plugin's TypoScript sets format = php. A
request-supplied tx_ on any other plugin falls back
to Fluid with the format stripped, so Fluid never parses a .php file as
a template. A visitor forcing format=html on a php-configured plugin
just gets the plugin's regular Fluid resolution — standard Extbase format
behavior.
Unlike Fluid, raw-PHP templates apply no output escaping. Build a PHP
array and echo json_ — never
concatenate JSON strings from record data, that is a JSON injection the
moment a value contains a quote. As everywhere in the headless pipeline,
JSON encoding is not HTML safety: the consuming frontend still escapes on
render.
headless.cookieDomainPerSite — derive the auth cookie domain per site (FE & BE middleware); see Multi-Site & URL Configuration.
headless.assetsCacheBusting — append the file mtime as a query string to processed-file URLs.
headless.prettyPrint — JSON_ on every response (debugging).
Older versions: the per-release availability matrix lives in
Reference: Feature Flags. The only flag dropped between 4.x and 5.x is
headless. — the redirects integration is now
auto-enabled when EXT:redirects is installed.
EXT:form
Form integration — JSON form definitions, i18n, decorators, validators and
the Json finisher — is documented in EXT:form.
Content element categories
The default TYPO3 Headless site set (friendsoftypo3/) does not render
categories at all: lib. does not define a categories field
(and therefore no content element carries one), and the page response does not contain
a categories field either. This avoids one sys_ join per content
element on every uncached render for projects that do not use categories.
If your project needs them, re-add the field in your site package TypoScript,
loaded after the set. Point pid at the place your categories are actually
stored — either the storage folder uid, or the current root page with recursive
as shown below.
Content element categories:
lib.contentElement.fields.categories = COA
lib.contentElement.fields.categories {
10 = CONTENT
10 {
table = sys_category
select {
pidInList.data = leveluid : 0
recursive = 99
selectFields = sys_category.title
join = sys_category_record_mm on sys_category_record_mm.uid_local = sys_category.uid
where {
field = uid
wrap = AND sys_category_record_mm.tablenames = 'tt_content' AND sys_category_record_mm.uid_foreign=|
}
}
renderObj = TEXT
renderObj {
field = title
wrap = |###BREAK###
}
}
stdWrap.split {
token = ###BREAK###
cObjNum = 1 |*|2|*| 3
1 {
current = 1
stdWrap.wrap = |
}
2 {
current = 1
stdWrap.wrap = ,|
}
3 {
current = 1
stdWrap.wrap = |
}
}
}
Page categories — the legacy lib. definition is still shipped
(the legacy and mixed sets load it; the default set does not). Import it,
fix the storage pid the same way, and
add the field back to the page response:
@import 'EXT:headless/Configuration/TypoScript/Legacy/Categories.typoscript'
lib.categories.10.select.pidInList >
lib.categories.10.select.pidInList.data = leveluid : 0
lib.categories.10.select.recursive = 99
page.10.fields.categories =< lib.categories
Both snippets render a comma-separated string of category titles, matching the output of the legacy set — use them when the consuming frontend should not need any changes.
Alternative: categories as a JSON array
If your frontend does not depend on the legacy string format, prefer structured
output. This variant uses the EXT: DatabaseQueryProcessor and renders
each category as an object, so the field becomes
"categories": instead of "News,Events":
lib.contentElement.fields.categories = JSON
lib.contentElement.fields.categories {
dataProcessing {
10 = FriendsOfTYPO3\Headless\DataProcessing\DatabaseQueryProcessor
10 {
table = sys_category
pidInList.data = leveluid : 0
recursive = 99
join = sys_category_record_mm ON sys_category_record_mm.uid_local = sys_category.uid
where.data = field:uid
where.wrap = sys_category_record_mm.tablenames = 'tt_content' AND sys_category_record_mm.uid_foreign=|
orderBy = sys_category.sorting
as = categories
fields {
id = INT
id {
field = uid
}
title = TEXT
title {
field = title
}
}
}
}
}
For page categories use the same definition with
where.
and assign it to the page response instead:
page.10.fields.categories = JSON
page.10.fields.categories {
dataProcessing {
# same processor configuration as above, with tablenames = 'pages'
}
}
Note this changes the shape of the categories field — frontends migrating from
the legacy set must be updated accordingly.
Preview of hidden pages
The frontend can preview hidden pages if backend cookies reach it. Since
there are no cross-domain cookies, backend and frontend must share a root
domain (e.g. api. / domain.); set it as cookie
(note the leading dot):
$GLOBALS['TYPO3_CONF_VARS']['BE']['cookieDomain'] = '.domain.com';
Important
If you are logged into the backend when this changes, delete the
be_ cookie in your browser — the old cookie blocks login.
If the frontend forwards all backend cookies, previewing hidden content
works. For multi-domain setups (api./domain1.,
api./domain2.) use the headless.
feature flag instead of a static cookie.
Workspace preview
Previewing workspace versions from the Workspaces module works out of the box — the JSON output renders the workspace overlay of the content, and backend preview URLs are rewritten to the frontend domain. The cookie requirements above apply here too. No feature flag is involved (in 4.x this ran through workspace XClasses, likewise enabled automatically).
Backend preview of mixed-mode sites
Backend-initiated page previews of a headless: 2 site render HTML by
default. To preview JSON instead, set in the site's settings.:
headless:
preview:
overrideMode: 1
The value replaces the site's mode for backend previews only (0 HTML —
the default, 1 JSON). Sites with headless: 0 or headless: 1 are not
affected.
XML sitemap
Since 4.0 the XML sitemap is plain core EXT: — headless only ships the
rendering templates. If URLs in the sitemap index (/sitemap.) point at
the API host instead of the frontend, set frontend in the site's
config. (the field shows up in the backend site module only with
headless. enabled — editing the YAML directly always works),
or point the sitemap at frontend via settings.:
headless:
sitemap:
key: frontendBase
The sitemap-index links are detected by their page type. If your sitemap
uses a custom typeNum, also set it (default 1533906435) — otherwise the
index links are not rewritten at all:
headless:
sitemap:
type: '2400000000'