Architecture
This page explains how a Content Block record becomes JSON — the pipeline, the building blocks and the extension points. It is background, not a tutorial; for concrete tasks see the how-to guides.
The short version
The extension's whole job is array shaping: it takes rich TYPO3 domain objects and turns them into plain, JSON-compatible arrays — with a stable, frozen output contract. Object resolution is done by the TYPO3 Core (Record); key mapping, field ordering and value shaping are done here. That split is deliberate: it is what allows the same code to run on TYPO3 13 and 14 without version forks.
The pipeline
TypoScript data processor (Site Set: lib.contentBlock)
└─ nb-content-blocks-json (ContentBlocksJsonDataProcessor)
├─ RecordFactory::createResolvedRecordFromDatabaseRow() [Core]
│ DB row → rich objects (Record, FileReference, TypolinkParameter,
│ DateTimeImmutable, LazyRecordCollection, ...)
└─ RecordArrayBuilder::build()
├─ strip system fields (uid, pid, colPos, CType, ...)
├─ map columns → Content Block field identifiers
│ (tx_myext_field → my_field, via ContentBlocksIdentifierMapper)
├─ dispatch ModifyArrayRecursiveToArrayEvent per field (deprecated)
├─ strings → FieldValueTransformerChain
│ (PasswordBlanker, RichtextParser — schema-driven)
├─ everything else → NormalizerChain
│ first registered normalizer with supports() === true wins;
│ unknown types → UnknownTypeNormalizer (null + debug log)
└─ ksort
└─ headless.php include (optional, per Content Block)
└─ sub data processors (optional, TypoScript dataProcessing.)
nb- (Container) follows the same shape for EXT:container children: it fetches the children of a container column and renders each through the same Record.
Building blocks
| Building block | Purpose |
|---|---|
Data | TypoScript entry point (nb-): resolves the record, delegates to Record, includes headless., runs sub data processors |
Data | Same for EXT:container children (nb-), via b13/container's Container |
Normalization/ | Orchestrates one record: system fields, identifier mapping, event, transformers, normalizers, ksort |
Normalization/ | Iterates the tagged normalizers; falls back to Unknown |
Normalization/ | One class per value type — see Normalizers and transformers |
Field | String shaping driven by the field's schema type (password → "", richtext → parse) |
Content | Column → field identifier mapping; the only ContentBlocks definition still in the conversion path |
Content | Loads the optional per-block headless. (declarative image variants), with caching |
Normalization/ | Per-run state: current Tca, request and Content of the originating DataProcessor, TypoScript options., per-field image processing; lets normalizers recurse without circular DI |
Why normalizers (and not one big converter)
Before the 2026-08 rewrite, one class held a giant switch with instanceof chains over ContentBlocks internals, make everywhere, and version hacks (property_) to construct those internals in tests. The rewrite (see the design record) replaced it with a normalizer registry — the Symfony Serializer pattern, hand-rolled without the dependency:
- each normalizer is small and independently testable,
- site packages can add their own via DI tag (
nb_),headless. normalizer - field metadata comes from the Core Schema API (
Tca), which ships in TYPO3 13 and 14 — the version hacks disappeared because the code stopped constructing ContentBlocks internals.Schema Factory
ContentBlocks stays a hard dependency for exactly two things that have no Core equivalent: the identifier mapping (dropping it would change every consumer's JSON contract) and resolving the Content Block folder (headless., headless.).
Design principles
- The JSON contract is the product. Output shapes are frozen by characterization tests (
Content) that assert the complete JSON per fixture record. A refactor must not change them; a contract change is a deliberate, documented decision.Blocks Json Data Processor Char Test - Never break the response. Unknown value types become
nullwith a debug log entry; a missing file becomes an__entry in place — the page still renders.error Message - Config before code. What used to need a
headless.(image variants) is now declarative (php headless.+ TypoScript override).yaml headless.remains as an escape hatch.php
Extension points
| Extension point | Mechanism | Use for | Documentation |
|---|---|---|---|
| Normalizer | DI tag nb_ | own value types in the JSON output | How-to |
| Field value transformer | DI tag nb_ | own string field shaping | How-to |
headless. | per-Content Block PHP file | per-block post-processing of the whole data array | How-to |
Modify | PSR-14 event per field | legacy field overrides | How-to (deprecated) |
| Sub data processors | TypoScript data | TypoScript-computed data (menus, record lists) | How-to |
| Image variants | headless. + TypoScript options. | responsive thumbnails | How-to |
When several extension points could solve a problem, prefer the most declarative one (image variants over headless., transformer over event).