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