Zum Inhalt springen

TYPO3 Headless: The TYPO3 Headless API Extension and the „nuxt-typo3“ module – a detective attempt to understand the structures

Last updated on 21. Februar 2024


Very good Vue3, Nuxt3 and TypeScript tutorials or learning videos are available online e.g. here:


Unfortunately, things look quite different when implementing a Nuxt3 application with the TYPO3 framework. There is a serious lack of information and documentation.

But the people at the developing company did a very good coding job! Thanks a lot to our Polish Open Source heroes! – https://macopedia.com

This blog post tries to understand the connections and structures and hopes for many comments from fellow coders, who are also doing detective work, because the headless approach seems to be very promising for the future.

The prerequisite is a basic understanding of:

  • HTML
  • CSS
  • JavaScript
  • Git
  • Composer
  • TYPO3-12
  • TypeScript
  • Vue3
  • Nuxt3
  • the need to invest a lot of time and patience

Links and Documentations

The progressive web initiative at TYPO3

The TYPO3 headless Extension

The nuxt-typo3 module for Nuxt3

The three pillars


Latest Version 12 of the CMS

TYPO3 Extension

TYPO3 Headless API

Nuxt3 Module

nuxt-typo3 Module

The basic Idea

The general idea is that TYPO3 loads the content into the database and then makes the content available as a JSON API. This means that TYPO3 no longer generates the HTML page but only provides the backend to maintain the content, manages it with the help of a database and generates an API to access the content. The big change is achieved by the TYPO3 extension „headless“.

For the frontend, we create a Nuxt project and load the „nuxt-typo3“ module using npm or yarn. From now on, the TYPO3 content appears as if by magic on the output server of the Nuxt project.

But how and why, and how you adapt the whole thing to your own needs or the necessities of a specific project, remains in big parts a mystery.

Experience it in Action

If you want to try it out, on the following link you will find instructions to get a whole system up and running with a preconfigured DDEV setup called pwa-demo:

Video: https://www.youtube.com/watch?v=7MOwugAyHkY

Webpage t3headless: https://t3headless.io/

The t3headless website contains lots of nice advertising slogans, but few useful explanations.

Webpage Nuxt-module: https://t3headless.macopedia.io/nuxt-typo3/introduction

TYPO3 plus Extension – Nuxt3 Application – hosting of both

From here on, the blog is divided into three parts: TYPO3 and the headless extension (including other TYPO3 settings), the Nuxt3 frontend installation and the third part is hosting settings to bring the whole thing together.

An important last preliminary consideration

You can choose between two different approaches in the interaction between TYPO3 and a Nuxt application:

  • Only load texts and links to images and videos and then process them for the frontend at the Nuxt level. Apart from the names of the individual web pages, the additional beyond content information provided by the TYPO3-API then plays no role.
  • Try to process the API information that you get from the TYPO3 content elements used, the information about the respective backend layout and other information from TYPO3 in a structured and automated way and then get an almost finished preconfigured frontend output from Nuxt.

The nuxt-typo3 application from t3headless tries to go the second path, which ends up in a large spider’s web of Nuxt components, composables and countless cross-references, which try to turn individual content elements and the TYPO3 backend layouts used, into meaningful front-end output on the Nuxt side. But how to turn this automatism into an output suitable for your own project is not properly described anywhere. I will try here to bring some light into the darkness.

Part 1 – TYPO3 and the TYPO3 Headless Extension

The complete DDEV system install (pwa-demo) was discussed above.

For a dedicated test project, install TYPO3-12 at an external provider. With composer install the headless extension:

composer req friendsoftypo3/headless

Extension headless: https://extensions.typo3.org/extension/headless

Extension manual: https://docs.typo3.org/p/friendsoftypo3/headless/main/en-us/

At the world icon of the TYPO3 backend, create a new root page and include the static template of headless and delete all other templates (There is a hint in the description to disable fluid-styled-content!). Install an empty sitepackage. The all in one install above used the sitepackage for some configuration, but I didn’t find any hints what they did there.

Open Question: What has to be adjusted via the sitepackage?

In the TYPO3 site configuration (Sites), you can choose between different options:

  • Full headless mode
  • Mixed headless/fluid-mode
  • No headless

Open Question:
If you use mixed mode, can you activate the static template of fluid-styled-content?

The TYPO3 entry URL to call the headless API should be a subdomain e. g. „https://api.your-domain.de“

