# VITEC Headless JSON Architecture — Software Architecture and Interface Specification | | | |---|---| | **Document identifier** | EVO‑VITEC‑HL‑001 | | **Version** | 1.7 | | **Status** | Released | | **Date** | 2026‑08‑05 | | **Applies to** | `evomedien/vitec` on TYPO3 v14.3 (headless) | | **Owner** | evomedien — VITEC relaunch | **Revision history** | Version | Date | Changes | |---|---|---| | 1.0 | 2026‑07‑09 | Initial released specification. | | 1.1 | 2026‑08‑05 | Catalogue completed with the card, customer‑logo, location and form plugins (7.8–7.11, 8). Page‑level auxiliary fields — menus and favicons — specified (6.7, 7.12). Form‑submission endpoint added to the interface (7.8.2). Crop‑variant and debug‑flag conventions added (9.9, 9.10). News CType count corrected to nine and the `=<` marking on `news_pi1` withdrawn (7.6, 8, B‑1). Nonconformities B‑8…B‑10 recorded. | | 1.2 | 2026‑08‑05 | **Interface change (additive):** `tx_vitec_domain_model_market` and `tx_vitec_domain_model_solution` gained a `slug` field, now emitted by `MarketShowJsonRenderer`, `SolutionShowJsonRenderer` and the card payload. Market and Solution detail payloads specified (7.13); 7.9 updated. | | 1.3 | 2026‑08‑05 | **New plugin** `vitec_marketlist` (`MarketListJsonRenderer`, payload key `markets`) — first list plugin built entirely on the shared serializer per 9.2, with editor‑controlled selection and ordering. Clause 7.13 restructured into detail (7.13.1) and list (7.13.2); catalogue updated. | | 1.4 | 2026‑08‑05 | **Interface change (additive):** `tx_vitec_domain_model_market` gained a `detail_page` field (TCA `group`/`pages`), emitted as the **resolved** `detailUrl` in both market payloads (7.13.1, 7.13.2). Not added to Solution — see B‑11. | | 1.5 | 2026‑08‑05 | **Defect fix, output‑changing:** richtext fields were emitted as raw database content by every VITEC UserFunc renderer, leaving `t3://` links unresolved in the JSON. New `RteResolver` service and mandatory convention 9.11; applied at all 19 richtext call sites across 11 renderers. Duplication register 10.2 updated. | | 1.6 | 2026‑08‑06 | **Interface change (additive):** `tx_vitec_domain_model_download` gained a second category field `type` (display taxonomy; MM rows distinguished by `fieldname`), emitted as the string `type` in the downloadcard, downloadcardcollection and datasheets payloads — analogous to `filetype`. New CLI command `vitec:import-downloads` migrates the old‑site downloads (Collateral directory only; idempotent by slug; files fetched resumably; duplicate `file_url`s merged). | | 1.7 | 2026‑08‑06 | **Robustness:** the import normalizes legacy filenames on fetch so every imported file matches the version convention (`__NN_A` → `__NN-A`, `___NN` → `__NN`, `__NNA` → `__NN-A`, bare `__NN` → `__NN-A` as initial revision), and the three download renderers gained a `filepath` fallback in `getDownloadFile()` (FAL → convention → filepath) as a safety net for anything that still escapes it. Extends the B‑4 duplication (three copies of the fallback) — consolidation target remains a shared file‑resolver service (10.2). | 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, cards, customer logos, locations, forms) 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. Alongside `content[]` the page object carries **page‑level auxiliary fields** — the navigation menus, the favicon set and the schema.org graph (Clause 6.7). One **write‑side** endpoint complements the read interface: form submissions are POSTed back to the CMS (Clause 7.8.2). --- ## 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, page‑level auxiliary fields and the form‑submission endpoint (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 Frontend middlewares Two middlewares are registered in `Configuration/RequestMiddlewares.php`, both after `typo3/cms-core/normalized-params-attribute` and before `typo3/cms-frontend/site` — that is, **before page resolution**: | Middleware | Purpose | |---|---| | `vitec/form-submission` | Answers `POST /api/vitec/form/` (Clause 7.8.2). Every other request passes through untouched. | | `vitec/success-story-path-rewrite` | Rewrites `/success-stories/` internally to `/success-stories/story/` when `` matches a visible Success Story. The page router would otherwise always resolve the public SEO URL to the list page (longest page‑slug prefix), so the detail subpage could never answer it. The browser URL is unchanged; a non‑matching slug leaves the request untouched; any exception leaves the request untouched. | ### 5.4 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 ▼ [L7] Page‑level fields menus (MenuProcessor) + favicons ``` ### 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). ### 6.7 L7 — Page‑level auxiliary fields Beyond `content[]` and `jsonLd`, four further fields are attached to the page object in `Configuration/TypoScript/Headless/vitec_menus.typoscript`: ```typoscript page.10.fields.mainNavigation =< lib.mainNavigation page.10.fields.footerMenu =< lib.footerMenu page.10.fields.metaMenu =< lib.metaMenu page.10.fields.favicons = USER page.10.fields.favicons.userFunc = Evomedien\Vitec\UserFunc\FaviconsJsonRenderer->render ``` The three menus are built by `FriendsOfTYPO3\Headless\DataProcessing\MenuProcessor`, which resolves shortcut pages to their target, skips pages with `nav_hide = 1`, marks active/current items and nests sub‑levels under `children`. `mainNavigation` is the full hierarchy (`levels = 10`, `expandAll = 1`); `footerMenu` and `metaMenu` are curated flat lists driven by the site settings `menu.footer.pageUids` and `menu.meta.pageUids`. All three use the title field `nav_title // title` and exclude spacers. > **NOTE** These menus use the reference operator `=<` against `lib.*` objects. That > is the idiomatic headless form and is safe; the `<`‑copy rule of 6.4/10.5(1) > constrains `tt_content → tt_content` derivations only. Payloads are specified in Clause 7.12. --- ## 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). - `tx_vitec_bg_variant` is passed through unchanged (background variant of the grid). - `vitec_container` and `vitec_cards_carousel` **drop** `gap` (single column / single track) and add `cssClass`, read from the FlexForm `settings.cssClass`. `vitec_cards_carousel` additionally emits a `carousel` object from its FlexForm: ```jsonc { "carousel": { "slidesPerView": "3", "showArrows": "1", "showIndicators": "1", "loop": "0", "autoplay": "1", "autoplayInterval": "5000" } } ``` > **NOTE** These members are TypoScript `TEXT` values and therefore arrive as > **strings**, not numbers or booleans. The front end coerces them. The `colPos` value of each column is fixed by `COLPOS_TO_COLUMN` in `ContainerChildrenProcessor` — 211/212 (50/50), 221–223 (33/33/33), 231–234 (25/25/25/25), 241/242 (66/33), 251/252 (33/66) — and **shall** be kept in sync with the container TCA (Annex B‑5). ### 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, … } } ``` Nine News CTypes share this one renderer — `news_pi1` plus the eight variants `news_newsliststicky`, `news_newsselectedlist`, `news_newsdetail`, `news_newsdatemenu`, `news_categorylist`, `news_newssearchform`, `news_newssearchresult`, `news_taglist`. `news_newsdetail` yields `mode: "detail"` with a single `news` object. `news_pi1` is derived with the `<` copy operator from `lib.contentElementWithHeader`, the eight variants with `<` from `tt_content.news_pi1`. ### 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. ### 7.8 Forms #### 7.8.1 Form plugin payload Produced by `FormsJsonRenderer` under `content.form` for all three form CTypes (`vitec_contactform`, `vitec_demoform`, `vitec_helpdeskform`); the CType selects the definition through `FormDefinitions::CTYPE_MAP`: ```jsonc { "formKey": "contact", // contact | demo | helpdesk "title": "Contact VITEC", "endpoint": "/api/vitec/form/contact", "honeypot": "_website", "fields": [ { "name": "firstName", "type": "text", "label": "First Name", "required": true }, { "name": "email", "type": "email", "label": "Email", "required": true }, { "name": "country", "type": "select", "label": "Country", "required": false, "optionsSource": "countries" }, { "name": "solution", "type": "select", "label": "Solution of Interest", "required": false, "options": [ "IPTV Distribution", "…" ] } ] } ``` Field `type` is one of `text`, `email`, `tel`, `select`, `textarea`. A `select` carries either an inline `options` array **or** an `optionsSource` key (`countries`, `usStates`) naming a list the front end supplies itself — this keeps long ISO lists out of every page response and consistent across the app. `contact` and `demo` share one field set; `helpdesk` has its own, adding `product` and `serialNumber`. `Classes/Forms/FormDefinitions.php` is the **single source of truth**: the same definition produces this JSON *and* validates the submission server‑side. Fields **shall** be added there and nowhere else. #### 7.8.2 Submission endpoint (write side) `FormSubmissionMiddleware` (5.3) answers: ``` POST /api/vitec/form/ Content-Type: application/json ``` Processing order: honeypot check → validation → persistence → delivery. | Situation | HTTP | Body | |---|---|---| | Accepted — including honeypot tripped and delivery failure | 200 | `{"success": true}` | | Validation failed | 422 | `{"success": false, "errors": {"": ""}}` | | Unknown `formKey` | 404 | `{"success": false, "errors": {"_form": "Unknown form"}}` | | Method not POST | 405 | `{"success": false, "errors": {"_form": "POST only"}}` | | Unexpected error | 500 | `{"success": false, "errors": {"_form": "Unexpected error"}}` | Every accepted submission is written to `tx_vitec_form_submission` (`form_key`, `payload` as JSON, `delivery_method`, `delivery_status` ∈ {`pending`, `sent`, `failed`}, `delivery_error`) **before** delivery is attempted. Delivery therefore cannot lose data: a failed delivery still answers `success: true` and the failure is visible on the backend record. Unknown payload keys are dropped by `FormDefinitions::filterPayload()`; a filled honeypot field (`_website`) is answered with `success: true` and stored nowhere. Delivery is a strategy (`Classes/Forms/Delivery/DeliveryInterface`), selected by the FlexForm setting `delivery` (default `email`): | Strategy | State | |---|---| | `EmailDelivery` | active | | `SalesforceDelivery` | **prepared stub** — `deliver()` always throws. A submission with `delivery = salesforce` is stored and then marked `delivery_status = failed`; the endpoint still answers `success: true`, so no data is lost. The previous website posted to Salesforce Web‑to‑Lead; completing it requires the credentials plus the camelCase → Salesforce field‑id mapping. | > **NOTE (design constraint)** Delivery settings are read from the **first** > non‑deleted, non‑hidden plugin element of that CType found site‑wide — the endpoint > is stateless and receives no element uid. There is therefore **one delivery > configuration per form type**, not per placed element (Annex B‑10). ### 7.9 Card payload (`vitec_modelcard`) One plugin serves four model types. The FlexForm picks `modelType` plus one record, and **all** card data comes from that record — nothing is authored on the element: ```jsonc { "modelType": "product", // product | story | market | solution "layout": "vertical", "item": { "uid": 12, "title": "…", "slug": "…", "subtitle": "…", "teaser": "…", "image": { "url": "…", "srcset": [ … ] } } } ``` `item` is `null` when no record is selected or the record is hidden/deleted. Per model type: `story` reuses `UsecaseSerializer::serializeListItem()` verbatim (the canonical card serialisation); `product` falls back from the `image` field to `productimage`; `market` and `solution` share one field set and add `description`. **All four model types carry `slug`** (since v1.2), so the front end can build a detail link from any card without a second request. Image resolution is delegated to `UsecaseSerializer::image()`. This renderer is the reference for reusing a serializer instead of re‑implementing FAL logic (9.2). Model table names are held in the class constant `MODEL_TABLES` (10.3). ### 7.10 Locations payload (`vitec_locationlist`) Produced by `LocationsJsonRenderer` under `content.locations`. The plugin is **global**: every visible `tx_vitec_domain_model_location` record is emitted, ordered by `sorting`; the page carrying the plugin is not used as a filter. ```jsonc { "variant": "grid", // grid | list | map "mapText": "

", // only when variant = "map" and text is set, else null "locations": [ { "id": 3, "slug": "…", "name": "…", "countryCode": "DE", "coordinates": { "latitude": 50.1, "longitude": 8.6 }, "address": { "company": …, "street": …, "additional": …, "postalCode": …, "city": …, "region": …, "country": … }, "contact": { "phone": …, "fax": …, "email": … }, "links": { "contact": "/contact", "legal": [ "/imprint", "…" ] }, "marker": { "label": "…", "color": "#ff6633", "size": 0.5 }, "sorting": 1, "active": true } ] } ``` Empty `address` and `contact` members are `null`, never `""`, so the front end can test presence directly. `links.contact` is a resolved typolink; `links.legal` is a newline‑separated list of typolinks, each resolved individually. `marker.label` falls back to the location name, `marker.color` to `#ff6633`, `marker.size` to `0.5`. ### 7.11 Customer‑logo payload (`vitec_customerlogos`) Produced by `CustomerlogosJsonRenderer` under `content.customerlogos`: ```jsonc { "layout": "grid", // list | grid | carousel | marquee "logos": [ { "id": 7, "name": "…", "emphasized": false, "color": true, "logo": { "uid": 42, "url": "…", "title": "…", "alternative": "…", "srcset": [ … ], "properties": { "mimeType": "…" } } } ] } ``` Selection semantics — normative for the front end: | FlexForm state | Result | |---|---| | no customers selected | **all** logos, each `color: false` (render black‑and‑white) | | customers selected | the selected ones **first**, in selection order, `color: true`; then all remaining logos, `color: false` | | customers selected + `onlySelected` | only the selected ones, `color: true` | `color` is therefore a *rendering hint*, not a property of the record. SVG logos are delivered unprocessed with an empty `srcset`; raster logos receive a WebP `srcset`. ### 7.12 Page‑level auxiliary payloads Attached to the page object, not to a content element (Clause 6.7). **`mainNavigation` / `footerMenu` / `metaMenu`** — arrays of headless `MenuProcessor` items (`title`, `link`, `active`, `current`, `spacer`, `children[]`). **`favicons`** — a static, page‑independent descriptor set produced by `FaviconsJsonRenderer`, so the head tags are CMS‑driven instead of hard‑coded in React: ```jsonc { "themeColor": "#26358C", "manifest": "/fileadmin/icons/site.webmanifest", "links": [ { "rel": "icon", "type": "image/x-icon", "href": "/fileadmin/icons/favicon.ico" }, { "rel": "icon", "type": "image/png", "sizes": "32x32", "href": "…" }, { "rel": "icon", "type": "image/png", "sizes": "16x16", "href": "…" }, { "rel": "apple-touch-icon", "sizes": "180x180", "href": "…" }, { "rel": "manifest", "href": "…" } ] } ``` `links` is ordered most‑ to least‑specific and is meant to be rendered verbatim as `` elements. Hrefs are root‑relative under `/fileadmin/icons/` — the same convention as image URLs (`UsecaseSerializer::image()`). **If the front end is ever served from an origin other than the CMS, it must prefix these hrefs with the CMS base URL**, exactly as it does for image URLs. ### 7.13 Market and Solution payloads #### 7.13.1 Detail payloads `MarketShowJsonRenderer` and `SolutionShowJsonRenderer` emit the same shape under `content.market` and `content.solution` respectively — the two models are field‑for‑field identical: ```jsonc { "uid": 4, "title": "…", "slug": "…", "subtitle": "…", "teaser": "…", "description": "

", // RTE HTML "detailUrl": "/markets/aviation/", // market only, null when unset "categories": [ { "uid": 9, "title": "…", "description": "…" } ], "image": { "uid": 12, "url": "…", "title": "…", "alternative": "…", "description": "…", "srcset": [ … ], "properties": { "width": 1920, "height": 1080, … } } } ``` `slug` was added in v1.2. It is generated from `title` with `eval: uniqueInPid`, exactly as on `product` and `usecase` (9.9 governs their images, 9.6 their naming), and is the routing handle for the `/markets//` and `/solutions//` URLs of the relaunch sitemap. `detailUrl` was added in v1.4 and exists on **Market only**. It is backed by the TCA field `detail_page` (`type: group`, `allowed: pages`, `maxitems: 1`) — the editor picks the page with the standard page browser. The renderer **shall not** expose the raw page uid: a headless front end cannot turn a uid into a link, so the value is resolved with `typoLink_URL()` server side, the same convention as `headerLink` (6.2) and the location links (7.10). It is `null` when no page is selected or the link cannot be resolved — never `0` and never an empty string, so the front end can test it directly. > **NOTE** Neither model is *routed* by slug yet: market and solution pages are still > resolved as ordinary TYPO3 pages carrying a `…show` plugin that selects one record via > its FlexForm. The field exists so the front end can build canonical URLs today, and so > slug‑based routing (a route enhancer, as `product` already has, or a middleware, as > Success Stories have) can be introduced later without a second data migration. #### 7.13.2 Market list payload (`vitec_marketlist`) `MarketListJsonRenderer` emits, under `content.markets`: ```jsonc { "layout": "grid", // grid | list | carousel "markets": [ { "uid": 4, "title": "…", "slug": "…", "subtitle": "…", "teaser": "…", "description": "

