620 lines
29 KiB
Markdown
Executable File
620 lines
29 KiB
Markdown
Executable File
# 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.<key>
|
||
▼
|
||
[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.<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 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://…",
|
||
"<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": { /* 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 `<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
|
||
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/<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 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.*
|