Headless allows you to render JSON from TYPO3 content. You can customize output
by changing types, names and nesting of fields.
This extension provides the backend part (JSON API) for TYPO3 PWA solution.
The frontend part exists as JavaScript application
nuxt-typo3
which consumes the JSON API and renders the content using Nuxt framework of VueJS.
You can find the frontend documentation here.
If you have any questions just drop a line in our #initiative-pwa Slack channel.
Special thanks goes to macopedia.com company, which is sponsoring development of this solution.
TYPO3
The content of this document is related to TYPO3 CMS,
a GNU/GPL CMS/Framework available from typo3.org .
For Contributors
You are welcome to help improve this guide if you missing something.
Just click on "Edit me on GitHub" on the top right to submit your change request
or report a problem
EXT:headless renders TYPO3 pages and content as JSON. The response shape is
plain TypoScript — field names, types and nesting are customised with tools
TYPO3 integrators already know. Any frontend that speaks JSON can consume it;
nuxt-typo3 is the
reference implementation.
What you get
JSON API for pages, content elements, menus, metadata — with full
language/fallback handling
Per-site modes: full JSON, mixed (JSON only for
Accept: application/json), off
Extensible via TypoScript: custom fields, custom content elements
Installed core extensions integrate automatically: EXT:form,
EXT:felogin, EXT:redirects (JSON envelopes), EXT:seo
(meta tags, XML sitemap); workspace preview works out of the box
Five-minute path from a fresh TYPO3 install to your first JSON response.
1. Install
composer require friendsoftypo3/headless
Copied!
Composer is the recommended way. Classic mode still works but takes
manual steps: download the release, unpack it to
typo3conf/ext/headless, then activate it in the Extension Manager.
2. Create a root page + site config
In the backend, create a new page at the root level (the site root).
Then go to Site Management → Sites and add a site configuration
pointing at that page.
Important
Serve the API from a dedicated URL like https://api.example.com.
Paths on the main domain (https://example.com/api) can lead to
unexpected behaviour.
3. Add the headless set and switch the site to JSON
In MIXED mode the firstAccept header value must be exactly
application/json — Accept: application/json, text/plain, */* (the
axios/fetch default) or application/json; charset=utf-8 renders HTML.
Do not add a root sys_template record — sites using sets don't need one,
and its "Clear" flags would wipe the set TypoScript. Sites not using sets
can select the equivalent statics in a root TypoScript record instead:
"Headless", "Headless Legacy (4.x)" or "Headless - Mixed mode JSON response".
Headless 4.x ships sets too (TYPO3 v13): friendsoftypo3/headless (full
response) and friendsoftypo3/headless-mixed. On TYPO3 v12 include the
headless static template in the root TypoScript record instead.
Set and mode variants are described in Configuration.
4. Drop a content element on the page
Add a Text element (or any standard CE) to the page. Save.
Headless is enabled per site in config/sites/<identifier>/config.yaml:
dependencies:-friendsoftypo3/headlessheadless:1
Copied!
dependencies — pick one site set:
friendsoftypo3/headless — trimmed default response (new projects).
friendsoftypo3/headless-legacy — full 4.x-compatible response (upgrades).
friendsoftypo3/headless-mixed — JSON only for requests sent with exactly
Accept: application/json; everything else renders your own HTML page.
The JSON it serves is the full 4.x-shaped (legacy) response, not the
trimmed default one.
headless — run mode: 0 off, 1 always JSON, 2 mixed (Accept-driven —
pair it with the mixed set). In mixed mode the firstAccept header
value must match exactly: Accept: application/json, text/plain, */*
(the axios/fetch default) or application/json; charset=utf-8 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/headless
(there: the full response) and friendsoftypo3/headless-mixed. On TYPO3
v12 include the headless static template in the root TypoScript record
instead.
URLs pointing at the frontend domain (frontendBase, frontendApiProxy,
per-language variants) are covered in Multi-Site & URL Configuration.
Automatic integrations
Headless detects installed core extensions and integrates them without
further setup: EXT:form (JSON form definitions, form editor additions),
EXT:felogin (JSON login plugin), EXT:redirects (JSON redirect
envelopes, frontend-aware backend modules) and EXT:seo
(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/json requests with headless: 2.
headless.storageProxy — serve processed files through the site's
frontendApiProxy/frontendFileApi instead of the TYPO3 host.
headless.elementBodyResponse — on POST/PUT/DELETE requests return only
the element matching responseElementId 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&...
Copied!
Set responseElementRecursive=1 to match a nested (child) element.
On a mixed-mode site (headless: 2) the POST itself must carry the exact
Accept: application/json header, or the middleware does not act.
headless.overrideFluidTemplates — swap the core ViewFactoryInterface
for HeadlessViewFactory: Fluid views render JSON templates, raw-PHP
templates (HeadlessPhpView) are supported too.
To render an Extbase plugin through a raw-PHP template, set the plugin's
request format and place the template at
<templateRootPaths>/<ControllerName>/<ActionName>.php (highest-keyed
root path wins):
An explicit $view->render('Path/Name') from your own code resolves
Path/Name.php against the same root paths.
Template selection is not user-controllable: format is a regular Extbase
request parameter, but HeadlessViewFactory serves the raw-PHP view only
when the dispatched plugin's TypoScript sets format = php. A
request-supplied tx_myext_pi1[format]=php 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_encode($data, JSON_THROW_ON_ERROR) — 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_PRETTY_PRINT 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.redirectMiddlewares — the redirects integration is now
auto-enabled when EXT:redirects is installed.
EXT:form
Form integration — JSON form definitions, i18n, decorators, validators and
the JsonRedirect finisher — is documented in EXT:form.
Content element categories
The default TYPO3 Headless site set (friendsoftypo3/headless) does not render
categories at all: lib.contentElement 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_category 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 pidInList 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 = CONTENT10 {
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 = |
}
}
}
Copied!
Page categories — the legacy lib.categories 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:
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:headless DatabaseQueryProcessor and renders
each category as an object, so the field becomes
"categories": [{"id": 2, "title": "News"}, ...] 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
}
}
}
}
}
Copied!
For page categories use the same definition with
where.wrap = sys_category_record_mm.tablenames = 'pages' AND sys_category_record_mm.uid_foreign=|
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'
}
}
Copied!
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.com / domain.com); set it as cookieDomain
(note the leading dot):
If you are logged into the backend when this changes, delete the
be_typo_user 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.com/domain1.com,
api.domain2.com/domain2.com) use the headless.cookieDomainPerSite
feature flag instead of a static cookieDomain.
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.yaml:
headless:preview:overrideMode:1
Copied!
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:seo — headless only ships the
rendering templates. If URLs in the sitemap index (/sitemap.xml) point at
the API host instead of the frontend, set frontendApiProxy in the site's
config.yaml (the field shows up in the backend site module only with
headless.storageProxy enabled — editing the YAML directly always works),
or point the sitemap at frontendBase via settings.yaml:
headless:sitemap:key:frontendBase
Copied!
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'
Copied!
Multi-Site & URL Configuration
The headless setup typically has two domains — one for the API
(TYPO3 backend) and one for the frontend app. This page is the single
source of truth for how EXT:headless rewrites URLs, manages cookies
and routes assets across sites.
Glossary
Key
Meaning
base
Public URL of the TYPO3 site (the API).
frontendBase
Public URL of your SPA / frontend app. Used by
UrlUtility to rewrite links in the JSON
response.
frontendApiProxy
Public URL of the API as seen by browsers
going through the frontend's reverse proxy
(e.g. https://example.com/headless).
frontendFileApi
Public URL for processed files (images, PDFs).
Used together with the
headless.storageProxy feature flag.
cookieDomain
Domain to scope session cookies to. Needed
when API and frontend share a root domain.
baseVariants
Per-environment overrides of any of the
above, gated by an expression-language
condition.
Single-domain setup
API and frontend both on the same host. No URL rewriting needed.
API on api.example.com, frontend on example.com. Set
frontendBase so URLs in the JSON response point at the frontend.
rootPageId:1base:https://api.example.comheadless:1frontendBase:https://example.com# Optional, used with the headless.storageProxy feature flag:# processed-file URLs are served through these proxy paths# (page/typolink URLs always use frontendBase):frontendApiProxy:https://example.com/headlessfrontendFileApi:https://example.com/headless/fileadmin
Copied!
Now any typolink or page URL in the JSON response is rewritten from
https://api.example.com/... to https://example.com/....
Multi-language overrides
Each language can override the URL keys independently:
condition is evaluated by TYPO3CMSCoreExpressionLanguageResolver
with the site scope — available are the default variables
(applicationContext, typo3, date, features) and functions like
getenv("…"); request is not available here.
The first variant whose condition matches wins, and its values are read
verbatim: a key missing from the matching variant resolves to an empty
string, it does not fall back to the site-level value — repeat every
key in every variant.
Shared root domain & cookies
When api.example.com and example.com share a root domain
(example.com), the browser can carry session cookies between them
only if the cookie's Domain attribute is set to the shared root.
Option A — set globally (simple, single-site instance):
Option B — per-site (multi-domain instance): enable
headless.cookieDomainPerSite and put cookieDomain in the site
config. The CookieDomainPerSite middleware looks up the site by
exact request host and injects its cookieDomain for the
duration of the request.
If you changed cookieDomain after a backend login, remove the
stale be_typo_user cookie from your browser or you won't be able
to log back in.
Hidden-page preview
Preview of hidden pages relies on the same cookie flow as above. As
long as the frontend forwards all cookies to the API request, the
backend user's preview cookie reaches TYPO3 and hidden pages render.
Note
If you have a truly multi-domain setup (e.g. api1.example1.com
and api2.example2.com, no shared root), per-domain cookies are
not portable. You'll need custom middleware to bridge auth, or
token-based auth on the frontend side.
XML sitemap URLs
The links in the sitemap index (the t3://page?uid=current&type=…
sitemap-type links) resolve through frontendApiProxy; all other page
links use frontendBase. To make the index links use frontendBase
too, set:
Index links are matched by the sitemap page type. With a custom sitemap
typeNum, set headless.sitemap.type (default 1533906435) as well, or
the index links keep pointing at the API host:
headless:sitemap:type:'2400000000'
Copied!
Storage proxy (asset routing)
headless.storageProxy plus frontendFileApi in the site config
makes processed-file URLs (images, PDFs) point at the frontend's
proxy instead of the TYPO3 fileadmin. Useful when you want the
browser to fetch assets from the same origin as the SPA.
Form output decorators are documented with the rest of the form
integration in EXT:form; the default response shape and
every shipped lib.* object in Reference: TypoScript helpers.
TypoScript cObjects
EXT:headless registers a handful of new cObjects:
JSON
CONTENT_JSON
BOOL, FLOAT, INT
BOOL, FLOAT and INT take value and value. (stdWrap applied
to the value); every remaining top-level property is treated as stdWrap
configuration directly — noIndex.field = no_index works, there is no
stdWrap. sub-key like core TEXT. They return a real bool / float /
int and only work as fields inside a JSON cObject, not in generic
TypoScript.
JSON
Builds a JSON object inline.
lib.meta = JSON
lib.meta {
if.isTrue = 1
fields {
title = TEXT
title {
field = seo_title
stdWrap.ifEmpty.cObject = TEXT
stdWrap.ifEmpty.cObject {
field = title
}
}
robots {
fields {
noIndex = BOOL
noIndex.field = no_index
}
}
ogImage = TEXT
ogImage {
dataProcessing {
10 = FriendsOfTYPO3\Headless\DataProcessing\FilesProcessor
10 {
as = media
references.fieldName = og_image
processingConfiguration {
returnFlattenObject = 1
}
}
}
}
}
dataProcessing {
}
stdWrap {
}
}
Copied!
The JSON cObject understands these properties:
`if` — render the object only when the condition is met.
`fields` — array of child cObjects. Each field accepts:
intval / floatval / boolval — cast the result.
ifEmptyReturnNull — return null when the result is empty.
ifEmptyUnsetKey — drop the key when the result is empty.
source — nested field blocks only: output the block's fields
result under a different key. Ignored on plain fields, and ignored
when the block defines dataProcessing — that result is always
stored under the block's own key.
dataProcessing — run data processors (see lib.meta.ogImage).
`nullableFieldsIfEmpty` — comma list of field names to null out
when empty (bulk variant of ifEmptyReturnNull).
A field whose cObject is USER_INT (or whose output starts with an
<!--INT_SCRIPT placeholder) is wrapped in HeadlessUserInt markers,
so the uncacheable value is substituted into the JSON on output; with
ifEmptyReturnNull = 1 the nullable marker variant is used.
`dataProcessing` — replaces the fields output: the processors
run and the value registered under the last as key becomes the
object's content (e.g. MenuProcessor).
Set dataProcessingMerge to keep both.
`dataProcessingMerge` — merge instead of replace. With
dataProcessingMerge = 1 and fields present, the fields output is
kept and every processor result is added to it under the processor's
target (as) name; on a key collision the processor result wins.
lib.page = JSON
lib.page {
dataProcessingMerge = 1
fields {
title = TEXT
title.field = title
}
dataProcessing {
10 = FriendsOfTYPO3\Headless\DataProcessing\MenuProcessor
10.as = mainMenu
}
}
# {"title":"…","mainMenu":[…]} instead of the menu replacing the title
Copied!
The flag also works on a nested field block that defines both fields
and dataProcessing. Without the flag — or without fields — the
behaviour is unchanged: the processors replace the whole object.
`stdWrap` — stdWrap applied to the already-encoded JSON string.
CONTENT_JSON
Like core's CONTENT, but content elements are grouped by colPos
and JSON-encoded by default. All CONTENT options apply, plus four
JSON-specific extras:
merge
Run a second CONTENT_JSON query and merge the result into the
first — handy for the slide feature.
0 (default) groups by colPos — every rendered element must then
expose a colPos field, otherwise rendering throws a
RuntimeException. 1 returns a flat JSON array.
An empty result is encoded as {} with grouping enabled and as []
with doNotGroupByColPos = 1 — consumers must handle both types.
Order the colPos groups by the order of columns in the page's
backend layout instead of numerically.
returnSingleRow
Return the first matched element as a single object instead of an
array — for one-record queries. Only takes effect together with
doNotGroupByColPos = 1; with the default colPos grouping the flag
is silently ignored.
How to put your own data into the JSON response. Three flavours,
ordered by what's most common.
Custom content elements
Standard TYPO3 procedure — register a CE via TCA and add a frontend
template. For headless, the frontend template is TypoScript over
lib.contentElement (or lib.contentElementWithHeader when you want
the standard header block).
tt_content.demo >
tt_content.demo =< lib.contentElementWithHeader
tt_content.demo {
fields {
content {
fields {
demoField = TEXT
demoField.value = This is a demo content-element
bodytext = TEXT
bodytext {
field = bodytext
parseFunc =< lib.parseFunc_RTE
}
demoSubfields {
fields {
demoSubfield = TEXT
demoSubfield.value = Nested field
}
}
}
}
}
}
Copied!
fields can be nested to any depth — that's your JSON shape. Use
dataProcessing here just like in any other CE.
Internal Extbase plugins
Modern Extbase plugins register as content elements (CType)
rather than via the legacy list_type mechanism (deprecated in TYPO3
v13.4, removed in v14). Registration is the standard v14 pattern; only
the frontend template is headless-specific.
4. Wire the headless TypoScript per CType. TYPO3 ships an
EXTBASEPLUGIN cObject (since v12.3) — the modern, two-line
replacement for the old USER + Bootstrap->run form:
tt_content.myext_demoplugin =< lib.contentElementWithHeader
tt_content.myext_demoplugin {
fields {
content {
fields {
data = EXTBASEPLUGIN
data {
extensionName = MyExt
pluginName = DemoPlugin
settings {
test = TEXT
test.value = The demo is working
}
}
}
}
}
}
Copied!
The USER cObject form still works for back-compat, but the
EXTBASEPLUGIN cObject is the canonical pattern (it's what
ExtensionUtility::configurePlugin auto-generates internally).
vendorName is obsolete either way — plugin registration works on
FQCN controllers, extensionName + pluginName suffice.
controller is only needed if a plugin exposes multiple controllers;
otherwise the one passed to configurePlugin is used.
After a cache flush the JSON appears under the matching content
element:
Wire the foreign plugin's CType into tt_content.<ctype> and point
its templates at your own JSON Fluid templates. The CType signature
is whatever the foreign extension registers — for EXT:news it's
news_pi1.
If you're still maintaining a legacy plugin that registers via
list_type, switch the TypoScript key from tt_content.list
(with a CASE on list_type) to the per-CType pattern above.
The legacy list_type registration was removed in TYPO3 v14
(deprecated since v13.4), so v14-compatible plugins are always
CTypes.
Sometimes a CONTENT (or any other cObject) needs to land inside the
JSON output without being a CE — e.g. a list of related records on
every page. The trick is to make TYPO3's text output valid JSON
through stdWrap.split:
page.10.fields {
related = CONTENT
related {
table = tx_myextension_domain_model_things
select {
pidInList = this
}
renderObj = JSON
renderObj {
fields {
title = TEXT
title.field = title
link = TEXT
link.typolink.parameter.field = uid
link.typolink.returnLast = url
}
stdWrap.wrap = |###BREAK###
}
stdWrap {
innerWrap = [|]
split {
token = ###BREAK###
cObjNum = 1 |*|2|*| 3
1 { current = 1
stdWrap.wrap = | }
2 < .1
2.stdWrap.wrap = ,|
3 < .1
}
}
}
}
lib.meta exists only in the legacy (4.x-compatible) response — on the
default set the seo object is populated by the MetaHandler from
page properties instead. On the legacy set, replace lib.meta
wholesale on a specific route (e.g. news detail):
lib.meta.stdWrap.override.cObject = JSON
lib.meta.stdWrap.override.cObject {
if.isTrue.data = GP:tx_news_pi1|news
dataProcessing.10 = FriendsOfTYPO3\Headless\DataProcessing\DatabaseQueryProcessor
dataProcessing.10 {
table = tx_news_domain_model_news
uidInList.data = GP:tx_news_pi1|news
uidInList.intval = 1
pidInList = 0
max = 1
as = records
fields < lib.meta.fields
fields {
title = TEXT
title.field = title
subtitle = TEXT
subtitle.field = teaser
description = TEXT
description.field = bodytext
}
returnFlattenObject = 1
}
}
Copied!
Data Processors
Common behaviour
`appendData` — the menu, language-menu, gallery and files
processors share a destructive default: with appendData absent or
0 the processor removes data from the processed result and
strips data from every item in the target (as) array — for menus
recursively through all children. Set appendData = 1 to keep the
raw record data.
`as` defaults — when as is omitted, each processor falls back
to its default target name:
Processor
Default as
MenuProcessor
menu
LanguageMenuProcessor
languagemenu
GalleryProcessor
gallery
FilesProcessor
media
DatabaseQueryProcessor
records
RootSitesProcessor
sites
DatabaseQueryProcessor
It's the EXT:headless equivalent of TYPO3's own DatabaseQueryProcessor.
10 = FriendsOfTYPO3\Headless\DataProcessing\DatabaseQueryProcessor
10 {
table = tt_content
pidInList = 123
as = contents
fields {
header = TEXT
header {
field = header
}
bodytext = TEXT
bodytext {
field = bodytext
parseFunc =< lib.parseFunc_RTE
}
}
}
Copied!
Apart from the properties of TYPO3's DatabaseQueryProcessor (if, table, as and dataProcessing)
it provides the following properties:
fields — JSON-style field map applied to every row
overrideFields — same, merged over already-processed rows
returnFlattenObject (Default: 0) — return the single row directly
instead of an array
fields and overrideFields are rendered through the
JSON cObject, so every per-field option
documented there (intval, ifEmptyReturnNull, nested fields,
dataProcessing, …) works inside them.
ExtractPropertyProcessor
Extract a single (maybe nested) property from a given array.
Both as and key are required — the processor throws an
exception when either is missing. Its result replaces the entire
processed data: only [as => value] survives, everything else the
previous processors produced is discarded.
Example see below in section on FilesProcessor.
FilesProcessor
lib.meta.fields.ogImage = TEXT
lib.meta.fields.ogImage {
dataProcessing {
# Use the column 'og_image' to render an array with all relevant# information (such as the publicUrl)10 = FriendsOfTYPO3\Headless\DataProcessing\FilesProcessor
10.as = media
10.references.fieldName = og_image
10.processingConfiguration.returnFlattenObject = 1
# Extract only property 'publicUrl' from the above created array20 = FriendsOfTYPO3\Headless\DataProcessing\ExtractPropertyProcessor
20.key = media.publicUrl
20.as = media
}
}
Copied!
Sources besides references.fieldName: references (list of
sys_file_reference uids, optionally references.table), files
(sys_file uids), collections (file collection uids), folders
(combined-identifier folder paths, folders.recursive = 1 to descend).
Further options: sorting (file property, sorting.direction),
appendData (1 keeps the processed record data alongside the files;
absent or 0 — the default — strips data from the result, see
Common behaviour) and the processingConfiguration block described
in Image rendering. Default as is media.
FlexFormProcessor
This DataProcessor allows to process a flexform field such as tt_content.pi_flexform
and optionally override its property values.
fieldName defaults to pi_flexform. When as is omitted, the
processed flexform is written back in place — into
data.<fieldName>, or the top-level <fieldName> key when the value
lives there.
10 = FriendsOfTYPO3\Headless\DataProcessing\FlexFormProcessor
10 {
fieldName = pi_flexform
as = flexform
overrideFields {
fieldA = TEXT
fieldA {
value = 123
}
}
}
It's the EXT:headless equivalent of TYPO3's LanguageMenuProcessor —
same output enriched for JSON consumption, with the raw data stripped
by default. Allowed options: if, languages, as (default
languagemenu), addQueryString and appendData (see
Common behaviour). Unknown configuration keys throw an exception.
It's the EXT:headless equivalent of TYPO3's MenuProcessor.
On top of the core options it provides (configuration keys are
whitelisted — unknown keys throw an exception):
appendData — keep the page record on every menu item under
data; by default it is stripped recursively, including all
children (see Common behaviour).
additionalFields — comma list of record fields copied from each
item's page record onto the item itself, recursively into
children. Works without appendData.
overwriteMenuLevelConfig. — merged into the TMENU configuration
of every menu level.
overwriteMenuConfig. — merged into the whole generated HMENU
configuration.
Default as is menu. Each menu item is rendered as:
children is only present when the item has sub-items. With
appendData = 1 every item additionally keeps the full page record
under data; fields listed in additionalFields appear as extra
top-level keys on the item.
Have a look at lib.breadcrumbs for example (all shipped TypoScript
uses the registered short identifiers — headless-menu,
headless-files, headless-gallery, headless-database-query,
headless-language-menu, headless-root-sites, headless-flex-form,
headless-extract-property — the FQCNs work too):
10 = FriendsOfTYPO3\Headless\DataProcessing\RootSitesProcessor
10 {
as = sites
# allow to override provider of data for output processor, if empty defaults to FriendsOfTYPO3\Headless\DataProcessing\RootSiteProcessing\SiteProvider# your-class implementing FriendsOfTYPO3\Headless\DataProcessing\RootSiteProcessing\SiteProviderInterface# example value: Vendor\Project\RootSiteProcessing\CustomSiteProvider
siteProvider =
# allow to override output of processor, if empty defaults to FriendsOfTYPO3\Headless\DataProcessing\RootSiteProcessing\SiteSchema# your-class implementing FriendsOfTYPO3\Headless\DataProcessing\RootSiteProcessing\SiteSchemaInterface# example value: Vendor\Project\RootSiteProcessing\CustomSiteSchema
siteSchema =
# provider configuration, if empty defaults to 'sorting' field from pages table# example value = custom_sorting
sortingField =
# if empty defaults to sort by "sorting" field from `pages` table# your-class implementing FriendsOfTYPO3\Headless\DataProcessing\RootSiteProcessing\SiteSortingInterface# example value: Vendor\Project\RootSiteProcessing\CustomSorting
sortingImplementation =
# list of uid of root pages should be returned, i.e. you have 5 root pages(1,2,3,4,5), but two (4,5) of not ready to display, so you can hide it# example value = 1,2,3
allowedSites =
# automatically fetch root sites from another page/separator and filter sites yaml configs by returned list from database# very useful when you have multi site setup in one instance.# example value = 1
sitesFromPid =
# if empty defaults to uid,title,sorting - list of columns to fetch from database and provided for SiteSchema/DomainSchema to use# example value = uid,title,sorting
dbColumns =
# if empty defaults to "title" field from pages table, get site name from database# example value = your-custom-field-from-pages-table
titleField =
}
Copied!
Events you can listen to
EXT:headless dispatches PSR-14 events where adjusting the JSON
output is most useful. For the full signature of each event class
see Reference: Events.
FriendsOfTYPO3HeadlessEventEnrichFileDataEvent — fired in
FileUtility::process() after a file's default properties have
been collected, before crop-variants and autogenerate run.
Use it to add custom fields (focus point, alt text from another
source, signed CDN URL) to every file in the response. Two caveats:
process() recurses for crop variants and autogenerate derivatives,
so the event fires for each of them too; and a configured
properties.includeOnly filter is applied after the event —
list your custom keys there or they are dropped.
FriendsOfTYPO3HeadlessEventFileDataAfterCropVariantProcessingEvent
— fired once per file after all crop variants are processed (also
when there are none). Annotate or rewrite the complete file payload
including cropVariants.
For customising the JSON redirect envelope, listen to the core
TYPO3CMSRedirectsEventRedirectWasHitEvent — see
EXT:redirects.
Listener registration
The #[AsEventListener] attribute is the idiomatic TYPO3 v14 form —
no YAML needed:
properties.byType (0|1): Allows filter file properties by type (i.e. do not return video properties on images)
properties.defaultFieldsByType (coma separated list of fields): Default fields for when enabled option properties.byType
properties.defaultImageFields (coma separated list of fields): Default fields for image type when enabled option properties.byType
properties.defaultVideoFields (coma separated list of fields): Default fields for video type when enabled option properties.byType
properties.includeOnly (string, comma separated): Configure what file properties to return
properties.flatten (0|1): Flatten nested properties (dimensions array) to use with properties.includeOnly
returnFlattenObject: without that flag an array of (multiple) images is rendered. Set this if you're only rendering 1 image and want to reduce nesting.
delayProcessing: can be used to skip processing of images (and have them simply collected with the FilesProcessor), in order to have them processed by the next processor in line (which is generally GalleryProcessor).
fileExtension: can be used to convert the images to any desired format, e.g. webp.
autogenerate:
`retina2x`: set this to render an additional image URI in high quality (200%).
lqip: set this to render an additional image URI with low quality (10%).
* also custom defined size & file formats see example below
10 = FriendsOfTYPO3\Headless\DataProcessing\FilesProcessor
10 {
...
processingConfiguration {
# (1 by default = legacy output; set 0 for the new simplified format)
legacyReturn = 0
# Return whole LinkResult object instead simple url
linkResult = 1
# check if we need to conditionally check if we should generate crop variants
conditionalCropVariant = 1
# Generate cacheBusting urls for images and video files
cacheBusting = 1
properties {
# return props by mimeType
byType = 1
# return only properties defined below
includeOnly = alternative,width,height
# you can also alias fields# includeOnly = alternative as alt,width,height# with includeOnly you can use option `flatten` to flatten dimensions array
flatten = 1
}
}
}
Copied!
GalleryProcessor
Configuration
The rendering configuration is set directly on the processor: all
options of the processingConfiguration block documented above
(legacyReturn, fileExtension, cropVariant, …) are read from the
processor's top-level configuration — there is no
processingConfiguration. sub-key.
Default as is gallery. The processor also supports appendData
(1 keeps the raw record data; absent or 0 — the default — strips
data from the result) and cropVariant to select the crop variant
used for the width/height calculation (default default).
maxGalleryWidth: set to the core constant {$styles.content.textmedia.maxW}
maxGalleryWidthInText: set to the core constant {$styles.content.textmedia.maxWInText}
columnSpacing: set to the core constant {$styles.content.textmedia.columnSpacing}
borderWidth: set to the core constant {$styles.content.textmedia.borderWidth}
borderPadding: set to the core constant {$styles.content.textmedia.borderPadding}
autogenerate `retina2x`
lqip
* custom keys with factor / fileExtension — same syntax as in the
FilesProcessor example above
Snippets
Drop-in recipes for the most common questions.
Customise file output (URL signing, focus point, etc.)
Implement
FriendsOfTYPO3HeadlessFormDecoratorDefinitionDecoratorInterface
or extend AbstractFormDefinitionDecorator, then point the form's
renderingOptions.formDecorator at it. See EXT:form.
Add a JSON field to every page response
Extend the page object in TypoScript (loaded after the set):
page.10.fields {
myField = TEXT
myField.value = static value
myDynamic = TEXT
myDynamic.data = TSFE:id
}
Copied!
Read the current site's frontend URL
Inject FriendsOfTYPO3HeadlessUtilityHeadlessFrontendUrlInterface
and call withRequest($request)->getFrontendUrl(). Returns the
configured frontendBase for the active site and language.
Detect headless mode in your own code
Inject FriendsOfTYPO3HeadlessUtilityHeadlessModeInterface and
call isEnabledFor($request). FULL mode always returns true; MIXED
mode only when the request's first Accept header value is exactly
application/json (the strict match is the API contract).
Encode JSON safely from your own code
Inject FriendsOfTYPO3HeadlessJsonJsonEncoderInterface instead of
calling json_encode directly. It applies HTML-attribute-safe hex
flags (JSON_HEX_APOS | JSON_HEX_AMP) and the headless.prettyPrint
feature flag for free. Note that it does not throw on encoding
failures — a JsonException is caught, logged as critical, and the
string "[]" is returned instead.
Decode nested JSON inside an array
Inject FriendsOfTYPO3HeadlessJsonJsonDecoderInterface and call
decode($array). Any string value that looks like JSON gets decoded
(nested structures come back as stdClass objects, the outer array
stays an array). Useful when stitching multiple JSON cObjects
together.
Process a file with the same shape as the default response
Inject FriendsOfTYPO3HeadlessUtilityFileUtilityInterface and
call process($fileReference, ProcessingConfiguration::fromOptions($opts)).
The output matches what content elements receive — your custom
endpoints stay shape-compatible with the rest of the JSON API.
EXT:form
If EXT:form is enabled in the TYPO3 instance, EXT:headless produces
JSON form definitions instead of HTML. Forms designed in the form
editor work out of the box; this page documents the headless-specific
hooks that help frontend developers.
Note
On a MIXED-mode site (headless: 2) the JSON form definition — and
every submission — requires exactly Accept: application/json as
the first Accept header value; anything else renders HTML.
Configuration (YAML)
All options live in the form's YAML configuration file.
i18n strings
Add translated UI strings (button labels, help messages) directly in
the form root. They land in the response's i18n section.
i18n:identifier:'i18n'properties:someButtonLabel:'Submit or Cancel'someHelpMessage:'You need to fill out this form'requiredFields:'These fields are required'
Copied!
Strings are translated through the standard TYPO3 XLF pipeline.
Translation keys resolve against the form's original identifier
(kept in renderingOptions._originalIdentifier), not the runtime
identifier that headless suffixes with the content element uid — key
your XLF entries on the identifier from the form YAML. A forced
locale can be passed to FormTranslationService::translateElementValue()
as its optional fourth parameter.
On key collision, i18n.properties entries win over the
renderingOptions.submitButtonLabel shortcut — both end up in the
response's i18n object, YAML properties last.
Form decorator
Headless ships FormDefinitionDecorator as the default. Override per
form via renderingOptions.formDecorator:
TYPO3 v14.2 allows RTE content in form element labels and StaticText
elements. To ship that HTML safely in the JSON definition, opt in per form
to the shipped decorator:
HTML-carrying labels and static texts are then run through
lib.parseFunc_RTE (resolving t3:// links), falling back to
lib.parseFunc_links and finally to the plain HTML sanitizer when
neither lib exists, and flagged with labelFormat/textFormat: html
so the frontend knows to render them as markup. The decorator also
processes api.actionAfterSuccess.message the same way and flags it
with messageFormat: html.
Validator error codes
Per validator, set errorMessage to a TYPO3 XLF error code — the
default form translation files resolve it:
When the headless decorator sees FERegularExpression, it replaces
options.regularExpression with that value as-is in the JSON
response — the {expression, flags} pair maps directly to JavaScript's
new RegExp(expression, flags).
Custom options (dynamic select/radio/checkbox)
Implement FriendsOfTYPO3HeadlessFormCustomOptionsInterface and
point the field at it:
CountryOptions::get() is called per render and its return value
replaces the field's options. Although the interface only declares
get(): array, implementations are instantiated with four constructor
arguments — ($field, $formFields, $identifier, $formRuntime): the
field's definition array, all fields of the current page, the runtime
form identifier and the FormRuntime — so options can depend on the
surrounding form state.
If your custom form type isn't a standard one (so the frontend
wouldn't know what to render), override the type sent to the
frontend with FEOverrideType:
The standard RedirectFinisher issues a real HTTP redirect. In
headless mode that breaks the SPA flow. Use the JsonRedirect
finisher instead — it puts the redirect target into
api.actionAfterSuccess and lets the frontend decide what to do.
Since 5.0 the finisher is registered out of the box (form set
friendsoftypo3/headless-form,
EXT:headless/Configuration/Form/Headless/config.yaml) — use it
directly in your form definition:
finishers:-identifier:JsonRedirectoptions:pageUid:'2'message:'Thanks! You will be redirected shortly.'
Copied!
On success it emits { redirectUrl, statusCode, message }
(message defaults to null, statusCode to 303 and is also an
option). The redirectUrl is made relative only when its host already
equals the site's frontendBase host; otherwise the absolute
TYPO3-host URL is returned unchanged — it is not rewritten to
frontendBase. It does not redirect by itself. Further
options: additionalParameters (appended to the target URL) and
sameSiteOnly — with it, a pageUid outside the current site (or an
unresolvable one) falls back to the finisher's default target
(pageUid: 1) instead of being used.
Submitting the form
POST the form to the content element's link value — headless builds
it with tx_form_formframework[action]=perform and
tx_form_formframework[controller]=FormFrontend already appended.
Nothing inside the form definition itself is a valid submit target.
Every field is submitted under its exact name,
tx_form_formframework[<formId>][<identifier>]. Besides the visible
fields, the JSON elements contain Hidden elements that must be
posted back verbatim:
__state — the HMAC-protected form state; round-trip it unchanged.
__currentPage — the page index being submitted.
__trustedProperties — the extbase property-mapping token,
generated for the exact set of listed field names: omitting any
field (the honeypot included) fails property mapping.
__session — present only once the form is performing (after the
first POST); echo it back on subsequent steps.
Honeypot
When renderingOptions.honeypot.enable is true, an extra field with a
session-random identifier appears in elements and its name is
baked into __trustedProperties. Render it hidden from humans and
submit it empty — filling or omitting it fails the submission.
When using a custom honeypot element, its type must match
renderingOptions.honeypot.formElementToUse (default Honeypot) for
headless to expose it correctly in the JSON definition.
FriendsOfTYPO3HeadlessFormDecoratorAbstractFormDefinitionDecorator
— base class with hooks to override per-element or whole-form
output.
FriendsOfTYPO3HeadlessFormDecoratorDefinitionDecoratorInterface
— the contract a custom decorator implements.
Default output (FormDefinitionDecorator) — the decorator returns the
bare definition; in the page response it sits under the content
element's content.form key:
elements is abbreviated here — in a real response it lists the
current page's fields plus the hidden round-trip fields described in
Submitting the form above.
Subclass AbstractFormDefinitionDecorator if you only need to tweak
one element type or one form root field; implement
DefinitionDecoratorInterface directly if you want full control.
Attach via renderingOptions.formDecorator as shown above.
EXT:felogin
EXT:felogin works out of the box with headless. The headless XClass
on LoginController swaps the HTML view for a JSON response that
includes the form definition, the login status and any redirect
target.
Setup
Standard felogin setup — install (composer require typo3/cms-felogin),
drop a login plugin onto a page, configure storage pages in plugin
settings.
You can test the flow without a frontend with curl. TYPO3 v14 only
evaluates credentials that are accompanied by a valid, nonce-signed
__RequestToken, so a login is always two requests: fetch the form
first, then post it back together with its cookies.
# 1) Fetch the login form; store the typo3nonce_* cookie
curl -s -c cookies.txt -H 'Accept: application/json' \
https://api.example.com/login-page
# 2) Post credentials plus EVERY hidden field returned in# form.elements, sending the cookies back — the __RequestToken# is only valid together with its typo3nonce_* cookie
curl -i -b cookies.txt -c cookies.txt -X POST \
-H 'Accept: application/json' \
https://api.example.com/login-page \
--data-urlencode 'user=joe' \
--data-urlencode 'pass=secret' \
--data-urlencode 'logintype=login' \
--data-urlencode 'pid=<value from form.elements>' \
--data-urlencode '__RequestToken=<value from form.elements>'# …plus all remaining hidden fields from form.elements, verbatim
Copied!
A successful login responds with a set-cookie header carrying the
session cookie (fe_typo_user). On failure the JSON status flips
to failure and message carries the rendered header/text for the
failure state.
Note
On a MIXED-mode site (headless: 2) every request shown here must
send exactly Accept: application/json as the first Accept header
value. Lists such as application/json, text/plain, */* (the
axios/fetch default) or a ;charset= suffix fall back to HTML
rendering.
JSON response shape
The plugin output sits under the content element's content.data key:
tx_felogin_login[__trustedProperties]: the extbase property
mapping token. It is emitted with the plugin prefix and must be
posted back under that prefixed name.
__RequestToken: a TYPO3 RequestToken with scope
core/user-auth/fe, signed against the typo3nonce_* cookie set
on the GET request. This is the one field that stays
unprefixed.
Without a complete, unmodified set of these fields the login is
rejected before credentials are even checked.
When a redirect target applies, the plugin payload is replaced
entirely by { "redirectUrl": "…", "statusCode": 303, "status": "…" }
— the frontend performs the redirect itself.
Cookies & cross-domain
The session cookie is scoped to the API domain by default. If your
frontend lives on a different host, set a shared
$GLOBALS['TYPO3_CONF_VARS']['FE']['cookieDomain'], or enable the
headless.cookieDomainPerSite feature flag to derive it per site —
see Multi-Site & URL Configuration.
Headless ships LoginConfirmedEventListener that decorates the JSON
view with status = success. To run your own logic at the same
point, listen to TYPO3 core's
TYPO3CMSFrontendLoginEventLoginConfirmedEvent — see
Events you can listen to for listener registration.
EXT:redirects
Install EXT:redirects (composer require typo3/cms-redirects) and
the standard redirect manager works as usual. EXT:headless then
automatically swaps the relevant frontend middlewares so a matched
redirect surfaces to a headless frontend as JSON rather than a 30x
response — no feature flag required.
Matched redirects from the redirect manager are turned into JSON by the
headless/RedirectWasHit event listener (see below) — the core
redirecthandler middleware stays in place.
Note
The middleware swap only registers when EXT:redirects is
installed. Without it, all redirects — including plain shortcut
and mount-point pages — are served as real HTTP 30x responses
even in headless mode. The same applies per request in MIXED mode:
without exactly Accept: application/json as the first Accept
header value, redirects degrade to real 30x responses.
Requests for the headless page-content type (?type=834) bypass
shortcut and mount-point redirect handling entirely and are resolved
downstream instead.
redirectUrl is run through UrlUtility::prepareRelativeUrlIfPossible(),
so internal targets come back as relative paths (/new-target) when
they land on the same frontend host.
The HTTP response itself is always 200 — statusCode tells the
frontend which redirect to perform, and the frontend must apply the
usual method semantics itself (301/302 may switch to GET,
307/308 preserve the request method). Its value depends on the
source:
redirect-manager records: the record's configured target status
code;
shortcut/mount-point and site-base redirects: the HTTP code the core
middleware would have sent (typically 307).
Customising the redirect
The JSON envelope is built by
FriendsOfTYPO3HeadlessEventListenerHeadlessRedirectResponseListener,
which listens to the core TYPO3CMSRedirectsEventRedirectWasHitEvent
(identifier headless/RedirectWasHit). Register your own listener for
the same event after it and replace the response:
Page targets carrying Extbase plugin parameters in the typolink
additionalParams segment are rebuilt through the site router by
FriendsOfTYPO3HeadlessRedirectsTargetUrlResolver (the core
RedirectService drops that segment unless "keep query parameters"
is enabled). Override or extend that service via Services.yaml
when you need different target URL resolution.
Short URLs & QR codes (5.x)
The "Short URLs" and "QR Codes" backend modules of EXT:redirects show
source URLs on the TYPO3 host — useless when the public site lives on the
frontend domain. With headless, both modules (and the QR-code/short-URL
fields in redirect records) resolve source URLs against the site's
frontendBase instead, so copied links and scanned codes land on the
public frontend. No configuration needed beyond frontendBase; resolution
logic can be replaced by overriding
FriendsOfTYPO3HeadlessRedirectsSourceUrlResolver via Services.yaml.
Reference: TypoScript helpers
lib.* keys provided by the headless TypoScript. Override any
of them from your site setup to customise the JSON response.
Key
Purpose
lib.headlessPage
Bare PAGE skeleton (JSON headers, cache config, an
empty 10 = JSON). The response page object is
created from it via parse-time copy
(page < lib.headlessPage) and the fields live on
page.10.fields — override those, not the lib.
lib.content
CONTENT_JSON over tt_content grouped by colPos.
Referenced by page.10.fields.content.
lib.contentElement
Base JSON shape for every content element.
`lib.contentElementWithHeade
lib.contentElement plus the standard header block.
Use this as the base for custom CEs that should keep
the title/subtitle/header link block.
lib.galleryContentElement
lib.contentElementWithHeader plus the shared
image-gallery block (image, textpic, textmedia).
lib.meta
Legacy SEO meta object (legacy set only) — title,
description, keywords, robots, ogImage. The default
set uses the seo object instead.
lib.contentElement.fields.myField = TEXT
lib.contentElement.fields.myField.field = my_field
Copied!
Read the shipped TypoScript to see the defaults — every override above
just adds to or replaces values inside the same structure.
Files
The default TypoScript lives in
EXT:headless/Configuration/TypoScript/ and is loaded by the site sets
under EXT:headless/Configuration/Sets/. Subdirectories:
Helpers/ — lib.renderChildren, lib.parseFunc_links. (lib.parseFunc and
lib.parseFunc_RTE are provided globally by EXT:frontend since TYPO3 v13.2.)
Configuration/ — language/backend editor wiring.
Legacy/ — 4.x-only additions: lib.meta, lib.categories (page and
content element variants) and the full legacy page object. Loaded by the
legacy set, the static template and mixed mode — never by the default set.
PageResponse.typoscript — the trimmed default page object, loaded only
by the default set.
The default set
friendsoftypo3/headless (label "TYPO3 Headless",
EXT:headless/Configuration/Sets/Headless/) ships a trimmed response by
default. The legacy set friendsoftypo3/headless-legacy (label
"TYPO3 Headless Legacy (4.x)") declares the default set as a dependency and
only loads the Legacy/ delta on top: the full 4.x page object, lib.meta
and the categories fields. It is the quick upgrade path for existing
installs.
Sites not (yet) on sets can select the equivalent sys_template statics
instead: "Headless" (EXT:headless/Configuration/TypoScript/Headless, the
trimmed default response), "Headless Legacy (4.x)"
(EXT:headless/Configuration/TypoScript, the full 4.x response) and
"Headless - Mixed mode JSON response"
(EXT:headless/Configuration/TypoScript/Mixed). 4.x records keep working
unchanged — the 4.x "Headless" static stored the path that is now registered
as the legacy item, and the mixed static kept its path. Site packages that
@importEXT:headless/Configuration/TypoScript/setup.typoscript directly
also still get the full 4.x response. Note that a root sys_template record
with "Clear" flags wipes all set-provided TypoScript — delete the record when
switching a site to sets.
The default set imports the same lib.* helpers as the legacy setup —
everything in the table above still applies — with two exceptions: lib.meta
and lib.categories are not loaded, and lib.contentElement does not define
a categories field at all (the legacy delta re-adds it, which restores the
field on every shipped content element at once — all CEs reference
lib.contentElement at render time via =<).
The seo object is populated at runtime: the TypoScript only ships a
placeholder with seo.fields.title, which the
AfterCacheableContentIsGenerated listener detects before handing the
response to the MetaHandler. Do not remove the placeholder — without a
seo.title key in the rendered JSON, no seo data is generated at all.
page.meta. (e.g. the generator entry) is likewise still read by the
MetaHandler to build seo.meta.
The additional page types of the legacy setup are available unchanged:
initialData (typeNum = 834) and headless_domains (typeNum = 835).
Differences to the legacy set
Removed key
Replacement / how to get it back
page.10.fields.meta
Deprecated duplicate of seo — use seo, which the
MetaHandler fills from the same page properties
(title, description, OpenGraph/Twitter images, …).
page.10.fields.categories
Opt-in; see "Content element categories" in the
Configuration chapter (covers page categories too).
`lib.contentElement.fields.categorie
Opt-in; same Configuration chapter section.
plugin.tx_headless.staticTemplate
Not set by this set. Only relevant for third-party
extensions probing it to detect headless rendering.
Every removal saves work on uncached renders: meta cost two extra
FilesProcessor runs per page, and the categories fields cost one
sys_category join per page respectively per content element — while
returning an empty string unless the storage pid was reconfigured.
Reference: Events
PSR-14 events dispatched by EXT:headless. For registration syntax
and a worked example see Events you can listen to.
EnrichFileDataEvent
FriendsOfTYPO3HeadlessEventEnrichFileDataEvent
Fired by FileUtility::process() once a file's default properties
have been collected, before crop variants and autogenerate derive
extra fields. The canonical hook for adding custom file fields
(signed URLs, focus point, alt-text overrides) to every file in
the response.
Fired once per file, after all crop variants have been processed (and
also when the file has none). Use it to post-process the complete file
payload including its cropVariants. The event carries the base
per-file ProcessingConfiguration that was passed into
processCropVariants() — the per-variant configurations derived
inside the loop never reach the event.
Method
Returns
getProcessedFile(): array
The whole processed-file payload.
setProcessedFile(array): void
Replace it.
getOriginal(): FileInterface
The original file reference.
getProcessingConfiguration()
The per-file ProcessingConfiguration
(not per-variant).
Core events you may also care about
These come from TYPO3 core but are commonly used alongside headless:
TYPO3CMSRedirectsEventRedirectWasHitEvent — headless's
HeadlessRedirectResponseListener (identifier
headless/RedirectWasHit) builds the JSON redirect envelope from
this event. Register your own listener after it to replace the
response — see EXT:redirects.
TYPO3CMSFrontendEventAfterCacheableContentIsGeneratedEvent —
the headless AfterCacheableContentIsGeneratedListener listens to
this to bake SEO meta tags into the JSON response. Listen too if
you need to post-process the full encoded JSON before it goes into
the page cache.
TYPO3CMSFrontendEventModifyHrefLangTagsEvent — headless's
HeadlessHreflangGeneratorListener rewrites hreflang URLs through
UrlUtility.
TYPO3CMSFrontendEventAfterLinkIsGeneratedEvent — headless's
AfterLinkIsGeneratedListener rewrites typolink URLs through
UrlUtility.
TYPO3CMSCoreRoutingEventAfterPageUriGeneratedEvent —
headless's AfterPageUriGeneratedListener rewrites the host on
URIs produced by PageRouter::generateUri() — BE "view" links,
workspace split-screen previews and similar admin tooling land on
the configured frontendBase. It acts only on backend requests
and only when headless mode is enabled for the resolved site.
TYPO3CMSCoreResourceEventGeneratePublicUrlForResourceEvent —
headless's ProxyResourcePublicUrlListener (identifier
headless/ProxyResourcePublicUrl) rewrites public URLs of local,
publicly capable storages through the configured storage proxy.
Registered only when the headless.storageProxy feature flag is
enabled, and acts only on frontend requests with headless mode on.
TYPO3CMSFrontendLoginEventLoginConfirmedEvent — headless
assigns the success status to the JSON login response.
Quick lookup. For per-flag descriptions and usage see
Configuration.
Active in 5.x
Flag
Effect
headless.storageProxy
Route processed file URLs through frontendFileApi.
headless.elementBodyResponse
On POST/PUT/DELETE, return just the element
matching responseElementId from the body.
`headless.overrideFluidTemplates
Swap ViewFactoryInterface for
HeadlessViewFactory. Required to render
raw-PHP templates per view.
headless.cookieDomainPerSite
Per-site cookieDomain injection middleware.
headless.assetsCacheBusting
Append ?<mtime> to processed-file URLs.
headless.prettyPrint
JSON_PRETTY_PRINT on every encoder output.
Availability by version
Flag
2.x
3.x
4.x
5.x
FrontendBaseUrlInPagePreview
available
removed
removed
removed
headless.frontendUrls
>= 2.5
available
removed
removed
headless.storageProxy
>= 2.4
available
available
available
headless.redirectMiddlewares
>= 2.5
available
available
removed (auto-on when EXT:redirects is installed)
headless.nextMajor
>= 2.2
currently not used
currently not used
removed
headless.elementBodyResponse
>= 2.6
available
available
available
headless.simplifiedLinkTarget
>= 2.6
removed
not available
not available
headless.jsonViewModule
not available
>= 3.0
>= 3.0
removed (module discontinued)
headless.workspaces
not available
>= 3.1
removed (auto-on with EXT:workspaces)
removed (works without a flag)
headless.pageTitleProviders
not available
not available
>= 4.2.3 <= 4.4
removed
headless.overrideFluidTemplates
not available
not available
available
available (reworked, see UPGRADE.md)
headless.cookieDomainPerSite
not available
not available
available
available
headless.assetsCacheBusting
not available
not available
available
available
headless.prettyPrint
not available
not available
available
available
Reference: Interfaces
Public interfaces in the `FriendsOfTYPO3Headless` namespace. Inject
these rather than the concrete classes — it keeps your code testable
and DI-friendly.
Interface
What it gives you
UtilityHeadlessModeInterface
Detect / switch headless mode for a request. Methods:
isEnabled(), isEnabledFor($request),
withRequest($request),
overrideBackendRequestBySite($site, $language).
UtilityHeadlessFrontendUrlInterface
URL rewriting from backend to frontend. Methods:
withSite(), withRequest(),
getFrontendUrl(), getFrontendUrlWithSite(),
getFrontendUrlForPage(), getProxyUrl(),
getStorageProxyUrl(), resolveKey(),
prepareRelativeUrlIfPossible().
(withLanguage() exists only on the concrete
UrlUtility.)
UtilityFileUtilityInterface
File / image rendering. Methods: setRequest(),
processFile(), process(), processImageFile(),
processCropVariants(), getAbsoluteUrl(),
getErrors(). process() returns the same shape that
ships in content.*.media by default.
JsonJsonEncoderInterface
encode($data, $options = 0). Wraps json_encode with
HTML-safe flags and the headless.prettyPrint feature
flag. Does not throw on invalid data: encoding
errors are caught, logged as critical, and "[]" is
returned.
JsonJsonDecoderInterface
decode(array) recursively unwraps JSON strings nested
inside arrays. isJson($mixed) detects JSON by fully
decoding the value after cheap pre-checks.
SeoMetaHandlerInterface
process($request, $content) — runs the SEO meta-tag
pipeline (page title, meta registry, hreflang) and
merges it under content.seo.
Dynamic options for a form select/radio/checkbox. See
EXT:form.
FormDecoratorDefinitionDecoratorInterface
Custom JSON shape for an EXT:form definition.
DI wiring
FileUtilityInterface, HeadlessFrontendUrlInterface,
JsonEncoderInterface, JsonDecoderInterface and
MetaHandlerInterface are aliased in the extension's Services.php;
HeadlessModeInterface via #[AsAlias] on HeadlessMode. Plain
constructor injection works for those:
The remaining interfaces (RootSiteProcessing*,
FormCustomOptionsInterface, DefinitionDecoratorInterface) are
contracts you implement and hand to the extension via TypoScript or
form YAML — they carry no container alias.
UrlUtility and HeadlessMode are shared services; state safety comes
from their wither methods (withRequest() / withSite() return
clones), not from share: false.
Using EXT:felogin with the headless extension follows the standard setup as detailed in the felogin documentation; the headless-specific JSON output is described in EXT:felogin.
To test the login without a frontend, first GET the login page to obtain the nonce-signed __RequestToken hidden field and its typo3nonce_* cookie, then POST the credentials together with both — if the response contains a set-cookie header for the session, the login was successful. See EXT:felogin for the full flow.
Does EXT:headless work with other extensions?
Yes, the output of virtually any extension can be rendered into the JSON response. For detailed information, refer to the integration of external plugins section of this documentation. Additionally, you can review the code of headless_news as an example of how this integration works.
How to handle redirects in a headless setup?
The frontend application performs the actual redirect: a matched redirect from
EXT:redirects is returned as JSON ({ "redirectUrl": "...", "statusCode": 301 })
instead of an HTTP 30x response.
On headless 5.x (TYPO3 v14) this works automatically as soon as EXT:redirects
is installed — no feature flag. See EXT:redirects for the response
shape and customisation.
On headless 4.x and below, enable it with the headless.redirectMiddlewares
feature flag:
Can I use custom fields or content elements with EXT:headless?
Yes, EXT:headless supports the customization of JSON responses using TypoScript. You can define custom fields or content elements and extend the JSON output to include these customizations.
For example, to add a custom field, you can modify the TypoScript setup like this:
lib.customField = TEXT
lib.customField.value = My Custom Field
Copied!
This value can then be included in the JSON response as needed.
How to configure language and translation settings?
EXT:headless fully supports TYPO3's language and translation configurations, including fallback settings. To configure languages, follow these steps:
Define your languages in the site configuration YAML file.
Ensure that your content elements and page properties are translated according to TYPO3's multilingual guidelines.
Enable the headless mode in your site configuration's YAML file:
headless:1
Copied!
Send the responseElementId field with the ID of the plugin in the body of the plugin data during requests.
On a mixed-mode site (headless: 2), the request must additionally carry the exact Accept: application/json header, or the middleware does not act.
For example, a POST request might look like this:
POST https://example.tld/path-to-form-plugin
Content-Type: application/x-www-form-urlencoded
responseElementId=#ELEMENT_ID#&tx_form_formframework[email]=email&tx_form_formframework[name]=test...
Copied!
To handle nested elements, use the responseElementRecursive flag:
POST https://example.tld/path-to-form-plugin
Content-Type: application/x-www-form-urlencoded
responseElementId=#ELEMENT_ID#&responseElementRecursive=1&tx_form_formframework[email]=email&tx_form_formframework[name]=test...
Copied!
Sitemap
Reference to the headline
Copy and freely share the link
This link target has no permanent anchor assigned.The link below can be used, but is prone to change if the page gets moved.