---
title: "Post-process JSON with headless.php"
manual: "Headless Content Blocks"
version: "main"
permalink: "https://docs.typo3.org/permalink/netzbewegung/nb-headless-content-blocks:post-process-json-with-headless-php@main"
source: "Howto/PostProcessWithHeadlessPhp.rst"
rendered: "2026-09-22T01:10:25+00:00"
---

# Post-process JSON with headless.php {#post-process-json-with-headless-php}

This guide shows how to modify a Content Block's complete `data` array in PHP — the escape hatch when no declarative feature covers your case.

## When to use it (and when not) {#when-to-use-it-and-when-not}

Reach for `headless.php` only for what the built-in features cannot do:

| Need | Use instead |
| --- | --- |
| Responsive image variants | [Define image variants](https://docs.typo3.org/permalink/netzbewegung/nb-headless-content-blocks:define-image-variants@main) (`headless.yaml`) |
| Menus, record lists, TypoScript data | [Add sub data processors](https://docs.typo3.org/permalink/netzbewegung/nb-headless-content-blocks:add-sub-data-processors@main) |
| Custom value/string shaping | [Custom normalizer](https://docs.typo3.org/permalink/netzbewegung/nb-headless-content-blocks:register-a-custom-normalizer@main) / [transformer](https://docs.typo3.org/permalink/netzbewegung/nb-headless-content-blocks:register-a-field-value-transformer@main) |

`headless.php` remains for genuinely block-specific logic — merging two fields, calling an API, reshaping the whole payload.

> **Maintenance note:** on the production site this extension was built for, 13 `headless.php` files existed whose only job was thumbnail generation. All of them are candidates for deletion since declarative image variants exist — see [Migrate legacy thumbnails](https://docs.typo3.org/permalink/netzbewegung/nb-headless-content-blocks:migrate-legacy-thumbnails-to-headless-yaml@main) and the archived [legacy pattern](https://docs.typo3.org/permalink/netzbewegung/nb-headless-content-blocks:legacy-thumbnails-via-headless-php-imageviewhelper@main).

## Create the file {#create-the-file}

Place a `headless.php` next to the Content Block's `config.yaml`:

```php
your_extension/ContentBlocks/ContentElements/your-block/headless.php
```

The file receives the fully built `data` array and returns the modified one:

```php
<?php

declare(strict_types=1);

// $data: the complete JSON array of this block, field identifiers as keys

foreach ($data['items'] ?? [] as $itemKey => $item) {
    $data['items'][$itemKey]['combinedTitle'] = trim($item['title'] . ' ' . $item['subtitle']);
}

// You can also add completely new keys...
$data['lastModified'] = date(\DateTimeInterface::W3C);

// ...or remove fields from the output
unset($data['subtitle']);

return $data;
```

Rules:

-   The file is plain PHP included via `require` — keep it side-effect free apart from building the return value.
-   Return the array. A file that returns anything else (e.g. no `return` statement at all) throws an exception in a Development application context; in Production the processor falls back to the unmodified data.
-   The file runs on **every** render of this block type.

## Compatibility {#compatibility}

The extension plans to pass a second parameter with the normalization `Context` (`function (array $data, Context $context)`) — not yet part of the stable API. Do not rely on it yet; see the [design record](https://docs.typo3.org/permalink/netzbewegung/nb-headless-content-blocks:design-improve-rewrite-the-toarray-conversion@main) (decision 4).

## Troubleshooting {#troubleshooting}

Changes do not show up? `headless.php` is executed on every request — check that the file is really next to the block's `config.yaml` and that you are editing the Content Block the record actually uses (CType).