The frontend entry point in full headless mode can be empty and should in production mode point to the Nuxt dist folder of the final built. The variant point on the right side can be used e.g. for production mode or staging. (see the pwa-demo installation).

Open Question:
How do you exactly setup staging?

The redirects of TYPO3 are used for language changes. But didn’t find any documentation about it.

Open Question:
What should be done on the TYPO3 site beyond setting up different languages in the site settings? (Langauage change on the Nuxt side, see below.)

Now you can make different pages and fill them with content. The page with the world icon is the (Home) Nuxt index.vue page with the url slug: „/“.

For creating a link with <NuxtLink> you must mark it as string with backticks (escaping „\/“ didn’t work in my case):

<NuxtLink :to="`/`">

You can also make subpages and sub-subpages …

At the output of the „https://api.my-domain.de“ you will see JSON or the raw stringified output, that looks something like this:

{"id":43,"type":"Standard","slug":"\/","media":[],"meta":{"title":"TYPO3 Headless & PWA Demo","subtitle":"you have succesfully installed TYPO3","abstract":"Homepage for the Introduction Package of TYPO3","description":"","keywords":"typo3, introduction package, install, example setup","canonical":"","robots":{"noIndex":false,"noFollow":false},"author":"","authorEmail":"","ogTitle":"TYPO3 Headless & PWA Demo","ogDescription":"","ogImage":null,"twitterTitle":"TYPO3 Headless & PWA Demo","twitterDescription":"","twitterCard":"summary","twitterImage":null},"categories":"","breadcrumbs":[{"title":"TYPO3 Headless & PWA Demo","link":"\/","target":"","active":1,"current":1,"spacer":0,"hasSubpages":1}] … and so on …

This is in this case information of the root page. You can already identify some basic data:

  • data.id = 43 → TYPO3 id of the page
  • data.media = [] → no images, empty array
  • data.meta.title = „TYPO3 Headless & PWA Demo“
  • data.breadcrumbs[0].link = „\/“

The manual approach

You could now do a Nuxt fetch and show the name of the page in a headline:

// pages/index.vue

        <h1>{{ text }}</h1>

<script setup>
    const url = "https://api.my-page.de/";
    const text = ref(null);
    const { data } = await useFetch(url);
    text.value = data.value.meta.title

// useFetch is wrapping everything in a "data" object
// data is a Ref! therefore: "data.value"

Another manual example:
Fetching the text and the image of a TYPO3 standard „Text & Image“ element. This time the headline is hardcoded inside Nuxt:

// pages/index.vue

    <h1>Test page TYPO3 headless</h1>
    <div v-html="text"></div>
    <img :src="imgUrl" :alt="imgAlt" />

<script setup>
  const url = "https://api.my-page.de/";

  const text = ref(null);
  const imgUrl = ref(null);
  const imgAlt = ref(null);

 const { data } = await useFetch(url);

  text.value =

imgUrl.value =    

imgAlt.value = data.value.content.colPos0[1].content.gallery.rows["1"].columns["1"].properties.alternative;


// the API is delivering a p-tag together with the
// bodytext, to use it as such we need "v-html"
// Nuxt needs one surrounding element in the template: div
// for error handling, we should use optional
// chaining: data.value.content?.colPos0?.[0]
// and provide an empty array if it fails:
// data.value.content?.colPos0?.[0] || []

To find out what is where, you can just call the api with a browser. For example Firefox will give you a JSON output and a raw output. To see things even better, just copy the raw output in a Visual Studio Code .json file and auto-format it.

Open Question:
Can I manipulate what the api is outputting? Can I slim down the response, or only later map it in Nuxt into a new smaller object?
There is a hint about TypoScript possiblities but no description.

Open Questions:
TYPO3 extension headlesses manual:
Needs more and better explanation. If you know it anyway, it is a good documentation, but if not the explanations are very poor. Needs also examples (e.g. feature flags in the documentation is not clear what they do).

The central structural concepts!

  • Each TYPO3 web page provides a dedicated api link for the respective page under the page link: „https://api.my-domain.de/introduction-page“ or „https://api.my-domain.de/another-page“ etc.
  • You should setup backend-layouts (e.g. two-columns) in TYPO3 for the structure of the pages, that can be used by the „nuxt-typo3“ module to apply corresponding Nuxt layouts: „layouts/T3Bltwo-columns.vue“
    These layouts can be used for the basic frontend structure of your Nuxt project. It will also help to understand which content goes where for an editor who is working with the content in the TYPO3 backend.
  • The native TYPO3 content elements (in TYPO3-12 called: „Typical page content“), like „Text & Images“ or the „Regular Text Element“ are mapped with the „nuxt-typo3“ module to pre-configured Nuxt components for each content element e.g.: „components/T3CeText/T3CeText.vue“. These components can be overwritten.

Open Question:
If I want to use more than one version of a component representation for a respective (e.g. TYPO3 text) element, can I do this, or is the setting for the whole project. If a component gives me output that I don’t want to use for a specific area of the frontend, is then the only solution to hide it with CSS? An example would be the description information of an image, if I would want to show it sometimes and sometimes not.

Open Question:
Is there more concepts, that I missed?

Open Question
Overwriting the text TYPO3 content element module implementation in the module worked, but fetching the „Type“ as described in the documentation didn’t. This is not working, it goes to „module“ instead of „type“ (more see part 2):

import type { T3CeTextProps } from '@t3headless/nuxt-typo3';

work in progress

Part 2 – Nuxt3 and the „nuxt-typo3“ Module

tested with nuxt-typo3 version 2.0.2

The „nuxt-typo3“ module is a node-module that will be stored inside the „node_modules“ folder inside your Nuxt3 project. It can be found under the vendor name: „@t3headless/nuxt-typo3„.

The documentation is here: https://t3headless.macopedia.io/nuxt-typo3/introduction

There is also an older documentation online using Vue2, make sure to use Vue3/Nuxt3!

Unfortunately, the documentation looks like a detailed manual, but it is really only a documentation for a quick start without in-depth explanations.

Most important concepts to understand

  1. The „nuxt-typo3“ module provides different composables to fetch the JSON data and other work
  2. Backend-layouts from TYPO3 are used as the first starting point to automatically create a Nuxt frontend output according to matching Nuxt backend layouts components, that you have to create yourself.
  3. Your frontend needs App.vue, that uses „<NuxtPage />“ to catch all pages. You can overwrite the automatic pages layout and behavior by creating pages yourself in accordance with the TYPO3 pages names inside „pages/my-page.vue
  4. To create a menu you have to design it inside the layout folder (layouts/default.vue) in your Nuxt application. A reduced JSON object containing only data for all pages in one JSON can be fetched with the module composable: useT3Api() or by hand using the api TYPO3 endpoint: "https://api.your-domain.de/?type=834" (The normal api endpoint gives you a JSON for a single page).

A structural overview

SVG grafic nuxt-typo3 and typo3 headless overview


The Nuxt app renders the first page on the server (SSR – server side rendering) and delivers the first page as complete html page to the browser (good for SEO). In the same time it loads the Nuxt app with the nuxt-typo3 module and your own code (in the built version) to the browser. Now the Nuxt/Vue app takes over from inside the browser (CSR – client side rendering).

The automatized system of the nuxt-typo3 module

The nuxt-typo3 module has a automatic approach towards the transfer of the TYPO3 content into a Nuxt frontend app structure:

  1. page information are transformed into a menu structure
  2. the used TYPO3 backend layout for a respective page can be mapped into a backend-layout.vue:
    component/global/T3Bl + „name-of the-backend-layout or id“ + .vue
  3. the used TYPO3 content elements (e.g. text or text plus picture) are mapped into a matching Vue components provided by nuxt-typo3 or overwritten by yourself:
  4. TYPO3 language versions are used to get the corresponding version
  5. text is mapped into text output and media links into media tags in the components template area

Open Question:
How this exactly works is not clear yet! Especially when it comes to individual designs on different web pages using the same content elements.

A graphic that was shown in the the old manual you can find here:


It describes the following sequence:

  1. TYPO3 API gives the following settings, witch are mapped:
    a. title of the backend layout (I only got the id, as a number)
    b. page info + content
    c. layout (in TYPO3 backend called frontend layout; something I never used before)
  2. Inside a component/global/T3BlmyBackendLayout.vue you can sort the different backend layouts areas into html with a T3Renderer.
  3. For the display of the TYPO3 content elements (e.g. text, text + picture) you can use the provided ones from e.g.:
    Or overwrite them by providing your own ones:

Exploring the structure of the nuxt-typo3 module and its automated procedures

A. Nuxt Composables for different Duties


(should it be used with „await“? looks like no) has the following return values:

return {
const { pageData } = useT3Api()

// has the same output as

const { data } = await useFetch('https://api.my-domoin.de/thisPage')

// or same like using
// Firefox to connect to the api url of a specific page

pageData – gives the whole api content of a dedicated page.
Used inside a „layout/default.vue“ it switches automatically to the page where it is called. useFetch(url), gives the data of the page named in the url.

initialData – gives the navigation data of all pages and wraps them into a navigation object with an array of all page objects.

{ "navigation": [ { "title": "Home", "link": "/", "target": "", "active": 1, "current": 1, "spacer": 0, "hasSubpages": 1, "children": [ { "title": "First Page", "link": "/first-page", "target": "", "active": 0, "current": 0, "spacer": 0, "hasSubpages": 0 }, { "title": "About", "link": "/about", "target": "", "active": 0, "current": 0, "spacer": 0, "hasSubpages": 1, "children": [ { "title": "Sub About", "link": "/sub-about", "target": "", "active": 0, "current": 0, "spacer": 0, "hasSubpages": 1, "children": [ { "title": "Sub-Sub About", "link": "/sub-about/sub-sub-about", "target": "", "active": 0, "current": 0, "spacer": 0, "hasSubpages": 0 } ] } ] } ] } ], "i18n": [ { "languageId": 0, "locale": "de_DE", "title": "Deutsch", "navigationTitle": "Deutsch", "twoLetterIsoCode": "de", "hreflang": "de-DE", "direction": "ltr", "flag": "flags-de", "link": "/", "active": 1, "current": 0, "available": 1 } ] }

If you want to fetch the initial data by yourself you can use:

// initial page navigation data with useFetch

const {data} = await useFetch('http://api.my-page.de/?type=834')

// add at the end of the url: "/?type=834"

$fetch – It is not clear, why Vue3s „$fetch“ is passed through here?

In the case of „useFetch“ we have it from Vue3 (you must import it in your Nuxt app if you want to use this one: import from vue) and we have a new one from Nuxt, that overrides the one of Vue3 and is the default one in Nuxt.

Hint from Nuxt:
„Using $fetch in components without wrapping it with useAsyncData causes fetching the data twice: initially on the server, then again on the client-side during hydration, because $fetch does not transfer state from the server to the client. Thus, the fetch will be executed on both sides because the client has to get the data again.
We recommend to use useFetch or useAsyncData + $fetch to prevent double data fetching when fetching the component data.“

getPage – no clear what it does?

Would have expected something like the following, but it is not working:

// not working!
   This is the page id: {{ id }}

const { id } = getPage('https://api.your-domain.de')

The following code works, to get data about the respective page (e.g. my-page.vue) where the code is placed:

// this works

const { headData, pageData, backendLayout } = await useT3Page()

getInitialData – not clear what it does?

setHeaders – not clear what it does?

setOption – not clear what it does?

useT3DynamicComponents(type, prefix, mode)

Not clear what it does, and how?

You can render a content element type dynamically but how and where and why? Use case? The following code from the docs is not giving any output. Same for replacing „element.type“ with „Text“.

      type: element.type
      prefix: 'T3Ce',
      mode: 'Lazy'

see also: https://t3headless.macopedia.io/nuxt-typo3/composables/uset3dynamiccomponent


This composable has the following return values:

  return {

currentLocale – three steps:

First setup a new language inside TYPO3 backend, give it e.g. the „Entry point: ‚/de/‘. (Of course you also have to translate the content in usual TYPO3 manner.)

Second step register the locale in „nuxt.config.ts„:

// nuxt.config.ts

export default defineNuxtConfig({
  devtools: { enabled: true },
  modules: [
  typo3: {
    api: {
      baseUrl: process.env.API_BASE || 'https://api.my-webspace.de/'
    i18n: {
      locales: ['en', 'de'],
      default: 'en'

Third step show it in the frontend or use it for your design or logic:

// you could show the locale then as frontend output

    <p>This is the current locale: {{ currentLocale }}</p>
<script setup>
const { currentLocale } = useT3i18n()

getLocale – not clear what is the difference to „currentLocale“?

setLocale – is the most important function for your language menu.

Language switching

setLocale has two steps:
change the „currentLocale“ and fetch from the language api the new content.

// layouts/default.vue

    <h1>language testing</h1>

        This is the current locale: <b>{{ currentLocale }}</b>

      <button @click="setLocale('de')">Set Locale to Deutsch</button>
      <button @click="setLocale('en')">Set Locale to Englisch</button>

<script setup>
const { currentLocale, setLocale } = useT3i18n();

initLocale – not clear how it works?

getPathWithLocale – not clear how it works?

getCurrentLocaleData – not clear how it works?

In the language switching test I run into issues with 404 errors

Open Question:
– the endpoint for the page data is „/?type=834“ but nuxt-typo3 always produces an endpoint without the slash: „http://api.my-webpage.de/en?type=834“

Comment from the nuxt-typo3 documentation:

„Be aware that an endpoint starting with a slash means the module fetches InitialData only for the main page on initial load (including the current locale). When you omit the slash, InitialData will be fetched for the current page.“

Open Question:
So what should I use in the api settings in nuxt.cofnig.ts or should I just leave it like it is preseted in nuxt-typo3 → module.mjs: initialData: "/?type=834", Just change nothing?

– work in progress

We have two different api calls, the first one is having a URL parameter of „/?type=834“ and is responding with information about all active pages and the second is responding with the content of a single page.

// getting the data about all active pages
const { initialData } = useT3Api();

//getting the data of a single page and the content
const { pageData } = useT3Api();

The menu can be placed in „layouts/default.vue“ and will then be available for all pages:

// layouts/default.vue
// outputting a two level menu in a usual ul/li structure
// "home" is getting it's own section at the beginning

    <header v-if="navigation">
        <nuxt-link :to="navigation.link">
          {{ navigation.title }}

      <ul v-for="item in navigation.children" :key="item.link">
          <NuxtLink :to="item.link">
            {{ item.title }}
        <ul v-if="item.children">
          <li v-for="itemS in item.children" :key="itemS.link">
            <NuxtLink :to="itemS.link">
              {{ itemS.title }}

    <!-- End menu start of the slot -->
<slot />


<script setup lang="ts">

const { initialData } = useT3Api();
const navigation = computed(() => {
  return initialData.value?.navigation?.[0];


If you want to understand the structure you can call the api with Firefox or make it visible on your output:

// put this inside <template> while the "const {intitalData} = useT3Api()" call is inside <script>

<p>Initial Data all pages api JSON: {{ initialData }}</p>

navigation.children is giving all children from base url (home) and then we check if there is children and children of children.

For a different language it changes automatically.

Manual approach

You could also get the data by yourself by using useFetch (from Nuxt):

// getting data without the nuxt-typo3 module
// but you have to take care of errors

const { data } = await useFetch('http://api.your-domain.de/?type=834')

const navigation = computed(() => {
  return data.value?.navigation?.[0];

– work in progress

C. TYPO3 Backend Layouts to Vue Layouts

It needs two steps to enable Vue layouts to be applied:

a. in App.vue, enable layouts by using the <NuxtLayout> directive:

// App.vue

    <NuxtPage />

b. in the next step create a ‚layouts/default.vue‚ layout with at least a ‚<div><slot /></div>‚ (needs a single surrounding element, here the div).

After this the default.vue layout (that usually also has the navigation), is applied to every page that doesn’t have an explicit layout statement.

Code example above in the navigation section.

Individual layouts per page

Create a new layout at ‚layouts/my-layout.vue‘, again with at least one div and the slot. Then define it for an individual page:

<script setup lang="ts">

    layout: 'second-layout'

read more at the Nuxt docs:

Creating your own frontend implementation for a TYPO3 backend layout

Here the link to the documentation of nuxt-typo3:


In „components/global“ create a file with the name of the backend layout you want to target (in my case it was the id number of the backend layout that the api delivered).

The file starts with T3Bl and then add the id number of the backend layout e.g. T3Bl2.vue

Hint – sequence
First create the backend layout in TYPO3, give it a name and check the TYPO3 id it got. Then go to Nuxt and implement a new T3Bl + id.vue component. The new backend layout will be applied automatically.

Backend layouts, example from the nuxt-typo3 documentation

// content of my: components/global/T3Bl2.vue
// my api only gave me the id 2 for this backend layout
// not the title as a string

      Custom Backend Layout
      <p>First column</p>
      <p>Second column</p>

  <script lang="ts" setup>
// there was a issue before version 2.0.2  but now:

  import type { T3BackendLayout } from '@t3headless/nuxt-typo3'



Hint from nuxt-typo3 manual:
The T3Renderer component is responsible for rendering all Content Elements from a specific column.

I seems that "defineProps<>()" is the magic function providing the content.

Here the explanation from the Vue docs (https://vuejs.org/guide/typescript/composition-api.html):

Vue3 documentation:
We can also move the props types into a separate interface:

<script setup lang="ts">
interface Props {
  foo: string
  bar?: number

const props = defineProps<Props>()

This also works if Props is imported from an external source. This feature requires TypeScript to be a peer dependency of Vue.vue

<script setup lang="ts">
import type { Props } from './foo'

const props = defineProps<Props>()

End Vue documentation

In our case the interface for T3BackendLayouts looks like this:

// in: node_modules ... nuxt-typo3 ... module.d.mts

interface T3BackendLayout {
    content: {
        [key: string]: T3ContentElement<any>[];

If you store the return in a ‚const props‘

const props  = defineProps<T3BackendLayout>()

and show it in the frontend output
<p>This is the props: {{ props }}</p>
I got the following output:

This is the props: { "content": { "colPos1": [ { "id": 8, "type": "textpic", "colPos": 1, "categories": "", "appearance": { "layout": "default", "frameClass": "default", "spaceBefore": "", "spaceAfter": "" }, "content": { "header": "Root Element mit dem Namen HOME", "subheader": "", "headerLayout": 0, "headerPosition": "", "headerLink": "", "enlargeImageOnClick": false, "bodytext": "<p>DEUTSCH acinia Lorem Suspendisse at. lacinia. nibh adipiscing dignissim diam, fermentum ullamcorper eu. vel Suspendisse Nulla fringilla. malesuada a ultricies ipsum Donec et massa fermentum et diam, nibh dolor, sollicitudin sollicitudin sit laoreet at. fringilla. consectetur vestibulum&nbsp;</p>", "gallery": { "position": { "horizontal": "center", "vertical": "above", "noWrap": false }, "width": 600, "count": { "files": 1, "columns": 1, "rows": 1 }, "columnSpacing": 10, "border": { "enabled": false, "width": 2, "padding": 0 }, "rows": { "1": { "columns": { "1": { "publicUrl": "http://typo3-composer.p661210.webspaceconfig.de/fileadmin/_processed_/4/2/csm_vue-project_a2e28c1f4b.png", "properties": { "title": "vue image title tag", "alternative": "vue image alt tag", "description": "vue image description tag", "link": null, "linkData": null, "mimeType": "image/png", "type": "image", "filename": "csm_vue-project_a2e28c1f4b.png", "originalUrl": "/fileadmin/images/vue-project.png", "uidLocal": 1, "fileReferenceUid": 5, "size": "79 KB", "dimensions": { "width": 600, "height": 333 }, "cropDimensions": { "width": 600, "height": 333 }, "crop": { "default": { "cropArea": { "x": 0, "y": 0, "width": 1, "height": 1 }, "selectedRatio": "NaN", "focusArea": null } }, "autoplay": null, "extension": null } } } } } } } } ], "colPos0": [ { "id": 9, "type": "text", "colPos": 0, "categories": "", "appearance": { "layout": "default", "frameClass": "default", "spaceBefore": "", "spaceAfter": "" }, "content": { "header": "Text in rechter Spalte", "subheader": "", "headerLayout": 0, "headerPosition": "", "headerLink": "", "bodytext": "<p>DEUTSCH<br>bibendum at. dolor, amet, vestibulum vel Nulla Maecenas at. sit vestibulum aliquet amet, Maecenas sit vel non ultricies ante In et lacinia dolor, laoreet tellus Suspendisse Sed&nbsp;</p>" } } ] } }

It is just the „content“ part of the api JSON response for a certain page, without the meta data of the page.

TYPO3 Bug?
During development moving elements around in the TYPO3 backend, I ended up with not having them in the appropriate backend layout column although the TYPO3 backend displayed it as if they are in the matching column. The api gave me only one column in a two column backend layout. After deleting the elements and making them new it worked.

A thought:
What is the big fuss about backend layouts?
In my normal TYPO3 programming I don’t use them at all. I use DCEs (dce extension from Armin Vieweg) and slice a web page into horizontal sections, where every section is a DCE. Each DCE gets an id and its own css class in a wrapper div. Each DCE lives in its own world and is docking to the one above. Simple and well sorted, also in the css where each DCE gets its own section.
In normal TYPO3 coding backend layouts in my opinion are to big, You could end up creating a new backend layout for every single page and if you add something in the middle, you have to change the whole structure.

In TYPO3 headless it is a bit different, since the editor in the TYPO3 backend needs some hints to where the content will go.

TYPO3 backend layouts „id“ or „title“

Backend layouts made in the page tree of TYPO3 as a „record“ will be exported by the api with their „id“. If you get them from a „page.tsconfig“ file from your TYPO3 sitepackage, the title will be outputted by the api.

(thanks for contribution from twoldanski)

Since backend layouts must match between TYPO3 and the Nuxt backend layout components they should be out of the reach of a editor.

So my best practice is as follows and makes a sitepackage necesarry for the TYPO3 api:

  1. design the backend layouts in the TYPO3 backend
  2. copy the code to your sitepackage (Configuration/BackendLayouts/my-backend-layout.tsconfig) import them inside a Configuration/page.tsconfig file
  3. change the title of the backend layout to a unique name for all languages
  4. use this title in the Nuxt app T3Bl +title
  5. if you change the title you have to change it on both sides, TYPO3 and Nuxt

– work in progress

D. TYPO3 Content Elements to Vue Components

To create a structure for an individual web page in a project you have three steps of design options:

  1. create a individual layout/menu in „layouts/myLayout.vue“ and apply it to your own page vue component (pages/my-page.vue). Basically menus, language buttons, headers, footers.
    jump to section →
  2. design the corresponding vue backend layout to your TYPO3 backend layout and apply it to your page component. Sorting the page content to a page design. Create pages/my-page.vue
    jump to section →
  3. overwrite the structure of the TYPO3 corresponding vue content elements component with your own component e.g. for the text element:

The official documentation about overwriting vue components for content elements is here:


The official documentation of nuxt-typo3 has a few problems:

Here is the code:

// from the docs overwriting T3CeText.vue → components/global/T3CeText.vue

  <div class="t3-ce-text">
    <h2>{{ header }}</h2>

// this needs to be changed see below:
    <p>{{ bodytext }}</p>


<script lang="ts" setup>
import type { T3CeTextProps } from '@t3headless/nuxt-typo3';

const props = defineProps<T3CeTextProps>();
// Implement or extend JavaScript logic here

p {
  color: blue;
  • First the import from '@t3healdess/nuxt-typo3' goes to ‚module‘ and not to ‚types‘, so you have to use the whole path:
    from '/home/thomas/CODE/Nuxt/nuxt-typo3/node_modules/@t3headless/nuxt-typo3/dist/types'
    with version 2.0.2 it works
  • The bodytext comes with a <p> tag, that will be displayed like text, so you need to do:
    <div v-html="bodytext"></div> or
    <T3HtmlParser :content="bodytext"/> (without surrounding div) or
    <T3HtmlParser v-if="bodytext" :content="bodytext"/>
  • The type T3CeTextProps (only bodytext), is extending the type T3CeBaseProps, so it has access to the header, but you have to fill of course the respective field in the TYPO3 backend.

You find type information in the TypeScript declaration file of the nuxt-typo3 module:

Open Question:
Why is the direct acces to modules.d.ts not working and you have to go through types.d.ts and use the long ‚from‘ path (see above)?

The nuxt-typo3 module uses in his files:
import type { T3CeTextProps } from '../../../types'

– work in progress

E. Language switching and matching Content switching

The language switching is quite simple

see above under composables useT3i18nsetLocale

Just make a language button and use @click="setLocale('en')". Simple and nice, good job done in Poland!

work in progress

Part 3 – Hosting Considerations for TYPO3 and a Nuxt Frontend

Open Question:
In my installation I run into issues with CORS Cross-Origin Resource Sharing. What is here the best practice solution? What is the solution for production mode?

Devleopment solution:
The site is rendering after refreshing the browser window (Nuxt renders on the server) but the api-server blocks access from the localhost (npm run dev) frontend. This setting on the api-servers .htaccess (TYPO3 Apache server) helps for the development:

# inside .htaccess

Header set Access-Control-Allow-Origin '*'

Deploying: See at the end of „Best Pracitce“.

work in progress

Part 4 – My „Best Practice“ for a simple web page

– so far, work in progress

Make a wire frame design of the web page

Setup a TYPO3 composer installation at your favourite provider

nothing special here

Install the headless Extension

composer req friendsoftypo3/headless

Add the static template from the headless extension and remove the fluid-styled-template.

  • Go to TYPO3 „Sites“ and choose „Full headless mode“.
  • For the „TYPO3 entry point“ put your sub-domains name: „https://api.my-domain.de/“, Frontend entry point can stay empty.
  • Add one or more languages. For the additinal languages use: „https://api.my-domain.de/en/“ or any other matching two letter language code.
  • Make the world icon your site entry point
  • Generate your pages
  • Make a folder at the end of your page tree and call it „backendLayouts“, inside the folder create your backend layouts according to the wire frame design
  • Choose for your pages the backend layout you want to use
  • Use the TYPO3 existing content elements to crate your content
  • Make the translations


Kick-Start a Nuxt project and add the „nuxt-typo3“ module

npx nuxi@latest init -t gh:TYPO3-Headless/nuxt-typo3-starter <project-name>

If you run into issues with Python3 and missing matching libraries on your computer, just use yarn.

More details for the installation are here: https://t3headless.macopedia.io/nuxt-typo3/introduction.

// install the module

npm install @t3headless/nuxt-typo3 --save-dev

You should find the module now in:


Go to the terminal and cd into the project folder

Open the project in Visual Studio Code (VSC) by typing:

code .

In VSC open a terminal and start the development server:

npm run dev

Register the nuxt-typo3 module, and set the url/api and the languages in your „nuxt.config.ts“:

export default defineNuxtConfig({
  devtools: { enabled: true },
  modules: [
  typo3: {
    api: {
      baseUrl: process.env.API_BASE || 'https://api.my-domain.de'
    i18n: {
      locales: ['en', 'de'],
      default: 'de'

In your browser on http://localhost:3000 you should see already the output of the starting (Home) page.

Now start with the demands of the individual web page. Code your frontend Nuxt application in VSC:

Make the general layout/s:
menu, language buttons, logo, header <slot /> footer

Make a folder with the name „layouts“ and at least one file with the name:

If necessary make more layouts for different pages of your project.

Create Vue components matching with the TYPO3 backend layouts

Inside the „components“ folder create a folder „global„, all components go into „global“:

Create the „T3Bl + backend-layout-id“ or „T3Bl + backend-layout-name“ vue components. Check at your api output what you got, ids or names.

Create a „pages“ folder and create pages matching the url slug as the name

e.g.: pages/About.vue

The fist letter can be capital (as usuall in Vue).

The „index.vue“ page is the start (Home) page. Or the start page of a folder with sub-pages (normal Vue style).

You can give a page a special header, menu, footer layout by registering a layout:

<script setup>
    layout: 'two-column-layout'

And you have to tell the page whitch backend layout should be used:


If you want to even more fine grain your structural design, overwrite the existing content elements Vue components

Inside „components/global“ create overwritng components for the content elements.

e.g.: T3CeText.vue


Add CSS to the respective <style> areas or in a general CSS style sheet. You can also use frameworks.

Deployment at a normal TYPO3 Provider

There are different options.

The Nuxt application must be prerendered for production mode and all routes must be set correctly. If you dont’t use a specialized service you need to setup a secure environment.

I didn’t do any testing so far.

The nuxt manual gives the following command for a static server:

npx nuxi generate

The entry point for the frontend will then be: „.output/public

more details here under „Static Hosting“:

Still unclear how everything is installed on a TYPO3 server environment.
The hoster Mittwald in Germany for example takes 9 Euro per month additionally only to activate NodeJS and then still has issues with the ports.

Here are instructions from Mittwald: 9 Euro for a NodeJS socket and a full virtual machine (starts from 35 Euro) if you need NodeJS on a port (usually port 3000)

Mittwlad support:

You can make configurations in .mwservice.yaml. You can find further information here:
Please note that NuxtJS usually requires a port specification, but in our Node.js environment the instance is addressed via a socket.
So that NuxtJS can still run with a socket, the Node module „socket.io“ must be added. You can install this module with the command
npm i nuxt-socket-io
Here is the documentation for this:

If your configurations still require the port, we would recommend our „proSpace“ tariff. There NodeJS can be addressed via port.

work in progress

If you have insights or you do understand certain parts of the TYPO3 headless approach PLEASE comment!

Author: Thomas Hezel – 2024 – info@zazu.berlin

    Schreibe einen Kommentar

    Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert