Checkpoint: headless JSON architecture documented + cleanup (Stufe 1+2)

Restore point before Stufe 3 (central resolver-service refactoring).

Includes this session's work:
- Success Story (usecase) rebuild: new fields/tabs, UsecaseSerializer,
  thin List/Show renderers, MM relations, inline content elements.
- vitec_columns content block (two-column layout with per-item content)
  incl. unified header section; special-cased JSON resolution.
- Container per-column flex (items[].config align/justify) + gap on parent.
- Unified header section across CEs/plugins/containers; header fields in JSON.
- ISO-style architecture spec: Documentation/Headless-JSON-Architecture.md.
- Cleanup: removed local scratch + verified .bak backups.
- Fixes: B-6 (undefined thumbnail variable in Downloadcardcollection image
  resolver), B-7 (unsatisfiable legacy CType OR branch in Downloadcard/Datasheets).

Rollback: git reset --hard snapshot-2026-07-09-pre-stufe3

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
o-rasche
2026-07-09 10:01:56 +02:00
parent c6759186cb
commit bb7c03235d
237 changed files with 6698 additions and 1749 deletions

View File

@@ -0,0 +1,619 @@
# 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.*