Files
VITEC-website/packages/vitec/Documentation/Headless-JSON-Architecture.md
2026-07-09 10:10:05 +02:00

620 lines
29 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# VITEC Headless JSON Architecture — Software Architecture and Interface Specification
| | |
|---|---|
| **Document identifier** | EVOVITECHL001 |
| **Version** | 1.0 |
| **Status** | Released |
| **Date** | 20260709 |
| **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 howto.
## 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 **contentelement 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 upgradesafety requirements (Clause 10);
- conformance criteria (Clause 11).
### 1.2 Out of scope
Frontend (React) implementation, hosting/deployment, the editorial (backend) TCA
form design except where it determines JSON output, and nonheadless (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 86011, *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 contentelement 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 domainspecific 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 | Manytomany 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 | PagetoJSON 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 projects custom extension |
> **NOTE** `friendsoftypo3/headless` is pinned to a **release candidate** (`^5.0@rc`).
> This is an upgradesensitivity 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 mixedmode 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 toplevel 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 topdown through the following layers.
```
HTTP request (Accept: application/json, headless:1)
[L1] Page renderer friendsoftypo3/headless
│ builds { meta, content[], … , jsonLd }
[L2] Contentelement 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.<key>
[L4] Containers tt_content.vitec_cols_* = JSON
│ ContainerChildrenProcessor → items[].contentElements[]
[L5] Normalisation & nesting ContentElementResolver / PLUGIN_RENDERERS
[L6] Structured data (JSONLD) 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 JSONLD
graph. VITEC does not replace this layer; it contributes elements to `content[]`
(L2L5) and the `jsonLd` field (L6).
### 6.2 L2 — Contentelement envelope
Each CType is bound to a headless library object:
```typoscript
tt_content.<ctype> < 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.<key>`:
```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 selfcontained 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 (wholegrid 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 B1).
### 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 (JSONLD)
`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 newsdetail 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 hardcoded `/fileadmin` prefix (see 10.3 / Annex B5).
---
## 7 JSON interface specification
### 7.1 Encoding
Output **shall** be RFC 8259 JSON, UTF8. Timestamps **shall** be Unix epoch seconds
(integer); where ISO 8601 strings are required by schema.org they are produced inside
the JSONLD layer.
### 7.2 Contentelement 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://…",
"<key>": { /* 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": { /* nonsystem 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 **parentlevel** property (wholegrid gap).
- `align`/`justify` are **percolumn** and are read from the parent record
(`tx_vitec_col{N}_align/justify`); they therefore apply to **all** children of that
column. These containeronly fields **shall not** appear in a childs `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 perblock
`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 hardcoded.
---
## 8 Contenttype 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 + nbheadless | — | (auto) | element |
| `vitec_columns` (CB, inline) | Content Blocks | UsecaseSerializer (special) | `columns` | element |
⚠ = uses the reference operator `=<`; see Annex B1.
---
## 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 `<Domain><Kind>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
DomaintoJSON 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 Dualentry pattern
`render()` **shall** handle toplevel invocation: use `$this->cObj->data` when it is the
plugins 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 Pageid 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 Failsoft 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 Nestedplugin 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 upgradesafety (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, dualentry 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 filebyconvention | 5 renderers | `ConventionFileResolver` |
| `letterSequenceToRank()` sort helper | 5 renderers | static utility |
| Pageid discovery | all renderers | `PageIdResolver::resolve()` |
> **RULE** When a shared resolver service exists, new renderers **shall** use it and
> **shall not** reimplement 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 — upgradesensitivity points
| Point | Risk | Mitigation |
|---|---|---|
| `friendsoftypo3/headless ^5.0@rc` | RC; `lib.contentElement(WithHeader)` shape may change | Pin exact RC; reverify envelope after any bump; keep payloads decoupled (P2) |
| `nb-headless-content-blocks ^0.0.x` | pre1.0; CB→JSON shape and collection storage may change | `vitec_columns` resolution reads the table from TCA (`foreign_table`) — do **not** hardcode CB tables |
| `georgringer/news ^14` | 10 News CTypes hardmapped in `NewsJsonRenderer` | Keep the CType→layout map in one place; reverify on major news upgrade |
| `b13/container ^3.1` | child linkage via `tx_container_parent`; pagemodule grid required | Documented limitation: containers cannot be authored inside IRRE (Annex B2) |
### 10.5 Modifiability — mandatory rules distilled
1. Container CTypes **shall** use `<` (copy), never `=<` (reference) — see Annex B1.
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 hardcoded parentcategory 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 dualentry
pattern of 9.3;
- **C3** pageid 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 containernestable 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/<Domain>Serializer` with `serializeListItem()`
and/or `serializeDetail()`; reuse existing resolver services.
3. **Renderer** — add `Classes/UserFunc/<Domain><Kind>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.<ctype> < lib.contentElementWithHeader` and
`content.fields.<key> = 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 technicaldebt register
The following items were identified during architecture analysis. Items marked
*(to verify)* were reported by static review and **shall** be confirmed before
remediation.
- **B1 — `=<` 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).
- **B2 — Containers cannot be authored inline (IRRE).** b13 container children live in
`tx_container_parent` and require the pagemodule 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.
- **B3 — Duplicated `PLUGIN_RENDERERS` map** in `ContentElementResolver` and
`ContainerChildrenProcessor` (9.7/10.3).
- **B4 — Inline duplication** of image/category/MM/pageid logic (10.2).
- **B5 — Hardcoded literals** — model/MM table names, a `/fileadmin` prefix for
event JSONLD image URLs (`PageJsonLdRenderer`), and at least one magic
parentcategory uid appear as inline literals across download/datasheet/structureddata
code *(to verify and extract to constants)*. `COLPOS_TO_COLUMN` in
`ContainerChildrenProcessor` must be kept in sync with the container TCA colPos values.
- **B6 — `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).
- **B7 — 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 howto (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 EVOVITECHL001 v1.0.*