# VITEC Headless JSON Architecture — Software Architecture and Interface Specification | | | |---|---| | **Document identifier** | EVO‑VITEC‑HL‑001 | | **Version** | 1.0 | | **Status** | Released | | **Date** | 2026‑07‑09 | | **Applies to** | `evomedien/vitec` on TYPO3 v14.3 (headless) | | **Owner** | evomedien — VITEC relaunch | This document is drafted in the style of, and adopts the terminology conventions of, ISO/IEC/IEEE 42010 (architecture description), ISO/IEC/IEEE 26514 (information for users) and ISO/IEC 25010 (product quality). The key words **shall**, **should** and **may** are to be interpreted as normative requirements, recommendations and permissions respectively. --- ## Foreword The VITEC web platform is a *headless* TYPO3 installation: the CMS does not render HTML pages, it emits JSON that is consumed by a separate React front end. This specification describes the architecture, the public JSON interface, and the engineering conventions that keep the JSON output **consistent** across all content types and **maintainable** across TYPO3 and extension upgrades. It supersedes, as the authoritative reference, the informal tutorial `Documentation/HeadlessIntegration.md`, which is retained as an informative how‑to. ## Introduction The platform combines the generic headless page renderer (`friendsoftypo3/headless`) with three sources of content JSON: 1. **Custom plugins** (product, use case/success story, market, solution, downloads, datasheets, events, news) rendered by dedicated *UserFunc* classes; 2. **Layout containers** (b13/container based column grids and a card carousel) rendered by a *DataProcessor*; 3. **Content Blocks** (`friendsoftypo3/content-blocks`) serialised automatically by `nb-headless-content-blocks`. All three are unified under a single **content‑element envelope** so that the front end can consume every element with one predictable shape. --- ## 1 Scope ### 1.1 In scope This document specifies: - the runtime environment and the software stack (Clause 5); - the JSON rendering pipeline and its layers (Clause 6); - the public JSON interface — envelope, payloads, structured data (Clause 7); - the catalogue of content types and their JSON keys (Clause 8); - the mandatory conventions for implementing and extending renderers (Clause 9); - maintainability and upgrade‑safety requirements (Clause 10); - conformance criteria (Clause 11). ### 1.2 Out of scope Front‑end (React) implementation, hosting/deployment, the editorial (backend) TCA form design except where it determines JSON output, and non‑headless (Fluid) rendering paths. ## 2 Normative references The following documents are referred to in the text. For dated references, only the edition cited applies. - ISO/IEC/IEEE 42010, *Software, systems and enterprise — Architecture description* - ISO/IEC 25010, *Systems and software Quality Requirements and Evaluation (SQuaRE) — Product quality model* - ISO/IEC/IEEE 26514, *Systems and software engineering — Design and development of information for users* - ISO 8601‑1, *Date and time — Representations for information interchange* - IETF RFC 8259, *The JavaScript Object Notation (JSON) Data Interchange Format* - IETF RFC 2119, *Key words for use in RFCs to indicate requirement levels* - schema.org vocabulary (informative), *https://schema.org* ## 3 Terms and definitions **3.1 headless** — operating mode in which TYPO3 returns JSON instead of HTML; enabled per site by `headless: 1` and the headless Site Sets. **3.2 content element** — a `tt_content` record; the atomic unit of page content. **3.3 CType** — the content‑element type identifier stored in `tt_content.CType` (e.g. `vitec_productlist`, `news_pi1`, `vitec_cols_50_50`). **3.4 envelope** — the invariant outer JSON structure shared by every content element (Clause 7.2). **3.5 payload** — the domain‑specific JSON produced for one content element and placed inside the envelope (Clause 7.3). **3.6 renderer** — a *UserFunc* class under `Classes/UserFunc/` that produces a payload. **3.7 serializer** — a service class under `Classes/Service/` that converts a domain record into JSON, used by one or more renderers. **3.8 resolver / processor** — `ContentElementResolver` and `ContainerChildrenProcessor`; they normalise a raw `tt_content` row into an envelope and resolve nested elements. **3.9 container** — a b13/container CType that owns child content elements via `tx_container_parent` and emits them as `items`. **3.10 Content Block** — a declaratively defined content element (`friendsoftypo3/content-blocks`), serialised by `nb-headless-content-blocks`. ## 4 Symbols and abbreviated terms | Term | Meaning | |---|---| | FAL | File Abstraction Layer (TYPO3 file handling) | | IRRE | Inline Relational Record Editing (`type: inline`) | | MM | Many‑to‑many junction table | | CB | Content Block | | TS | TypoScript | | CE | Content element | --- ## 5 Runtime environment (architecture context) ### 5.1 Software stack | Component | Version | Role | |---|---|---| | TYPO3 CMS | ^14.3 | Core CMS | | PHP | 8.x (per TYPO3 14) | Runtime | | `friendsoftypo3/headless` | ^5.0@rc | Page‑to‑JSON renderer, `lib.contentElement` | | `friendsoftypo3/content-blocks` | ^2.4 | Declarative content elements | | `netzbewegung/nb-headless-content-blocks` | ^0.0.23 | Content Blocks → JSON | | `b13/container` | ^3.1 | Nested column containers | | `georgringer/news` | ^14.0 | News records and plugins | | `evomedien/vitec` | ^1.0 | This project’s custom extension | > **NOTE** `friendsoftypo3/headless` is pinned to a **release candidate** (`^5.0@rc`). > This is an upgrade‑sensitivity point; see 10.4. ### 5.2 Site configuration The headless mode is activated in `config/sites/vitec/config.yaml`: ```yaml base: / headless: 1 frontendBase: '' dependencies: - friendsoftypo3/headless - friendsoftypo3/headless-mixed - nb-headless-content-blocks/headless-content-blocks - georgringer/news ``` The Site **Sets** listed under `dependencies` load, in order, the headless TypoScript base, the mixed‑mode overrides, the Content Blocks JSON integration and the News integration. The VITEC Set (`EXT:vitec/Configuration/Sets/Vitecset`) layers the custom definitions on top. The headless page response carries `Content-Type: application/json; charset=utf-8`. Slug routing is configured with route enhancers for products (`tx_vitec_domain_model_product.slug`) and news detail (`path_segment`). ### 5.3 Architectural principles (rationale) - **P1 — One envelope.** Every content element, regardless of source, is exposed with the same outer shape so the front end has a single rendering contract. - **P2 — Payload isolation.** Domain JSON is produced in PHP, fully decoupled from TypoScript, so business logic is testable and versionable. - **P3 — Dual entry.** Each renderer works both as a top‑level plugin and as a nested child of a container (Clause 9.3). - **P4 — Fail soft.** A failing element yields empty output, never a broken page (Clause 9.5). --- ## 6 Rendering pipeline The JSON for one page is assembled top‑down through the following layers. ``` HTTP request (Accept: application/json, headless:1) │ ▼ [L1] Page renderer friendsoftypo3/headless │ builds { meta, content[], … , jsonLd } ▼ [L2] Content‑element envelope lib.contentElement / lib.contentElementWithHeader │ per CType: id, type, colPos, appearance, content{header,…} ▼ [L3] Payload injection USER cObj → Classes/UserFunc/*JsonRenderer::render │ places domain JSON under content. ▼ [L4] Containers tt_content.vitec_cols_* = JSON │ ContainerChildrenProcessor → items[].contentElements[] ▼ [L5] Normalisation & nesting ContentElementResolver / PLUGIN_RENDERERS ▼ [L6] Structured data (JSON‑LD) PageJsonLdRenderer + StructuredDataService ``` ### 6.1 L1 — Page renderer `friendsoftypo3/headless` converts the requested page into a JSON document containing page metadata, the ordered array of content elements, navigation and the JSON‑LD graph. VITEC does not replace this layer; it contributes elements to `content[]` (L2–L5) and the `jsonLd` field (L6). ### 6.2 L2 — Content‑element envelope Each CType is bound to a headless library object: ```typoscript tt_content. < lib.contentElementWithHeader ``` `lib.contentElement` provides `id`, `type`, `colPos`, `categories`, `appearance`. `lib.contentElementWithHeader` additionally provides, under `content`, the standard header fields: `header`, `subheader`, `headerLayout`, `headerPosition`, `headerLink` (link resolved via typolink). **All VITEC plugins and all News CTypes inherit `lib.contentElementWithHeader`**, giving a uniform header section in both backend and JSON (see the companion header convention). ### 6.3 L3 — Payload injection The domain payload is added as a `USER` content object under `content.fields.`: ```typoscript tt_content.vitec_productlist < lib.contentElementWithHeader tt_content.vitec_productlist { fields { content { fields { products = USER products.userFunc = Evomedien\Vitec\UserFunc\ProductListJsonRenderer->render } } } } ``` `render()` returns a JSON string that the headless JSON cObject embeds verbatim at `content.products`. The key is **plural for list plugins** and **singular for detail plugins** (Clause 9.6). ### 6.4 L4 — Containers Column containers are defined as a self‑contained JSON object, not via `lib.contentElement`: ```typoscript tt_content.vitec_cols_50_50 = JSON tt_content.vitec_cols_50_50.fields { id … type … appearance … header … subheader … gap = TEXT # tx_vitec_gap (whole‑grid gap) items = JSON items.dataProcessing.10 = Evomedien\Vitec\DataProcessing\ContainerChildrenProcessor } tt_content.vitec_cols_33_66 < tt_content.vitec_cols_50_50 tt_content.vitec_container < tt_content.vitec_cols_50_50 # single column, no gap tt_content.vitec_cards_carousel < tt_content.vitec_cols_50_50 # + carousel settings ``` > **RULE (normative)** Container CTypes **shall** be derived with the **copy** > operator `<`, never the reference operator `=<`. A `tt_content → tt_content` > reference is not recognised as an independent renderer by the headless content > mapper and silently falls back to raw output (see 10.5, and Annex B‑1). ### 6.5 L5 — Normalisation and nested plugins `ContainerChildrenProcessor` queries children by `tx_container_parent`, groups them by `colPos` and emits, per column, a flex configuration plus the resolved children. Each child is normalised by the same envelope logic as `ContentElementResolver`. When a child is itself a VITEC plugin, it is resolved through the shared `PLUGIN_RENDERERS` map (Clause 7.4). ### 6.6 L6 — Structured data (JSON‑LD) `PageJsonLdRenderer` (bound at `page…fields.jsonLd`) assembles a schema.org `@graph` via `StructuredDataService`, encoded with `JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE`. Node types and their triggers: | Node | Emitted when | |---|---| | `Organization` | every page (from site settings `seo.organization.*`) | | `WebSite` | only on the site root page | | `BreadcrumbList` | from the rootline (spacer/folder/recycler doktypes skipped) | | `NewsArticle` | on news‑detail page layouts (`pages.layout ∈ {13,14,15}`) | | `FAQPage` | when the page contains `vitec_faq` elements → `vitec_faq_items` collection | | `ExhibitionEvent` | from `vitec_eventlist` elements → `tx_vitec_domain_model_event` (windowed by the FlexForm `daysinadvance`) | | `Product`, `VideoObject` | via `ProductShowJsonRenderer` for product detail pages | FAQ and event nodes are collected from the respective tables; event image URLs are currently built with a hard‑coded `/fileadmin` prefix (see 10.3 / Annex B‑5). --- ## 7 JSON interface specification ### 7.1 Encoding Output **shall** be RFC 8259 JSON, UTF‑8. Timestamps **shall** be Unix epoch seconds (integer); where ISO 8601 strings are required by schema.org they are produced inside the JSON‑LD layer. ### 7.2 Content‑element envelope Every element in `content[]` conforms to: ```jsonc { "id": 123, // tt_content.uid "type": "vitec_productlist", // tt_content.CType "colPos": 0, "appearance": { "layout": "0", "frameClass": "default", "spaceBefore": "", "spaceAfter": "" }, "content": { // present for lib.contentElement(WithHeader) CTypes "header": "…", "subheader": "…", "headerLayout": 2, "headerPosition": "", "headerLink": "https://…", "": { /* payload, Clause 7.3 */ } } } ``` Elements produced by the resolver/processor (container children, inline story CEs) use a lean variant of the envelope: ```jsonc { "id": 456, "type": "text", "colPos": 211, "sorting": 1, "appearance": { … }, "data": { /* non‑system fields */ } } ``` ### 7.3 Payload keys | Kind | Key | Cardinality | |---|---|---| | List plugin | plural noun (`products`, `usecases`, `news → items`) | array | | Detail plugin | singular noun (`product`, `usecase`, `market`, `solution`) | object | | Container | `items` | array of `{config, contentElements}` | ### 7.4 Container payload ```jsonc { "type": "vitec_cols_33_66", "header": "…", "headerLayout": 2, "headerLink": "…", "gap": "3", "items": [ { "config": { "colPos": 251, "align": "stretch", "justify": "flex-start" }, "contentElements": [ /* normalised children, recursively */ ] }, { "config": { "colPos": 252, "align": "center", "justify": "space-between" }, "contentElements": [ … ] } ] } ``` - `gap` is a **parent‑level** property (whole‑grid gap). - `align`/`justify` are **per‑column** and are read from the parent record (`tx_vitec_col{N}_align/justify`); they therefore apply to **all** children of that column. These container‑only fields **shall not** appear in a child’s `data` (enforced by `CONTAINER_FIELDS` filtering). ### 7.5 Success Story (use case) detail payload Produced by `UsecaseSerializer::serializeDetail()` — the reference implementation of the centralised pattern (Clause 9.2): ```jsonc { "uid": 1, "title": "…", "slug": "…", "subtitle": "…", "teaser": "…", "cardImage": { "url": "…", "srcset": [ … ] }, "customerLogo": { … }, "hero": { "bgImage": …, "smallImage": …, "video": …, "overlayColor": "#000", "overlayOpacity": 0.4, "layout": "fullscreen", "textTheme": "light" }, "contentElements": [ /* inline CEs; the vitec_columns block is resolved specially */ ], "related": { "show": true, "market": {…}, "solutions": [ … ], "products": [ … ], "categories": [ … ] }, "seo": { "title": "…", "description": "…", "canonical": "…", "robots": { "noIndex": false, "noFollow": false }, "openGraph": { "title": …, "description": …, "image": … }, "twitter": { "title": …, "description": …, "image": … } }, "appearance": { "layoutVariant": "standard", "backgroundVariant": "none", "accentColor": "", "featured": false, … } } ``` SEO fields use fallback resolution (`seo_* → title/teaser`; `og_* → seo_* → title`; `twitter_* → og_*`; images `og_image → card_image → hero_bgimage`) so the front end always receives complete metadata. ### 7.6 News payload Produced by `NewsJsonRenderer` under `content.news`: ```jsonc { "mode": "list", // "list" | "detail" | "error" "items": [ { "uid": 1, "title": "…", "alternativeTitle": "…", "pathSegment": "…", "detailUrl": "/news/…", "canonicalUrl": "https://…", "teaser": "…", "bodytext": "…", "datetime": 1625097600, "categories": [ … ], "media": [ … ] } ], "settings": { "type": "news_pi1", "templateLayout": "1", "templateLayoutLabel": "Compact List", "detailPid": 45, … } } ``` Ten News CTypes (`news_pi1`, `news_newsdetail`, `news_newsliststicky`, …) share one renderer; `news_newsdetail` yields `mode: "detail"` with a single `news` object. ### 7.7 Content Blocks Content Blocks (`card`, `cta-banner`, `hero-section`, `intro-paragraph`, `video`, `faq`, `columns`) are serialised automatically by `nb-headless-content-blocks`: its `ContentBlocksJsonDataProcessor` converts the resolved record via `RecordToArray`, dropping system fields and recursing into files (→ URL + metadata), collections (e.g. `faq_items` → array of child records) and typolinks. The output uses the lean envelope (`{id, type, colPos, sorting, appearance, data}`). An optional per‑block `headless.php` hook may transform the array (none are currently defined). Fields carry the `vitec_` vendor prefix in storage. The **`vitec_columns`** block is the single exception: because it is authored **inline inside a Success Story record**, its collection items are resolved explicitly by `UsecaseSerializer::resolveColumnsElement()` into `{ header…, layout, columns: { left: [], right: [] } }`, with the collection storage table discovered from TCA (`foreign_table`) rather than hard‑coded. --- ## 8 Content‑type catalogue | CType | TS pattern | Renderer / Processor | Payload key | Kind | |---|---|---|---|---| | `vitec_productlist` | `< lib.contentElementWithHeader` | ProductListJsonRenderer | `products` | list | | `vitec_productshow` | `< lib.…WithHeader` | ProductShowJsonRenderer | `product` | detail | | `vitec_usecaselist` | `< lib.…WithHeader` | UsecaseListJsonRenderer → **UsecaseSerializer** | `usecases` | list | | `vitec_usecaseshow` | `< lib.…WithHeader` | UsecaseShowJsonRenderer → **UsecaseSerializer** | `usecase` | detail | | `vitec_marketshow` | `< lib.…WithHeader` | MarketShowJsonRenderer | `market` | detail | | `vitec_solutionshow` | `< lib.…WithHeader` | SolutionShowJsonRenderer | `solution` | detail | | `vitec_downloadcard` | `< lib.…WithHeader` | DownloadcardJsonRenderer | `downloadcard` | detail | | `vitec_downloadcardcollection` | `< lib.…WithHeader` | DownloadcardcollectionJsonRenderer | `downloadcardcollection` | list | | `vitec_datasheets` | `< lib.…WithHeader` | DatasheetsJsonRenderer | `datasheets` | list | | `vitec_eventlist` | `=< lib.…WithHeader` ⚠ | EventlistJsonRenderer | `eventlist` | list | | `news_pi1` (+9 variants) | `=< lib.…WithHeader`; variants `<` | NewsJsonRenderer | `news` | list/detail | | `vitec_cols_50_50` | `= JSON` | ContainerChildrenProcessor | `items` | container | | `vitec_cols_33_66 / 66_33 / 33_33_33 / 25_25_25_25` | `< vitec_cols_50_50` | ContainerChildrenProcessor | `items` | container | | `vitec_container` | `< vitec_cols_50_50` | ContainerChildrenProcessor | `items` | container (1 col) | | `vitec_cards_carousel` | `< vitec_cols_50_50` | ContainerChildrenProcessor | `items` + `carousel` | container | | Content Blocks (`vitec_card`, …) | Content Blocks + nb‑headless | — | (auto) | element | | `vitec_columns` (CB, inline) | Content Blocks | UsecaseSerializer (special) | `columns` | element | ⚠ = uses the reference operator `=<`; see Annex B‑1. --- ## 9 Conventions (normative) These rules define the **single, uniform way** to implement and extend headless renderers. New code **shall** comply; existing code **should** be aligned when touched. ### 9.1 Renderer class shape A payload renderer **shall**: 1. reside in `Classes/UserFunc/` and be named `JsonRenderer`; 2. expose `#[AsAllowedCallable] public function render(string $content, array $conf): string`; 3. expose `public function renderForRecord(array $row): string` for reuse by containers/resolvers (Clause 9.3); 4. return a JSON **string** (never an array/object). ### 9.2 Serialisation ownership Domain‑to‑JSON conversion **should** live in a `Classes/Service/*Serializer` class, and the renderer **should** be a thin wrapper around it. `UsecaseSerializer` is the reference implementation. New list/detail pairs **shall** share one serializer. ### 9.3 Dual‑entry pattern `render()` **shall** handle top‑level invocation: use `$this->cObj->data` when it is the plugin’s own row, otherwise perform page discovery (Clause 9.4). `renderForRecord()` **shall** accept an explicit `tt_content` row and be free of page/context assumptions, so it can be called by `ContainerChildrenProcessor` and `ContentElementResolver`. ### 9.4 Page‑id discovery Renderers **shall** resolve the current page id in this order: ```php $id = $GLOBALS['TYPO3_REQUEST']?->getAttribute('frontend.page.information')?->getId() ?? 0; if ($id <= 0) { $id = (int)($GLOBALS['TSFE']->id ?? 0); } // fallback ``` Reliance on `$GLOBALS['TSFE']->id` alone is **prohibited** (it is frequently `null` in the JSON cObject context). ### 9.5 Fail‑soft error handling The body of `renderForRecord()` **shall** be wrapped in `try { … } catch (\Throwable $e) { return ''; }`. Diagnostic output **may** be emitted only when a FlexForm `debug` flag is set. A failing element **shall not** propagate an exception to the page. ### 9.6 Naming - List payload keys **shall** be plural; detail keys **shall** be singular. - JSON property names **shall** be `camelCase` for computed/composed fields; raw passthrough fields in `data` retain their database names. ### 9.7 Nested‑plugin registration A plugin that may appear inside a container **shall** be registered in the `PLUGIN_RENDERERS` map in **both** `ContentElementResolver` and `ContainerChildrenProcessor`, as `CType => [RendererClass::class, 'jsonKey']`. > **NOTE** The duplicated map is a known maintenance hazard (10.3). Until it is > centralised, both copies **shall** be kept in sync. ### 9.8 Header section Every custom CE/plugin/container **shall** expose the standard header section (the core `headers` palette; Content Blocks use a `header_section` palette). See the companion "header convention". Header fields in JSON use the names `header`, `subheader`, `headerLayout`, `headerPosition`, `headerLink`. --- ## 10 Maintainability and upgrade‑safety (ISO 25010) This clause records the quality characteristics *maintainability* and *portability* and the concrete risks and rules that preserve them. ### 10.1 Modularity — current state Strengths: uniform envelope, dual‑entry pattern, exception safety, a shared `PLUGIN_RENDERERS` map, and the `UsecaseSerializer` reference pattern. Weakness: **substantial duplication** across the inline renderers. ### 10.2 Reusability — duplication register The following logic is duplicated across many renderers and **should** be extracted into shared services (target design in parentheses): | Duplicated logic | Occurrences | Target service | |---|---|---| | FAL image/`srcset` resolution | Product(List/Show), Market, Solution, Event, Datasheets | `FalImageResolver` | | FAL video resolution | Product, Usecase, hero | `FalImageResolver::video()` | | `sys_category` MM query | ≥ 9 renderers | `CategoryResolver` | | Custom MM (product↔download, …) | ≥ 5 renderers | `RelationResolver::resolveMany()` | | Download file‑by‑convention | 5 renderers | `ConventionFileResolver` | | `letterSequenceToRank()` sort helper | 5 renderers | static utility | | Page‑id discovery | all renderers | `PageIdResolver::resolve()` | > **RULE** When a shared resolver service exists, new renderers **shall** use it and > **shall not** re‑implement the logic inline. ### 10.3 Analysability — single sources of truth - The `PLUGIN_RENDERERS` map exists in two files (10.2/9.7); it **should** be promoted to one shared constant/class. - Table and field names are string literals scattered across renderers. New code **shall** define table/field names as **class constants** (as `UsecaseSerializer` and the processors already do) to localise upgrade impact. ### 10.4 Portability — upgrade‑sensitivity points | Point | Risk | Mitigation | |---|---|---| | `friendsoftypo3/headless ^5.0@rc` | RC; `lib.contentElement(WithHeader)` shape may change | Pin exact RC; re‑verify envelope after any bump; keep payloads decoupled (P2) | | `nb-headless-content-blocks ^0.0.x` | pre‑1.0; CB→JSON shape and collection storage may change | `vitec_columns` resolution reads the table from TCA (`foreign_table`) — do **not** hard‑code CB tables | | `georgringer/news ^14` | 10 News CTypes hard‑mapped in `NewsJsonRenderer` | Keep the CType→layout map in one place; re‑verify on major news upgrade | | `b13/container ^3.1` | child linkage via `tx_container_parent`; page‑module grid required | Documented limitation: containers cannot be authored inside IRRE (Annex B‑2) | ### 10.5 Modifiability — mandatory rules distilled 1. Container CTypes **shall** use `<` (copy), never `=<` (reference) — see Annex B‑1. 2. Table/field names **shall** be class constants, not inline literals. 3. Renderers **shall** reuse shared resolver services once they exist. 4. Magic numeric literals (e.g. a hard‑coded parent‑category uid) **shall** be replaced by named constants or configuration. --- ## 11 Conformance An implementation conforms to this specification if, for every content type it exposes: - **C1** the output validates as RFC 8259 JSON and matches the envelope of 7.2; - **C2** the responsible renderer satisfies the class shape of 9.1 and the dual‑entry pattern of 9.3; - **C3** page‑id discovery follows 9.4 and error handling follows 9.5; - **C4** payload keys follow 9.6 and the header section follows 9.8; - **C5** container derivation follows the `<`‑copy rule of 6.4/10.5(1); - **C6** any container‑nestable plugin is registered per 9.7. Deviations are recorded in Annex B and **shall** carry a remediation plan. --- ## Annex A (normative) — Checklist: adding a new headless plugin 1. **Model/TCA/SQL** — create the domain table and TCA; define table/field names as constants. 2. **Serializer** — add `Classes/Service/Serializer` with `serializeListItem()` and/or `serializeDetail()`; reuse existing resolver services. 3. **Renderer** — add `Classes/UserFunc/JsonRenderer` per 9.1, delegating to the serializer; implement `render()` (9.3/9.4) and `renderForRecord()`. 4. **TypoScript** — in `Configuration/Sets/Vitecset/setup.typoscript`: `tt_content. < lib.contentElementWithHeader` and `content.fields. = USER` + `.userFunc = …->render`. 5. **Header section** — ensure the `headers` palette is present (9.8). 6. **Nesting** — if the plugin may sit inside a container, register it in `PLUGIN_RENDERERS` in **both** the resolver and the processor (9.7). 7. **Deploy** — `vendor/bin/typo3 database:updateschema "*.add,*.change"` then `vendor/bin/typo3 cache:flush`. 8. **Verify** — fetch the page JSON; confirm envelope (C1), keys (C4) and nested output. --- ## Annex B (informative) — Nonconformity and technical‑debt register The following items were identified during architecture analysis. Items marked *(to verify)* were reported by static review and **shall** be confirmed before remediation. - **B‑1 — `=<` on Event/News CTypes.** `vitec_eventlist` and `news_pi1` use the reference operator `=< lib.contentElementWithHeader`. Reference *to a `lib.*` object* is used by headless itself and is generally safe; however, for consistency with 10.5(1) these are VERIFIED SAFE and left as-is (references to lib.* are idiomatic in headless; only tt_content-to-tt_content references are unsafe, and those are already `<` for containers). - **B‑2 — Containers cannot be authored inline (IRRE).** b13 container children live in `tx_container_parent` and require the page‑module grid; they cannot be created inside an inline field. This is a platform limitation, not a defect. The Success Story "Columns" Content Block (`vitec_columns`) is the sanctioned inline alternative. - **B‑3 — Duplicated `PLUGIN_RENDERERS` map** in `ContentElementResolver` and `ContainerChildrenProcessor` (9.7/10.3). - **B‑4 — Inline duplication** of image/category/MM/page‑id logic (10.2). - **B‑5 — Hard‑coded literals** — model/MM table names, a `/fileadmin` prefix for event JSON‑LD image URLs (`PageJsonLdRenderer`), and at least one magic parent‑category uid appear as inline literals across download/datasheet/structured‑data code *(to verify and extract to constants)*. `COLPOS_TO_COLUMN` in `ContainerChildrenProcessor` must be kept in sync with the container TCA colPos values. - **B‑6 — `DownloadcardcollectionJsonRenderer`**: two misplaced thumbnail output lines referenced an undefined variable in the image resolver. FIXED 2026-07-09 (removed; the PDF-thumbnail method is unaffected). - **B‑7 — v13→v14 `CType`/`list_type` compatibility branches** in some download/ datasheet renderers FIXED 2026-07-09: the unsatisfiable legacy OR branch was removed; the query now filters on the v14 CType only. --- ## Annex C (informative) — Related documents - `Documentation/HeadlessIntegration.md` — informal how‑to (superseded as the authoritative reference by this document). - Header convention — the uniform header section across CEs, plugins and containers. - `Configuration/Sets/Vitecset/setup.typoscript` — the single TypoScript entry point. *End of document EVO‑VITEC‑HL‑001 v1.0.*