Consuming the API
A frontend renders the response by walking the JSON: page, backend layout,
columns, content elements by type. The examples on this page use nuxt-typo3
(@t3headless/, Nuxt 3/4), which maps the response to Vue
components by convention. Request mechanics (headers, cookies, CORS) are in
Making requests.
Fetching a page
A page is requested at its own URL on the API host, language prefix included. The keys a frontend uses:
id,type,slug— record uid, page type (Standard,Shortcut, …), path.appearance.— which columns exist;backend Layout appearance.— the page layout selected in the page properties.layout content— content elements grouped by column:col,Pos0 col, …Pos1 seo— title, meta tags, hreflang links,<html>/<body>attributes (Reference: TypoScript helpers).breadcrumbs,i18n,media— rootline, language list, page media.
nuxt-typo3 handles this in a catch-all route. use fetches the page for
the current route from typo3., keeps it in page and derives
the layout names:
<!-- pages/[...slug].vue -->
<template>
<NuxtLayout :name="frontendLayout">
<T3BackendLayout
v-if="pageData?.content"
:name="backendLayout"
:content="pageData.content"
/>
</NuxtLayout>
</template>
<script setup lang="ts">
const { headData, pageData, backendLayout, frontendLayout } = await useT3Page()
useHead(headData)
definePageMeta({
layout: false,
// for nuxt4 also add t3Page: true
})
</script>
Call use once per page; elsewhere read page from use
or use use. use fetches
any other page on demand.
Initial data (?type=834)
Site-wide data that does not change from page to page is kept out of the
page response and served once per language by ?type=834:
navigation— the page tree from the site root, seven levels (MenuProcessor items).i18n— the language list, identical to the one in the page response.user.—logged truefor a logged-in frontend user; the key is absent otherwise.backend— for a logged-in backend user, the URLs of the backend edit forms for the current page and its records.Editor
Abbreviated response (user and backend appear only for logged-in
users):
{
"navigation": [
{
"title": "Home", "link": "/", "target": "", "active": 1, "current": 1,
"spacer": 0, "hasSubpages": 1,
"children": [
{ "title": "About", "link": "/about", "target": "", "active": 0,
"current": 0, "spacer": 0, "hasSubpages": 0 }
]
}
],
"i18n": [
{ "languageId": 0, "locale": "en-US", "title": "English",
"navigationTitle": "English", "hreflang": "en-US", "direction": "ltr",
"flag": "us", "link": "/", "active": 1, "current": 1, "available": 1 },
{ "languageId": 1, "locale": "de-DE", "title": "Deutsch",
"navigationTitle": "Deutsch", "hreflang": "de-DE", "direction": "ltr",
"flag": "de", "link": "/de/", "active": 0, "current": 0, "available": 1 }
],
"user": { "logged": true },
"backendEditor": { "record": "…", "page": "…" }
}
The request goes to any page URL of the site with ?type=834; the language
prefix selects the language (/de/?type=834). The response is cached like a
page. Login state is evaluated in a TypoScript condition, so anonymous and
logged-in visitors get separate cache entries. On a headless: 2 site the
request needs the JSON Accept header like every other one. Requests for this
type skip shortcut and mount-point redirects, so a site root configured as a
shortcut still delivers the data.
nuxt-typo3 loads it before the first page render (features.)
from typo3. (default /?type=834), prefixed with
the current locale path, and keeps it in use. It is
reloaded when use switches the language, and refetched
with endpoints. when the localised request fails, so the
error page still has a navigation. get fetches it manually.
initial is a page object copied from lib., so the site
package extends it through initial. Anything a layout needs on
every page belongs here, for example a footer menu from a folder and the
site name:
initialData.10.fields {
siteName = TEXT
siteName.data = site:websiteTitle
footerNavigation = JSON
footerNavigation {
dataProcessing {
10 = headless-menu
10 {
special = directory
special.value = 15
as = footerNavigation
}
}
}
}
The new keys appear on initial in nuxt-typo3; extend its
T3Initial interface in the project for typed access. Page-specific
data does not belong here: it would be stale until the next initial data
request.
Rendering content
content is grouped by col, so the frontend needs one component per
backend layout that places the columns, and one component per content element
type. nuxt-typo3 resolves both by name:
appearance.→backend Layout: "singlecolumn" T3Bl. The component receivesSinglecolumn contentand renders each column withT3Renderer.- Every element in a column →
T3Ce<Type>with thetypeconverted from snake_case to PascalCase:text→T3Ce,Text image_→with_ description T3Ce. The keys of the element'sImage With Description contentobject become props, together withid,typeandappearance(T3Ce). Types without a component fall back to theBase Props Defaultcomponent.
<!-- components/global/T3BlSinglecolumn.vue -->
<template>
<div>
<T3Renderer v-if="content?.colPos0" :content="content.colPos0" />
<aside>
<T3Renderer v-if="content?.colPos1" :content="content.colPos1" />
</aside>
</div>
</template>
<script lang="ts" setup>
import type { T3BackendLayout } from '@t3headless/nuxt-typo3'
defineProps<T3BackendLayout>()
</script>
A custom content element registered in TYPO3 as image_
(Custom content elements) with bodytext and assets fields
is rendered by:
<!-- components/global/T3CeImageWithDescription.vue -->
<template>
<h2>{{ header }}</h2>
<T3HtmlParser :content="bodytext" />
<img v-if="assets?.[0]" :src="assets[0].publicUrl" :alt="assets[0].properties.alternative">
</template>
<script setup lang="ts">
import type { T3CeBaseProps, T3File } from '@t3headless/nuxt-typo3'
interface T3CeImageWithDescription extends T3CeBaseProps {
bodytext: string
assets?: T3File[]
}
withDefaults(defineProps<T3CeImageWithDescription>(), { bodytext: '' })
</script>
RTE fields (bodytext of text, textpic, textmedia) contain HTML. Links
in it already point at frontend; T3Html renders the HTML and
turns internal links into router links. Shipped components can be replaced by
placing a component with the same name in components/
(Reference: Content elements lists every shipped type and its props).
Languages
i18n on every page response and in ?type=834 lists the site languages with
link, active, current and available. A language switcher renders those
links; switching languages is a navigation to the other language's URL,
because TYPO3 resolves the language from the path. nuxt-typo3 detects the
language from the URL prefix using typo3., so that list has to
match the language base paths of the site (['en', 'de'] for /en/ and
/de/).
use provides current, set (which re-fetches
initial) and get.
Head data
seo carries everything for the document head. use and
use return it as head in the shape use expects; the
composable also splits it into base, opengraph and twitter for custom
head handling.
Forms and login
form_ elements carry the form definition in content.
and the submit URL in content. (EXT:form). nuxt-typo3
renders it with T3Ce, which wraps T3Form, resolves a
field component per type/identifier (T3Form, …) and posts the
values form-encoded to link; the api object of the response drives error
and success handling. Login (felogin_) works the same way on
content. (EXT:felogin). Both need the TYPO3 cookies
(Cookies).
Errors
For a missing page TYPO3 returns the response of its site error handling
with the matching status code. nuxt-typo3 turns a non-2xx API response into
a Nuxt error; the error. page can render TYPO3's error page content
through use and its page.
Multiple sites and ?type=835
?type=835 (headless_) lists the sites of the TYPO3 instance that
have headless enabled, one entry per site:
[
{
"name": "example.com",
"baseURL": "https://example.com",
"api": { "baseURL": "https://example.com/headless" },
"i18n": { "locales": ["en_US", "de_DE"], "defaultLocale": "en_US" }
}
]
base is the site's frontend, api. its frontend
(empty when the site has none, so set it on every site), locales the
typo3Language keys — derived from the site language's locale (en_,
de_) unless typo3Language is set explicitly. A frontend that serves several TYPO3
sites from one deployment uses it to map the request hostname to the API it
has to call.
nuxt-typo3 does not call it at runtime. Its multi-site setup is static:
typo3. holds one entry per hostname, matched against the incoming
request, each with its own api and i18n settings:
export default defineNuxtConfig({
typo3: {
sites: [
{
hostname: ['example.com', 'www.example.com'],
api: { baseUrl: 'https://api.example.com' },
i18n: { default: 'en', locales: ['en', 'de'] },
},
{
hostname: 'example.de',
api: { baseUrl: 'https://api.example.de' },
i18n: { default: 'de', locales: ['de'] },
},
],
},
})
The array can be generated from ?type=835 at build time, so a site added
in TYPO3 needs no change in the frontend repository. The shipped output is
not enough for that: nuxt-typo3 detects the language from the URL prefix
(de), while i18n. are TYPO3's typo3Language keys (en_,
de_). Extend the endpoint with a languages list first (see
Extending the output below); nuxt. is an ES module and can
then await the request:
import { ofetch } from 'ofetch'
interface HeadlessDomain {
name: string
api: { baseURL: string }
languages: { locale: string, base: string, hreflang: string }[]
}
const domains = await ofetch<HeadlessDomain[]>(`${process.env.TYPO3_API}/?type=835`)
export default defineNuxtConfig({
typo3: {
sites: domains.map((domain) => ({
hostname: domain.name,
api: { baseUrl: domain.api.baseURL },
i18n: {
default: domain.languages[0].base,
locales: domain.languages.map((language) => language.base),
},
})),
},
})
api. comes from the site's frontend; a site without it
gets an empty API URL here.
Extending the output
headless_ is a normal page object; its processor configuration can
be changed from the site package (loaded after the set). All options
(allowed, sites, site, …) are listed in
RootSitesProcessor. The usual change is the schema:
site replaces the class that shapes each entry. Extend the shipped
\Friends
to add fields; it receives the Site objects through the
provider. The languages entry below carries the URL prefix per language
(the language code for a language served without prefix):
namespace Vendor\SitePackage\Headless;
use FriendsOfTYPO3\Headless\DataProcessing\RootSiteProcessing\SiteProviderInterface;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
class LanguageAwareDomainSchema extends \FriendsOfTYPO3\Headless\DataProcessing\RootSiteProcessing\DomainSchema
{
public function process(SiteProviderInterface $provider, array $options = []): array
{
$result = parent::process($provider, $options);
foreach (array_values($provider->getSites()) as $index => $site) {
$result[$index]['languages'] = array_map(
static fn (SiteLanguage $language): array => [
'locale' => $language->getTypo3Language(),
'base' => trim($language->getBase()->getPath(), '/')
?: $language->getLocale()->getLanguageCode(),
'hreflang' => $language->getHreflang(),
],
array_values($site->getLanguages()),
);
}
return $result;
}
}
headless_domains.10.dataProcessing.10.siteSchema = Vendor\SitePackage\Headless\LanguageAwareDomainSchema
The class is instantiated with General, which only
takes it from the DI container when the service is public — headless marks
its own Domain public for that reason. Do the same for your class
in the site package's Services., otherwise make calls the
constructor without arguments and instantiation fails. The snippet assumes
the file's _defaults enable autowire: true:
services:
Vendor\SitePackage\Headless\LanguageAwareDomainSchema:
public: true