", // RTE HTML, resolved per 9.11 "detailUrl": "/markets/aviation/", // null when no detail page is set "image": { "url": "…", "srcset": [ … ] } } ] } ``` The page carrying the plugin is never used as a filter. Which markets appear, and in which order, is decided by the FlexForm field `settings.markets`: | FlexForm state | Result | |---|---| | nothing selected | **all** visible markets, alphabetical by `title` | | markets selected | exactly those, **in the order the editor arranged them** in the FlexForm | The second row is the load‑bearing one: `settings.markets` stores a comma‑separated uid list whose sequence *is* the intended display order. A `WHERE uid IN (…)` query returns rows in storage order and would silently discard it, so the renderer fetches the (small) table once and rebuilds the sequence in PHP — the same approach `CustomerlogosJsonRenderer` uses. Fetching everything also means a selected record that has since been hidden or deleted simply drops out instead of producing a gap or an error. `tx_vitec_domain_model_market` has **no `sorting` column**, which is why the unselected case falls back to alphabetical rather than to a backend‑defined order. Each item carries the **same field set as the detail payload** (7.13.1) apart from `categories`, so the front end can render a list item, a card and a detail header from one shape. `description` is included and is RTE HTML resolved per 9.11 — it was omitted in v1.3 on the assumption that lists only need `teaser`, which turned out to be wrong in practice. Image resolution is delegated to `UsecaseSerializer::image()` per 9.2; this renderer duplicates no FAL logic. A Solution list counterpart does not exist yet. When it is added it **should** reuse this shape under `content.solutions`. --- ## 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_marketlist` | `< lib.…WithHeader` | MarketListJsonRenderer | `markets` | list (global) | | `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 | | `vitec_locationlist` | `< lib.…WithHeader` | LocationsJsonRenderer | `locations` | list (global) | | `vitec_customerlogos` | `< lib.…WithHeader` | CustomerlogosJsonRenderer | `customerlogos` | list | | `vitec_modelcard` | `< lib.…WithHeader` | ModelcardJsonRenderer | `card` | detail (4 model types) | | `vitec_contactform` | `< lib.…WithHeader` | FormsJsonRenderer | `form` | form | | `vitec_demoform` | `< tt_content.vitec_contactform` | FormsJsonRenderer | `form` | form | | `vitec_helpdeskform` | `< tt_content.vitec_contactform` | FormsJsonRenderer | `form` | form | | `news_pi1` (+8 variants) | `< lib.…WithHeader`; variants `< tt_content.news_pi1` | 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 `=<` against a `lib.*` object; verified safe, see Annex B‑1. `vitec_eventlist` is the only remaining `=<` derivation among the content elements. **Page‑level fields** (not content elements): `jsonLd` (`PageJsonLdRenderer`), `favicons` (`FaviconsJsonRenderer`) and `mainNavigation` / `footerMenu` / `metaMenu` (headless `MenuProcessor`) — Clauses 6.7 and 7.12. **Registered but not headless‑enabled:** `vitec_simplecard` is registered as an Extbase plugin and offered in the content‑element wizard, but has neither a TypoScript mapping nor a renderer; placed on a page it emits a content element without a payload (Annex B‑8). --- ## 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`. ### 9.9 Image crop variants The "first image" of every card‑capable model (Product, Success Story, Market, Solution) **shall** declare its crop variants through the single definition `Evomedien\Vitec\Tca\CropVariants::firstImage()`, referenced from the model TCA: ```php 'cropVariants' => \Evomedien\Vitec\Tca\CropVariants::firstImage(), ``` It yields three editor tabs — `default` (free, 16:9, 4:3, 1:1; for detail, hero and list use), `card` (4:3) and `largeCard` (16:9); a free crop stays available in each. Crop variants **shall not** be redefined per model. ### 9.10 Debug flag A plugin FlexForm **may** expose a `settings.debug` checkbox. When it is set the renderer **shall** attach a `debug` member to its payload (typically `settings` plus a count or the resolved record uid) and **shall not** change the regular payload in any other way. This is the only sanctioned form of diagnostic output (9.5); `debug` is absent from production payloads. ### 9.11 Richtext fields Any field whose TCA carries `enableRichtext` **shall** be passed through `Evomedien\Vitec\Service\RteResolver::html()` before it enters a payload. Handing the raw database column to JSON is **prohibited**. ```php 'description' => RteResolver::html($market['description'] ?? ''), // correct 'description' => (string)($market['description'] ?? ''), // prohibited ``` **Rationale.** TYPO3 stores richtext with unresolved internal references — internal links as ``, legacy content as `` tags, images with relative paths. Fluid resolves these through `parseFunc` at render time. A headless renderer that skips that step ships dead links to the front end, and the defect is invisible until an editor actually places an internal link. The upstream packages already do this for the output they own: `friendsoftypo3/headless` applies `parseFunc =< lib.parseFunc_RTE` to the core text elements via TypoScript, and `nb-headless-content-blocks` calls `parseFunc($value, null, '< lib.parseFunc_RTE')` for every Content Block field with `enableRichtext`. `RteResolver` performs the identical call, so all three paths produce the same HTML. The resolver is **static** — the conversion is stateless and the call sites are payload array literals — and **fail‑soft** per 9.5: when no TypoScript setup is available it returns the raw value rather than throwing, so content is never lost. > **NOTE** `enableRichtext` **shall** be the boolean `true`, not the string `'true'`. > FormEngine accepts both, but `nb-headless-content-blocks` tests with `=== true` and a > string silently disables its richtext conversion. Three product fields carried the > string form until v1.5. Fields that merely *look* like richtext are out of scope: FAL metadata (`sys_file_reference.description`), `sys_category.description` and `sys_file` metadata are plain text and **shall not** be passed through the resolver. --- ## 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 — reused without duplication by `ModelcardJsonRenderer` (7.9), while `FormDefinitions` (7.8.1) is the equivalent single source of truth for the form plugins. Weakness: **substantial duplication** remains across the older inline renderers (Product, Download, Datasheet), which is why those are the largest files in the extension. ### 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, Customerlogos | `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()` | | RTE `parseFunc` conversion | 19 sites / 11 renderers | **`RteResolver::html()` — DONE (v1.5)** | > **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. **Registration** — `ExtensionUtility::configurePlugin()` in `ext_localconf.php`, a FlexForm under `Configuration/FlexForms/`, an icon, and a wizard entry in `Configuration/page.tsconfig`. 5. **TypoScript** — in `Configuration/Sets/Vitecset/setup.typoscript`: `tt_content. < lib.contentElementWithHeader` and `content.fields. = USER` + `.userFunc = …->render`. **Omitting this step yields a content element with no payload** (Annex B‑8). 6. **Header section** — ensure the `headers` palette is present (9.8). 7. **Nesting** — if the plugin may sit inside a container, register it in `PLUGIN_RENDERERS` in **both** the resolver and the processor (9.7). 8. **Deploy** — `vendor/bin/typo3 database:updateschema "*.add,*.change"` then `vendor/bin/typo3 cache:flush`. 9. **Verify** — fetch the page JSON; confirm envelope (C1), keys (C4) and nested output; confirm `debug` is absent unless the FlexForm flag is set (9.10). --- ## 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 the Event CType.** `vitec_eventlist` uses the reference operator `=< lib.contentElementWithHeader`. VERIFIED SAFE and left as is: a reference *to a `lib.*` object* is idiomatic in headless and used by headless itself; only `tt_content → tt_content` references are unsafe, and those are `<` copies throughout (containers, News variants, form variants). The page‑level menus use `=<` against `lib.*` objects for the same reason (6.7). **CORRECTION 2026‑08‑05:** v1.0 of this document also listed `news_pi1` here — `news_pi1` is, and was, derived with `<`. - **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). OPEN. Both copies verified in sync on 2026‑08‑05 — 25 entries each (16 VITEC CTypes + 9 News CTypes). Remediation: promote to a single shared constant. - **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`), the `/fileadmin/icons` prefix and the theme colour in `FaviconsJsonRenderer`, 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. - **B‑8 — `vitec_simplecard` has no headless rendering.** The plugin is registered in `ext_localconf.php`, has a FlexForm, Fluid templates and a wizard entry in `Configuration/page.tsconfig`, but there is no `tt_content.vitec_simplecard` mapping in `setup.typoscript` and no `*JsonRenderer`. Placed on a page it produces a content element without a payload. Decide: complete it per Annex A, or withdraw the plugin registration and the wizard entry. - **B‑9 — Stray files in the extension.** Four `*.bak.rebuild` files (`UsecaseListJsonRenderer`, `UsecaseShowJsonRenderer`, `tx_vitec_domain_model_usecase`, `ext_tables.sql`) and several tracked `.msys00000…` artefacts under `Classes/DataProcessing/` and `Classes/Service/`. They are never loaded, but they are indexed by IDEs and static analysis. Remove. - **B‑10 — One delivery configuration per form type.** `FormSubmissionMiddleware` reads the delivery settings from the first matching plugin element found site‑wide (7.8.2). Per‑element configuration would require the front end to submit the element uid and the endpoint to resolve it. This is a documented constraint, not a defect — but placing two elements of the same form type with different `delivery` settings is silently ineffective. - **B‑11 — `detail_page` exists on Market but not on Solution.** Introduced in v1.4 on `tx_vitec_domain_model_market` only, because that is what was requested. The two models are otherwise field‑for‑field identical (7.13.1) and their renderers share one payload shape, so the asymmetry is a latent inconsistency: `SolutionShowJsonRenderer` emits no `detailUrl` at all. Either add the field to Solution as well, or record the divergence as intended. Related: `ModelcardJsonRenderer` serves `market` and `solution` from one shared branch and therefore emits no `detailUrl` either — the card payload (7.9) cannot link to a market detail page until this is resolved. - **B‑12 — `detailUrl()` duplicated** in `MarketListJsonRenderer` and `MarketShowJsonRenderer`, and a third near‑identical `resolveLink()` lives in `LocationsJsonRenderer` (7.10). Three copies of "resolve a link server side" is the smallest concrete case of B‑4; it is the natural seed for the `LinkResolver` service that 10.2 calls for. --- ## 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.7.*