Files
VITEC-website/packages/vitec/Documentation/Headless-JSON-Architecture.md
T
orascheandClaude Opus 5.5 9211c8a395 Megamenu: rail display for page trees, cache tags, editorial promo boxes
- Page-tree entries get a "Page tree display" field (columns | rail |
  rail-compact), emitted as display + cardSize: Markets and Solutions
  show their main pages as a list and the sub-pages as image cards on
  hover. Page-tree items carry image (page media) and teaser (abstract)
  from stage 2. Additive; front ends ignoring display render columns.
- vitec:attach-page-media gains --depth=2 and falls back to the page's
  hero background before the grey placeholder; the 39 sub-solution
  pages got their hero image as page media.
- MegamenuJsonRenderer tags every page carrying the menu with the menu
  tables and tt_content_<uid>, so saving menu records refreshes all
  pages instead of waiting for a full flush.
- Products catalog: drop catalog.news; both promo boxes ("Product
  highlight", "Product news") are the entry's editorial teaser cards.
  ProvideInsightsSpotlight back to its previous state.
- Spec v1.29 and v1.30 (header version corrected), README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:35:23 +01:00

1709 lines
107 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# VITEC Headless JSON Architecture — Software Architecture and Interface Specification
| | |
|---|---|
| **Document identifier** | EVO‑VITEC‑HL‑001 |
| **Version** | 1.30 |
| **Status** | Released |
| **Date** | 2026‑11‑02 |
| **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). |
| 1.8 | 2026‑08‑13 | **Defect fix and interface change (additive).** Content Blocks never carried the Core *Appearance* tab: `layout`, `frame_class` — including the VITEC frame classes — `space_before_class`, `space_after_class`, `sectionIndex` and `linkToTop` were unreachable for editors on all nine blocks. Added centrally for every `vitec_*` type (7.7); the `appearance` envelope is unchanged, its values were merely always default. Side effect: those six columns now also appear raw inside `data` on top‑level blocks (B‑13), and `appearance.layout` is represented differently on the two envelope paths (B‑14). `intro-paragraph` gained `background_color` (7.7). `vitec_eventlist` gained the layout `regions`, emitting a `regions` array built from the region categories below parent 104; the event payload is specified for the first time (7.14). B‑11 and B‑12 recorded as resolved. |
| 1.9 | 2026‑08‑15 | **Interface change, partly breaking.** The four list plugins were unified: every one of them now emits an object carrying `layout`, the new `showToolbar` flag and its payload array. `vitec_usecaselist` and `vitec_productlist` previously emitted a **bare array** — front ends reading them have to move one level down (7.3, 7.14, 8). New plugin **`vitec_solutionlist`** (`SolutionListJsonRenderer`, key `solutions`), the counterpart 7.13.2 had been asking for since v1.2. `tx_vitec_domain_model_usecase` gained `detail_page`, emitted as the resolved `detailUrl` in the story card shape and therefore also in the `vitec_modelcard` story branch, which had carried no link at all until now. Noted: `vitec_productlist` had a configurable `layout` that was never serialised, and its vocabulary (`0`–`3`) differs from the other lists. |
| 1.10 | 2026‑08‑18 | **Interface change (additive).** `tx_vitec_domain_model_product` gained five fields: `heroimage` (multiple FAL images, detail payload only), the richtext fields `description2`, `capabilities` and `textrelatedproducts`, and `portfolio` (TCA `link`) — emitted as a **resolved URL** through the new `LinkResolver::typolinkUrl()`. The product payloads are specified for the first time (7.15). Links in `contentelement` / `contentelementcta` that point at a **container** now resolve its children (`items`, page‑level shape) and `background` (7.4). Two record link handlers (`download`, `product`) added to the link browser, resolved server‑side per the new Clause 9.12; new middleware `vitec/download-file` streams `/download/file/<uid>` as a forced download (5.3), file lookup consolidated into `DownloadFileResolver` — first step towards the B‑4 target (10.2). Backend‑only: `relatedprodukt` moved from the Misc tab to General. Editorial: the document footer had been stuck at v1.7 since v1.8. |
| 1.11 | 2026‑08‑21 | **New interface: site search.** Apache Solr 10 (dedicated VPS behind an HTTPS reverse proxy) with `apache-solr-for-typo3/solr` 14.0.0-RC1. The EXT:solr results plugin `solr_pi_results` on the search page is rendered headless by `SearchJsonRenderer` (payload key `search`) - request/response contract in the new Clause 7.16. Indexed corpus: pages plus product, market (`detail_page` only), use-case, news and download records; result `type` vocabulary `page\|product\|market\|story\|news\|download`. Downloads gained the canonical route `/download/<slug>` (uid route kept for the record links) and `private_download` is now enforced by `DownloadFileResolver` (5.3, 9.12). The VITEC Set now declares the solr set as a dependency - overriding a foreign set’s TypoScript requires loading after it (5.2). Editorial: the header table had been stuck at v1.9 since v1.10. |
| 1.12 | 2026‑08‑24 | **Interface change (additive).** The search endpoint (7.16) gained a type filter and facet counts: request parameter `filter` (a value from the `type` vocabulary; the natural name `type` is unavailable - it is TYPO3’s reserved page-type parameter), response keys `filter` (active filter or null) and `facets.type` (per-type document counts with `active` flags; counts stay complete while a filter is active, except when the filtered result is empty). Editorial baseline of 76 managed synonyms imported into `core_en` (codecs, acquired-brand names such as `exterity => avedia`, UK/US spellings, common misspellings) - synonyms apply at query time, no re-index. New autocomplete endpoint: the EXT:solr suggest plugin as lean JSON page type 7384, deliberately USER_INT (7.16). |
| 1.14 | 2026‑09‑11 | **Interface change (additive).** Secondary search facets: stories index their market titles (`market_stringM`, MM relation), products their sys_category titles (`category_stringM`); the search endpoint accepts `market` and `category` GET parameters (values verbatim from `facets.<name>[].value`; an unknown value simply yields zero results) and echoes them as `activeFacets`. Visibility fix: the usecases queue now honours `no_index`/`hideonwebsite`, products defensively `hideonwebsite` - and the excluded story’s document was removed explicitly, because re-indexing never deletes. |
| 1.15 | 2026‑09‑11 | **Corpus change (additive, index side).** Download documents now carry the extracted file text: Solr 10 has no ExtractingRequestHandler, so a dedicated Apache Tika container on the Solr VPS (Caddy route `/tika/*`, own basic-auth credential) extracts PDF/Office contents during indexing (`TikaDownloadContentIndexer` on `BeforeDocumentIsProcessedForIndexingEvent`, fail-soft per 9.5, cached in `var/tika-cache` keyed on path+size+mtime). Datasheet specifications are now searchable ("625i", "genlock"). No interface change. |
| 1.16 | 2026‑09‑11 | **New plugin** `vitec_productfinder` (`ProductFinderJsonRenderer`, payload key `productfinder`): the product category hierarchy on its own, as the data source for the filter selects above the product lists (7.15.3, 8). No change to `vitec_productlist` — filtering happens client side over the products already delivered, keyed on the category uids this payload carries. |
| 1.17 | 2026‑09‑11 | **Interface change (additive).** The product list payload gained `successStories` (7.15.2.1): stories an editor attaches to a `sys_category` (tab *Options*, with its own layout), emitted in the list envelope beside `products`. Each card carries `forCategories` — the product categories it came from — so the front end can hide it with the same `?cat=`/`?subcat=` filter it applies to the products. Story links reuse the 7.5 rule, with the detail page taken from the element or the new site setting `vitec.storyDetailPid`. |
| 1.18 | 2026‑09‑24 | **Interface change (additive).** Products gained `solutions` — an MM relation to solutions, emitted as solution cards in both product payloads — and a **Resources tab**: child table `tx_vitec_product_resource` with documentation / support cards (editable title, text and icon around one link, resolved per 9.12; cards without a resolvable link are not emitted) plus a `datasheets` MM selection of download records rendered into the same envelope as `type: datasheet` cards, delivered detail‑only as the `resources` list envelope whose `layout` switches to `carousel` above three cards in total (7.15). The `solutions` relation accepts group‑level records: the eight groups exist as `is_group = 1` solutions (created by `vitec:create-solution-groups`, excluded from lists and from `vitec:align-solutions`) because the agency workbooks reference the group level. Editorial: the footer had been stuck at v1.15 since 1.16/1.17 (2026‑09‑11). |
| 1.19 | 2026‑09‑25 | **Interface change (additive).** The `vitec_card` Content Block gained the `layout_variant` value `flipcard` and a *Card Back* palette: `back_headline` (string, max 100) and `back_text` (richtext → HTML). The two fields are `displayCond`‑gated on the flipcard layout in the backend but serialised unconditionally (7.7) — the front end **shall** key the back face on `layout_variant`, never on field presence, because stored back values persist when an editor switches the layout away. Backend repair: `vitec_layout_variant` / `vitec_background_variant` are shared with `vitec_ctabanner`, and the index‑wise Content‑Blocks merge had dropped four card‑only select values (“Missing label”) — per‑type item lists now come from `columnsOverrides` (7.7); the Card‑Back palette lives in the Layout tab below the variant select. Editorial: the head‑matter version had been stuck at v1.17 since 1.18. |
| 1.20 | 2026‑10‑01 | **Interface change (additive).** Sub-market page template: frontend layout 21 with one colPos group per design section (`colPos110`..`colPos160`, clause 7.17); new Content Blocks `vitec_markethero`, `vitec_marketintro`, `vitec_marketinsights`; new plugin `vitec_productfeature` (key `productfeature`, one product in the 7.15.2 item shape); story cards gained `quote`/`quotePerson`/`quoteRole`; `vitec_usecaselist` gained the layout value `testimonials`; `vitec_productlist` gained hand-picked product selection (editor order, category filter ignored). Field semantics: Template_Sub-Market_Spezifikation_2026-10-01.md. |
| 1.21 | 2026‑10‑07 | **Interface change (additive) and corpus change.** Search results carry `image` (processed 640 px tile image, root-relative, `null` for types without one). Corpus: story documents now include their body (the inline `tt_content` children), page and story content include the text columns of the market/solution Content Blocks and the `vitec_items`/`vitec_faq_items` collection children; solutions are indexed. News rank by publication date (`datetime`; `crdate` is the import date) with an age-decay boost. EXT:solr upgraded from 14.0.0-RC1 to 14.0.2 final (TYPO3 and the Solr image). Editorial: 7.16 now states the `/_json` prefix the URL swap introduced, the `type` vocabulary lists `solution` (delivered since 1.14), and the footer had been stuck at v1.19. |
| 1.22 | 2026‑10‑09 | **Interface change (additive) and defect fix.** Search cards ("best bets", 7.16): editorial `tx_vitec_domain_model_searchcard` records with keyword lists; the search payload gained `cards` (max. 3, priority-ordered, page 1 only, synonym-aware keyword match; record cards mirror the record’s indexed document, filled card fields override). Defect fix: a request without `q` returned an empty body instead of the documented empty shape (`stdClass` resolved inside the renderer namespace). |
| 1.23 | 2026‑10‑09 | **New interface (additive).** Product type-ahead for the megamenu catalog panel: lean page type `?type=7385&q=` (7.16) returning `suggestions` and up to six product cards via Solr, last term completed as a prefix. The megamenu gained the panel layout `catalog`, filled by the VITEC listener `ProvideProductCatalog` (main categories → subcategory groups → products with one-line teasers). |
| 1.24 | 2026‑10‑23 | **Interface change (additive).** New frontend layout **22 Solution Group Page** for the eight solution group hero pages (zones hero = `colPos1`, main = `colPos0`). Element order: `vitec_markethero` + `vitec_introparagraph` in the hero zone; `vitec_marketintro` 2, `vitec_introparagraph` + 4 `text_only` cards (challenges), `vitec_introparagraph` + 4 alternating `horizontal`/`horizontal_reverse` cards (capabilities, bullets as `<ul>`), `vitec_usecaselist` (testimonials, shipped hidden until stories are picked), `vitec_marketinsights`, `vitec_ctabanner` in main — imported from the agency Template‑06 workbooks by `vitec:import-solution-group-workbooks`; page properties as on the sub‑solution pages. |
| 1.25 | 2026‑10‑23 | **Interface change (additive).** The markets branch is aligned with the agreed agency structure: `/markets` eleven market hero pages their sub‑market pages (the use‑case level stays sections per 7.17). New frontend layout **23 Market Hero Page** (zones hero = `colPos1`, main = `colPos0`): `vitec_markethero` (CTA anchors `#submarkets`) + `vitec_introparagraph` (header carries the hero H1) in the hero zone; `vitec_marketintro`, `vitec_introparagraph`, sub‑market `vitec_card`s whose `primary_cta` links the child page, `vitec_ctabanner` in main (`vitec_customerlogos` and `vitec_usecaselist` ship hidden until filled editorially). Imported by `vitec:import-market-hero-workbooks`; the tree itself by `vitec:align-market-pages` (renames with slug redirects, child slugs normalised — this also repairs embedded‑slash slugs). Sub‑market pages gained the full page‑properties set. |
| 1.26 | 2026‑10‑26 | **Interface change (additive).** Product detail resources band simplified (customer CR): the `resources` envelope gains **`sectionLayout`** — `50-50` (default) \| `33-33-33` — from the new per‑product select *Resources Section Layout* (Layout 1: Technical Highlights left, resource cards right, 50/50; Layout 2: highlights over two columns, cards in the third). Capabilities leave the band editorially; the TCA fields `capabilities`, `datasheets` and `downloads` are parked in the Misc tab — all three stay in the payload unchanged. The automatic newest‑datasheet/documentation cards remain deferred; meanwhile the manual card `restype` vocabulary gains `datasheet` and `brochure` — a pinned datasheet card links the download record and stays current because the download route always serves the newest file. |
| 1.27 | 2026‑11‑02 | **Interface change (additive).** Product *Resources Section Layout* gained the value `hardware` (“Layout 3 - Hardware option”): `resources.sectionLayout = "hardware"` — Technical Highlights over two columns, no resource cards (`resources.items` is emitted empty even when cards are maintained, so they reappear on a switch back). |
| 1.28 | 2026‑11‑02 | **Interface change (breaking for the products menu, additive elsewhere).** Product section rebuilt after the agency sitemap/menu prototype of 2026-11-02: 20 product overview pages `/products/<slug>` grouped by sysfolders (3 categories → 9 groups) below /products. The megamenu `catalog` payload is now **version 2**: `categories[].groups[].items[]` of type `page` (overview page with `productCount`/`products`/`image`) or `shortcut` (`target` indices), plus `viewAll` and `news`; the menu no longer lists products. Transitional v1 fields (`groups[].products` etc.) stay until the front end has switched. The product type-ahead (`?type=7385`) gained `overviews` and a per-product `overview` with menu path. |
| 1.29 | 2026‑11‑02 | **Interface change (removal within the unreleased catalog v2).** `catalog.news` dropped (decision: no automatic news in the products menu). Both promo boxes of the products panel, "Product highlight" and "Product news", are the entry's editorial `teasers` (`kicker` = box label). Megamenu caching: pages carrying the menu are now tagged with the menu tables, so saving menu records refreshes every page; data read from the page tree still needs a cache flush. |
| 1.30 | 2026‑11‑02 | **Interface change (additive).** Megamenu `pagetree` entries carry `display` (`columns` \| `rail`) and, for `rail`, `cardSize` (`regular` \| `compact`): Markets and Solutions switch to a left list with the sub-pages as image cards on hover. Page-tree items gained `image` (page media) and `teaser` (abstract) from stage 2; the sub-solution pages received their hero background as page media (sub-markets had page media already). |
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 |
| `apache-solr-for-typo3/solr` | 14.0.0-RC1 | Site search: indexing pipeline, query API |
| Apache Solr | 10.0.0 (dedicated VPS) | Search server, reached via an HTTPS reverse proxy |
| `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. The same applies to
> `apache-solr-for-typo3/solr`, pinned to `14.0.0-RC1` (the only line for TYPO3 14).
### 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; it declares `apache-solr-for-typo3/solr` as a set dependency so
the solr defaults load **before** the VITEC overrides - sets load in dependency
order, and a set loaded later silently wins. 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
Three middlewares are registered in `Configuration/RequestMiddlewares.php`, all 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/<formKey>` (Clause 7.8.2). Every other request passes through untouched. |
| `vitec/success-story-path-rewrite` | Rewrites `/success-stories/<slug>` internally to `/success-stories/story/<slug>` when `<slug>` 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. |
| `vitec/download-file` | Answers `GET /download/<slug>` (canonical; used by search results) and `GET /download/file/<uid>` (legacy; the download record links build this form): resolves the download record’s file (`DownloadFileResolver`: FAL → Collateral naming convention → `filepath`) and streams it with `Content-Disposition: attachment` (Clause 9.12). Records flagged `private_download` are refused. Unknown uid/slug, hidden or private record or missing file fall through to normal page resolution. |
### 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.<key>
▼
[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.<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).
### 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://…",
"<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`, `markets`, `solutions`, `news → items`) | **object** (see below) |
| Detail plugin | singular noun (`product`, `usecase`, `market`, `solution`) | object |
| Container | `items` | array of `{config, contentElements}` |
**List envelope (since v1.9).** The four record list plugins — `vitec_productlist`,
`vitec_usecaselist`, `vitec_marketlist`, `vitec_solutionlist` — share one shape:
```jsonc
{ "layout": "grid", // display variant, see the note below
"showToolbar": false, // render the toolbar above the list
"<plural>": [ … ] } // products | usecases | markets | solutions
```
`vitec_usecaselist` and `vitec_productlist` emitted a bare array before v1.9; a consumer has
to read `<key>.<key>` now. `layout` uses `grid | list | carousel | 50-50` everywhere **except**
`vitec_productlist`, which keeps its own `0`–`3` ("Default", "Variation 1–3") — unifying it
would move existing content onto a different layout and needs a migration, so it was left
alone.
`news_*` is not part of this: it keeps its own `{mode, items|news, settings}` envelope (7.6).
### 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).
**Linked containers (since v1.10).** When a link resolved through
`ContentElementResolver` (e.g. `contentelement` / `contentelementcta`, 7.15) points at
a container, the resolver attaches the container's children under `items` in exactly
the column shape above, plus the resolved `background`. Nested containers recurse
(depth‑capped at 5, cycle‑safe); children are stripped of container‑level fields as in
the page‑level rendering; nested plugins resolve per 9.7.
### 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.
**Field naming.** Fields carry the `vitec_` vendor prefix in **storage**; the JSON key is
the plain YAML identifier. `ArrayRecursiveToArray` decorates every key with
`TcaFieldDefinition->identifier`, so the column `vitec_background_color` is emitted as
`background_color`. A front end **shall** key on the identifier, never on the column name.
**Field types are whitelisted.** `ArrayRecursiveToArray::processStringField()` switches over
the Content Blocks field types and drops anything that reaches its `default` branch. A new
field type therefore **shall** be verified against the live JSON before it is relied upon;
deriving it from the block definition alone is insufficient.
**Appearance (since v1.8).** Content Blocks builds its own `showitem`
(`TcaGenerator::getContentElementStandardShowItem`) and appends only the *Extended* tab, so
the Core *Appearance* tab was absent from every block.
`Configuration/TCA/Overrides/tt_content.php` now appends
`--div--;core.form.tabs:appearance` together with the **Core
palettes** `frames` and `appearanceLinks` to every `vitec_*` type whose `showitem` lacks
`--palette--;;frames`, positioned ahead of the *Extended* tab. The loop is idempotent, skips
the Extbase plugins (which inherit the tab from `tt_content` `types['header']`) and covers
future blocks automatically.
Redefining those palettes inside a block's `config.yaml` is **prohibited**: TCA palettes are
global per identifier, so a second definition of `frames` collides with the Core one — the
failure mode is a page module that throws `RecordPropertyNotFoundException` at runtime for
*existing* elements, not a build error.
`sectionIndex` and `linkToTop` are **not** part of the `appearance` envelope — `lib.appearance`
carries `layout`, `frameClass`, `spaceBefore` and `spaceAfter` only — and therefore do not
reach the JSON. `sectionIndex` still governs the Core section‑menu element; `linkToTop` is
inert in headless operation.
**Colour fields.** `intro-paragraph` carries `background_color` (`type: Color`): lower‑case
hex, empty string when unset, with the VITEC palette offered as `valuePicker` presets. Hex
is stored lower case deliberately, because the native colour picker emits lower case and an
upper‑case preset would store one colour under two spellings. A block can therefore receive
a background by **two** mechanisms — this field and the `vitec-bg-*` frame classes. The
precedence between them is a front‑end decision and is deliberately not fixed here.
**Card flip layout (since v1.19).** `vitec_card` carries the additional `layout_variant`
value `flipcard` together with a *Card Back* palette: `back_headline` (string, max 100)
and `back_text` (richtext → HTML per 9.11). Both fields are serialised by the
auto‑serialiser on **every** card — empty strings when unset — so a front end **shall**
render the back face only when `layout_variant` equals `flipcard`, never keyed on the
mere presence of the fields: an editor may switch a filled flip card to another layout,
and the stored back values persist. In the backend the two fields are gated by
`displayCond` on the flipcard layout (the select reloads the form via `onChange`);
the palette sits in the **Layout** tab directly below `layout_variant`.
Beware: `layout_variant` and `background_variant` are **columns shared with
`vitec_ctabanner`** (same identifier under the vendor prefix). Content Blocks merges
diverging item lists of such a shared select **index‑wise** into the base column —
four card‑only values simply did not exist as items (“[ Missing label ]” in the
backend). Since v1.19 `Configuration/TCA/Overrides/tt_content.php` pins the base
column to the shorter cta‑banner lists and hands `vitec_card` its full lists through
`columnsOverrides`, honoured by FormEngine **and** DataHandler. Because that per‑type
merge is `array_replace_recursive`, an items override **shall** be at least as long as
the base list — and a new block **shall not** reuse these identifiers with diverging
items; prefix instead, as `hero_layout_variant` / `intro_layout_variant` already do.
### 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/<formKey> 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": {"<field>": "<message>"}}` |
| 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": "<p>…</p>", // 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
`<link>` 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": "<p>…</p>", // 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/<slug>/` and `/solutions/<slug>/` 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": "<p>…</p>", // 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.
#### 7.13.3 Solution list payload (`vitec_solutionlist`)
> **NOTE (since v1.18)** Group‑level solution records (`is_group = 1`, the eight
> thematic groups) never appear in this list. They exist to be selected in the
> product `solutions` relation (7.15) and carry the group page as `detailUrl`;
> `vitec:align-solutions` ignores them, `vitec:create-solution-groups` creates
> them idempotently.
Added in v1.9, and deliberately the same shape as 7.13.2 — `SolutionListJsonRenderer` emits
under `content.solutions`:
```jsonc
{ "layout": "grid", "showToolbar": false,
"solutions": [
{ "uid": 7, "title": "…", "slug": "…", "subtitle": "…", "teaser": "…",
"description": "<p>…</p>", // RTE HTML, resolved per 9.11
"detailUrl": "/solutions/iptv/", // from the record's detail_page; null when unset
"image": { "url": "…", "srcset": [ … ] } } ] }
```
Selection and ordering follow 7.13.2 exactly: nothing selected means all visible solutions
alphabetically, a selection means precisely those in the arranged order.
`tx_vitec_domain_model_solution` has **no `sorting` column** either, which is why the
unselected case falls back to alphabetical rather than to a backend‑defined order.
### 7.14 Event list payload (`vitec_eventlist`)
`EventlistJsonRenderer` emits, under `content.eventlist`:
```jsonc
{ "events": [
{ "uid": 1, "title": "…", "slug": "…", "teaser": "…",
"description": "<p>…</p>", // RTE HTML, resolved per 9.11
"eventstart": "2027-07-09", // ISO date, null when unset
"eventend": "2027-07-09",
"venue": "…", "booth": "…", "city": "…", "country": "…",
"attendancemode": "offline", // offline | online | mixed
"eventstatus": "scheduled",
"eventurl": "…", "meetinglink": "…",
"image": { "url": "…", "srcset": [ … ] },
"categories": [ { "uid": 107, "title": "…", "description": "…" } ] } ],
"settings": { "layout": "list", // list | grid | teaserbar | regions
"showpast": false, "daysinadvance": 0, "limit": 0 },
"pastEvents": [ … ], // only when showpast is set
"regions": [ … ] } // only when layout = regions
```
"Upcoming" means `eventend >= today`, or `eventstart >= today` when no end date is set;
ordering is `eventstart` ascending. `pastEvents` inverts both and is present only when the
FlexForm flag `showpast` is set.
#### 7.14.1 Region payload (`layout: regions`)
For the `regions` layout the renderer additionally emits one entry per **region** — the
`sys_category` records directly below the parent category **104** — that has at least one
upcoming event:
```jsonc
{ "uid": 107, "title": "European Events",
"description": "", // plain text; sys_category is not a richtext field
"eventCount": 2,
"image": { "url": "…", "srcset": [ … ] }, // the next event's image, i.e. the region logo
"nextEvent": { "uid": 1, "title": "…", "slug": "…",
"eventstart": "2027-07-09", "eventend": "2027-07-09",
"eventurl": "…" } }
```
Normative behaviour:
- A region **without** an upcoming event is omitted entirely. `showpast` does not
reinstate it — the layout answers "what is coming up where", not "what happened".
- An event filed under a **descendant** of a region counts for that region. The category
branch is resolved in PHP from a single `sys_category` read, per 9.2's preference for one
query over per‑level recursion.
- Ordering is the **backend sorting** of the categories, so the carousel order is editor
controlled. `nextEvent.eventstart` is included so a front end may sort chronologically
instead.
- `daysinadvance` applies. **`limit` does not**: it caps events, and applying it before the
grouping would silently drop whole regions.
- The parent category id is the constant `REGION_PARENT_CATEGORY` in the renderer, not a
FlexForm setting — a hard‑coded literal in the sense of B‑5, accepted here because it
belongs to the content model rather than to a single content element.
An event carrying no category belongs to no region and is invisible to this layout while
remaining present in `events`.
### 7.15 Product payloads
#### 7.15.1 Detail payload (`vitec_productshow`, key `product`)
Produced by `ProductShowJsonRenderer::serializeProduct()`. Members:
| Member | Type | Notes |
|---|---|---|
| `uid` | int | |
| `title`, `slug`, `urltitle`, `seotitle`, `seometa`, `keywords`, `structureddata`, `teaser`, `subtitle`, `video`, `shortcutpid` | string | raw column values; `video` is a YouTube clip id |
| `applications`, `capabilities`, `description`, `description2`, `highlights`, `textrelatedproducts` | string | richtext, resolved per 9.11 |
| `hideonapp`, `hideonwebsite`, `hideondatasheets`, `hideonproducts`, `shortcut`, `legacy`, `supportproduct`, `subproduct`, `showdatapath` | bool | |
| `link` | string | `/product/<slug>` |
| `portfolio` | string \| null | resolved URL of the portfolio link (9.12); null = no link |
| `contentelement`, `contentelementcta` | object \| null | linked tt_content element in the envelope shape of 7.2; a linked container carries `items` and `background` (7.4) |
| `categories` | array | `{uid, title, description}` |
| `images` | array | product images with `url`, `srcset` and properties (9.9) |
| `heroimage` | array | hero images, same shape as `images` — **detail payload only** |
| `downloads` | array | download records incl. the resolved `file` |
| `ogimage`, `videofile` | object \| null | FAL singletons |
| `relatedprodukt` | array | card shape `{uid, title, slug, subtitle, teaser, description, link, images}`, backend order |
| `solutions` | array | solution card shape `{uid, title, slug, subtitle, teaser, description, detailUrl, isGroup, image}` via `tx_vitec_product_solution_mm`, backend order; an editor may select individual Generic Solutions or the eight group‑level records (`isGroup: true`, linking to the group page) |
| `resources` | object | list envelope `{layout, items[]}`: hand‑written cards (`type` `documentation\|support\|datasheet\|brochure`, `title`, `text`, `icon`, resolved `url` per 9.12; a card without a resolvable link is dropped) followed by the `datasheets` MM selection of download records as `type: datasheet` cards (title/teaser/icon from the record, `url` = the public download route); `layout` is `carousel` above three cards in total; `sectionLayout` (`50-50` default | `33-33-33` | `hardware`) mirrors the per‑product *Resources Section Layout* select — how the front end arranges the Technical Highlights / Resources band (1.26); `hardware` (1.27) spreads the Technical Highlights over two columns and carries NO resource cards (`items: []`, the band shows highlights only)\| `33-33-33`) mirrors the per‑product *Resources Section Layout* select — how the front end arranges the Technical Highlights / Resources band (1.26) — **detail payload only** |
| `jsonLd` | string | ready‑to‑emit schema.org `@graph` (6.6), detail only |
#### 7.15.2 List payload (`vitec_productlist`, key `products`)
List envelope per 7.3 (`layout`, `showToolbar`, `products[]`) — since v1.9. Each item
carries the member set of 7.15.1 **except** `heroimage`, `resources` and `jsonLd`. `heroimage` is
deliberately detail‑only: one further FAL query per product would buy data a list
rarely shows. The `layout` vocabulary remains `0`–`3` (see the v1.9 note).
---
#### 7.15.2.1 Success stories below a product list
The list payload carries a second array beside `products`: the success stories
an editor attached to the categories this element renders. The link lives on
`sys_category` (tab *Options*), not on the plugin, so one decision serves every
list that shows the category.
```json
"successStories": {
"layout": "grid",
"stories": [
{
"uid": 12,
"title": "Abu Dhabi Islamic Bank",
"detailUrl": "/success-stories/abu-dhabi-islamic-bank",
"cardImage": { },
"categories": [ ],
"forCategories": [58]
}
]
}
```
Envelope per 7.3 (`layout` plus the array); `layout` uses the story-list
vocabulary `grid|list|carousel|50-50` and comes from the first category that
contributes stories. Cards are the shared story shape of 7.5, so the same
front-end component renders them.
`forCategories` is the addition: the **product** categories the story was
attached to — not to be confused with `categories`, which stays the story's own
topic categories. The front end filters on it, using the same `?cat=` /
`?subcat=` parameters the product finder (7.15.3) writes: a card stays visible
while one of its categories is selected. Stories are collected over the
configured categories **and their descendants**, deduplicated, each carrying
every category it came from. An element configured to show all products has no
category context and returns the empty envelope.
`detailUrl` follows 7.5: the story's own `detail_page` wins; otherwise the URL
is built from the detail page's parent path plus the story slug — the form
`SuccessStoryPathRewrite` maps back. That page is taken from the element's
FlexForm, falling back to the site setting `vitec.storyDetailPid`, and
`detailUrl` is `null` when neither is set.
---
#### 7.15.3 Product Finder payload (`vitec_productfinder`, key `productfinder`)
The filter above the product lists is fed by its own element. It carries the
product **category hierarchy only** — no products, no counts:
```json
{
"type": "vitec_productfinder",
"content": {
"productfinder": {
"categories": [
{
"uid": 58,
"title": "Platforms and End-Points",
"subcategories": [
{ "uid": 66, "title": "Avedia Platform" },
{ "uid": 67, "title": "EZ TV Platform" },
{ "uid": 68, "title": "APEX Platform" }
]
}
]
}
}
}
```
`categories[]` are the children of the configured root category, `subcategories[]`
their direct children — the same two levels the product records hang on, so a
selected value maps straight onto `product.categories[].uid` in the list payload
(7.15.2). The editor places **one** element above the lists; FlexForm offers the
category root (empty = the `Product` tree) and the order of both levels (backend
order, title, uid).
Filtering itself is a front-end concern: the list plugin is unchanged, and the
front end narrows the products it already received using the URL parameters
`?cat=` and `?subcat=`. Categories without products are emitted like any other —
the front end decides whether to offer or suppress an empty result.
---
### 7.16 Search payload (`solr_pi_results`)
The site search runs on Apache Solr through EXT:solr. The EXT:solr results plugin
`solr_pi_results` sits on the dedicated search page (`/search`) and is rendered
headless by `SearchJsonRenderer` under the payload key `search`. The renderer runs
EXT:solr’s own query pipeline (`SearchRequestBuilder` → `SearchResultSetService`) -
only the rendering differs from the stock Fluid plugin.
Since the URL swap of 2026‑09‑28 every JSON URL carries the prefix `/_json`:
the search answers at `/_json/search?q=…`, while the bare `/search` serves the
single-page app. Paths in this clause are given without the prefix.
**Request.** GET parameters on the search page:
| Parameter | Meaning |
|---|---|
| `q` | Search terms. Absent or empty: the response keeps its full shape with `numFound: 0`, so the front end never needs a second code path. |
| `page` | 1-based result page, optional. |
| `filter` | Restrict results to one type from the `type` vocabulary below (e.g. `filter=product`). Unknown values are ignored. Named `filter` because `type` is TYPO3’s reserved page-type parameter. |
| `market` | Restrict to one market (stories carry the facet). Value verbatim from `facets.market[].value`; unknown values yield zero results. |
| `category` | Restrict to one product category. Value verbatim from `facets.category[].value`. |
The EXT:solr namespace (`tx_solr[q]`, `tx_solr[page]`) is accepted as a fallback.
All three parameters are excluded from cHash validation; the search page is never
served from the page cache (the renderer disables caching per request - cached
variants would collide because excluded parameters do not enter the cache
identifier).
**Response** under `content.search`:
```json
{
"query": "encoder",
"page": 1,
"resultsPerPage": 10,
"numFound": 202,
"totalPages": 21,
"filter": null,
"activeFacets": {},
"facets": {
"market": [
{ "value": "Sports, Venues & Entertainment", "count": 9, "active": false }
],
"type": [
{ "value": "news", "count": 137, "active": false },
{ "value": "product", "count": 13, "active": false }
]
},
"cards": [
{ "title": "Control Room Platforms", "teaser": "Unified, low-latency video …",
"image": "/fileadmin/_processed_/…/csm_gov_16-9_….jpg",
"url": "/solutions/control-room-command-centre-solutions/…",
"linkLabel": "", "type": "solution", "keyword": "control room" }
],
"results": [
{ "title": "MGW Diamond-H", "url": "/product/mgw-diamond-h-hdmi-encoder",
"type": "product", "teaser": "… 4K HDMI <mark>Encoder</mark> …",
"image": "/fileadmin/_processed_/…/csm_mgw-diamond-h_….jpg" }
],
"suggestions": []
}
```
- `type` **shall** be one of `page | product | market | solution | story | news |
download`
(mapping table in the renderer; unknown index types pass through verbatim).
- `teaser` carries the highlighted fragment (`<mark>…</mark>`, fragments joined
with " ... ") when highlighting applies, otherwise a plain 250-character excerpt
of the indexed content. It is the only payload field containing markup.
- `url` follows the shared link contract: root-relative, resolved server-side.
`download` results point at the forced-download endpoint `/download/<slug>`
(5.3) - front ends should present them as file downloads.
- `image` is a processed tile image (640 px wide, root-relative) or `null`.
Sources: products `image` (fallback `productimage`), stories `card_image`
(fallback `hero_bgimage`), markets/solutions `image`, news `fal_media`;
pages and downloads carry none.
- `facets.type` carries the per-type counts for filter tabs, ordered by count.
While a `filter` is active the counts remain those of the unfiltered query, so
tabs do not collapse - except when the filtered result is empty, where only
the active option (count 0) is returned; front ends should then offer
"remove filter" rather than rely on the other counts.
- `filter` echoes the active type filter, `null` when none; `activeFacets`
echoes the active secondary facet filters as `{name: value}`, `{}` when none.
- `facets.market` (story market titles, from the MM relation) and
`facets.category` (product sys_category titles) appear whenever matching
documents carry the fields; further facets are pure TypoScript.
- `suggestions` lists spellcheck alternatives ("did you mean"), `[]` if none.
- `cards` lists editorial search cards for the query (below), `[]` if none.
**Search cards** ("best bets", since v1.22). Editors maintain records of
`tx_vitec_domain_model_searchcard` (any storage folder; hidden/start/end time
apply). A card **shall** appear above the results when the query contains one
of its keywords as a whole word or phrase - case- and punctuation-insensitive,
including one level of the Solr managed synonyms in both directions. At most
three cards, highest `priority` first, deduplicated by `url`; delivered on
page 1 only (load-more pages carry `[]`) and independent of `filter`, the
secondary facets and `numFound` - a card may stand above zero results.
| Field | Meaning |
|---|---|
| `title` | Card title. |
| `teaser` | Plain text (no markup, unlike result teasers), may be empty. |
| `image` | 640 px tile, root-relative, or `null`. |
| `url` | Root-relative link target (or absolute for external links). |
| `linkLabel` | Button text, `""` = front-end default (e.g. "Learn more"). |
| `type` | Result type of the referenced record (`product`, `story`, …), `custom` for a free card. |
| `keyword` | The maintained keyword that fired (diagnostics, tracking). |
A card referencing a record takes title, teaser, image and URL from that
record’s **indexed Solr document**, so it always matches the record’s own
search result; filled card fields override. A card without a resolvable
title or URL (e.g. the record is hidden or not indexed) is skipped silently.
**Indexed corpus** (`plugin.tx_solr.index.queue`): pages, plus records with a
resolvable public URL - products, markets (only those with `detail_page`),
use cases, news (`type = 0`; "page as news" records are excluded because their
target pages are already indexed) and downloads (`private_download = 0 AND
hideonwebsite = 0`; their `content` additionally carries the file text extracted
through Apache Tika, so datasheet specifications are searchable). Solutions are indexed like markets (`detail_page > 0`). Stories include their
body - the inline `tt_content` children - and both page and story content
include the text columns of the market/solution Content Blocks and the
collection children (`vitec_items`, `vitec_faq_items`). Ranking boosts products
(^10) and stories (^2); news decay with age by their publication date
(`datetime` mapped to `created` - `crdate` is the import date), all other
types are exempt. Every executed search is
logged to `tx_solr_statistics` with the last two IP octets masked.
**Autocomplete.** `GET /_json/search?type=7384&tx_solr[queryString]=<input>` answers as
a lean JSON page type without the page envelope:
```json
{ "suggestions": { "encoder": 118, "encoding": 171 },
"suggestion": "enc", "documents": [], "didSecondSearch": false }
```
Term completions come from the index (spell field) with document counts;
synonyms and all indexed types are reflected automatically. The plugin is
wired as **USER_INT** - the example shipped with EXT:solr predates TYPO3 v14
(`config.no_cache` no longer exists), and a cached USER would pin the first
query’s suggestions for every later request. Front ends should debounce
(~200 ms), start at 2-3 characters, and turn a picked suggestion into a
regular `/_json/search?q=` request. Top-result documents are deliberately disabled
(`showTopResults = 0`): their shape is hardcoded upstream and carries the raw
document type and the full indexed content.
**Product type-ahead (megamenu, since v1.23).**
`GET /_json/?type=7385&q=<input>` answers as a lean JSON page type
(`ProductMenuSearchJsonRenderer`, USER_INT):
```json
{ "query": "vision",
"suggestions": [ "Vision Video Capture Cards", "VisionSD8" ],
"overviews": [ { "title": "Vision Video Capture Cards", "link": "/products/vision-video-capture-cards",
"path": [ "Video Wall Controllers & Management", "Capture & Graphics" ] } ],
"products": [ { "title": "VisionSD8", "teaser": "Multi-channel SDI capture …",
"image": "/fileadmin/_processed_/…", "url": "/product/visionsd8",
"overview": { "title": "Vision Video Capture Cards",
"link": "/products/vision-video-capture-cards",
"path": [ "Video Wall Controllers & Management", "Capture & Graphics" ] } } ] }
```
It runs through Solr restricted to product documents; the last term is
completed as a prefix, so cards appear while typing (inputs below two
characters yield the empty shape). At most six products, one card per
product. Since v1.28 `overviews` lists the product overview pages whose
title (or menu path) contains every typed term, and each product carries
`overview` - the overview page that presents it, with its menu path - or
`null` for a product outside the menu tree. `suggestions` are the overview
titles followed by the product titles. `teaser` is the product's plain-text
one-liner from the record (≤ 120 characters), `image` the indexed 640 px
tile or `null`.
**Products menu (megamenu entry `layout: "catalog"`, version 2 since v1.28).**
The catalog payload is produced by the `ProvideProductCatalog` listener of
EXT:evo_megamenu_json’s `BuildCatalogEvent` from the page tree below
`/products`: sysfolders on the first level are the menu **categories**,
sysfolders below them the **groups** (both labels, never pages), and the
pages below the groups the **overview pages** (`/products/<slug>`, built by
`vitec:build-product-overviews` from the agency sitemap of 2026-11-02) or
**shortcut** pages. The menu ends at the overview pages; product detail
pages are reached from there.
```json
"catalog": {
"version": 2,
"search": { "endpoint": "/_json/?type=7385", "param": "q", "minChars": 2, "placeholder": "Search products" },
"viewAll": { "title": "View all products", "link": "/products" },
"categories": [ { "title": "IP Video Streaming, Distribution & KVM",
"groups": [ { "title": "Encoders & Decoders",
"items": [
{ "type": "page", "uid": 587, "title": "Appliances", "link": "/products/appliances",
"productCount": 5, "products": [ { "title": "MGW Diamond-H", "link": "/product/…" } ],
"image": null },
{ "type": "shortcut", "title": "Aligo KVM", "link": "/products/aligo",
"target": { "category": 1, "group": 3, "page": 0 } } ] } ] } ]
}
```
- `items[].type` is `page` (an overview page) or `shortcut`. A shortcut's
`target` holds zero-based indices into `categories` / `groups` / `items`:
with `page` it points to an overview page (and `link` is that page),
without `page` it opens a whole group (`link` is `""`).
- `products` are the product cards of the overview page that link a detail
page (container children included, creation order); `productCount` is
their number - a card without a resolvable product is not counted.
- `image` (from content stage 2) is the overview page's first page media as a
640 px WebP, `null` when none is set.
- The promo boxes of the left column ("Product highlight", "Product news")
are the entry's editorial `teasers` next to `catalog` (`kicker` = box
label, plus `title`, `text`, `link`, `image`; designed for two, an empty
array hides the column). There are no automatic news (`catalog.news` was
dropped in v1.29).
- Hidden pages and pages with *Hide in menu* drop out; order = page tree
order. Version 1 (2026-10-09, sys_category groups with products) is no
longer the source.
- **Transitional v1 fields (deprecated, removed once the front end reads
`items`).** The deployed front end still evaluated the v1 shape for the
active state of the Products trigger and crashed the whole site without
it. Until it has switched, v2 additionally carries: `categories[].uid`
(folder uid) and `categories[].link` (`/products`); `groups[].uid` (`0`),
`groups[].link` (link of the group's first overview page) and
`groups[].products` (the products of all its overview pages, deduplicated
by link); and a `teaser` (card text, plain) on every product. Do not build
on these fields.
**Markets and Solutions menus (megamenu `layout: "pagetree"`, `display`
since v1.30).** Page-tree entries build their `columns` live from the
linked page (one column per child page, its children as `items`). The
entry field *Page tree display* adds the presentation:
```json
{ "title": "Markets", "link": "/markets", "layout": "pagetree",
"display": "rail", "cardSize": "regular",
"columns": [ { "title": "Transport", "link": "/markets/transport", "image": { "url": "…" },
"items": [ { "title": "Airports", "link": "/markets/transport/airports",
"image": { "url": "/fileadmin/_processed_/…webp", "width": 640, "height": 360, "alternative": "", "title": "" },
"teaser": "…" } ] } ] }
```
- `display: "columns"` (default; Support and every entry not switched):
columns side by side as before. `display: "rail"`: the columns as a list
on the left; hovering (or focusing) one shows its `items` as image cards
on the right, the first column is preselected. `cardSize` (`regular` |
`compact`, only with `rail`) sizes the cards - Solutions uses `compact`
so five sub-solutions fit next to the list.
- From content stage 2 every item carries `image` (first page media as a
640 px WebP, `null` when none is set) and, when the page has an abstract,
`teaser`. The sub-market pages already had page media; the 39 sub-solution
pages got their hero background as page media
(`vitec:attach-page-media --depth=2`); editors replace it in
the page properties.
- Additive: a front end that ignores `display` keeps rendering columns.
**Operations.** Indexing runs automatically: record and page saves enter the
index queue, a scheduler task (Index Queue Worker, every 5 minutes) pushes them
to Solr; the CLI command `vitec:solr-index` (with `--initialize` and `--debug`)
covers manual runs - EXT:solr 14 ships no console commands of its own. Versions:
EXT:solr 14.0.2 and the matching image `typo3solr/ext-solr:14.0.2` (Solr 10);
an image upgrade needs a fresh volume, because an existing volume keeps its
old configset - managed synonyms are restored through the REST API. Page
content is extracted from the page’s `tt_content` rows via
`index.queue.pages.fields.content` because the headless JSON output carries no
`TYPO3SEARCH` markers.
---
### 7.17 Sub-market page template (frontend layout 21)
A sub-market page is identified by `appearance.layout == "layout-21"`. Its
content is NOT read from element order: the backend layout assigns one column
per design section, and the JSON groups them as
| Group | Section | Elements |
|---|---|---|
| `colPos110` | Hero | `vitec_markethero` |
| `colPos120` | Intro | `vitec_marketintro` |
| `colPos130` | Workflow | `vitec_introparagraph` + container with `vitec_card` flip cards |
| `colPos140` | Testimonials | `vitec_usecaselist`, `layout: "testimonials"` |
| `colPos150` | Products | `vitec_productfeature` + `vitec_productlist` (hand-picked) |
| `colPos160` | Industry Insights | `vitec_marketinsights` |
The three `vitec_market*` elements are Content Blocks: their fields arrive
unprefixed under the element's `data` (7.7). Field reference and empty-value
semantics ("empty url = card hidden" etc.) are specified in
`Template_Sub-Market_Spezifikation_2026-10-01.md`; the per-field limits live
in the backend field descriptions.
Related interface additions (all additive):
- **Story cards** (7.5 shape, everywhere they appear) carry `quote`,
`quotePerson`, `quoteRole` - maintained once per story, empty strings until
maintained. The testimonial card renders them; the `vitec_usecaselist`
FlexForm gained the `layout` value `testimonials` for this.
- **`vitec_productfeature`** (payload key `productfeature`): exactly one
referenced product as a full product card in the 7.15.2 item shape, `null`
when unset or the product is not visible. Serialisation is shared with the
product list (one source of truth).
- **`vitec_productlist`** gained the FlexForm field *Hand-picked products*:
when set, the list contains exactly those products in the editor's order;
the category selection is ignored, the visibility filters stay in force.
## 8 Content‑type catalogue
| CType | TS pattern | Renderer / Processor | Payload key | Kind |
|---|---|---|---|---|
| `vitec_productlist` | `< lib.contentElementWithHeader` | ProductListJsonRenderer | `products` | list |
| `vitec_productfinder` | `< lib.contentElementWithHeader` | ProductFinderJsonRenderer | `productfinder` | taxonomy |
| `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_solutionlist` | `< lib.…WithHeader` | SolutionListJsonRenderer | `solutions` | 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 (7.14) |
| `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 |
| `solr_pi_results` | `< lib.…WithHeader` | SearchJsonRenderer | `search` | search (7.16) |
| `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 `<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`.
### 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 `<a href="t3://page?uid=12">`, legacy content as `<link>` 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.
### 9.12 Link resolution and record links
JSON output **shall never** carry an unresolved TYPO3 link (`t3://…`). Every link
field is resolved server‑side — `LinkResolver::pageUrl()` for page uids,
`LinkResolver::typolinkUrl()` for full typolink parameters — under the shared
contract **null means "no link"**: a hidden or deleted target resolves to null,
never to a broken URL.
The link browser offers two record link handlers (`TCEMAIN.linkHandler`,
`Configuration/page.tsconfig`), stored as `t3://record?identifier=<key>&uid=<n>`
and resolved by `config.recordLinks` in `setup.typoscript`:
| Identifier | Resolves to | Notes |
|---|---|---|
| `product` | `/product/<slug>` | same convention as the `link` member (7.15); assumes the product detail page (uid 10) answers `/product` |
| `download` | `/download/file/<uid>` | forced‑download endpoint (5.3); the canonical public form is `/download/<slug>` - both routes share `DownloadFileResolver`, which refuses `private_download` records |
---
## 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/<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. **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.<ctype> < lib.contentElementWithHeader` and
`content.fields.<key> = 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.
**Resolved 2026‑08‑10:** `detail_page` added to Solution with the identical TCA shape and
emitted by `SolutionShowJsonRenderer`; the shared `market | solution` branch of
`ModelcardJsonRenderer` now serialises `detailUrl`, so market cards link as well.
- **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.
**Resolved 2026‑08‑10:** `Service\LinkResolver::pageUrl()` is now the single
implementation, with five callers. The contract is **`null` = no link**; the two Usecase
renderers keep a thin wrapper appending `?? ''` because they build paths by concatenation.
`LocationsJsonRenderer` and `NewsJsonRenderer` stay outside it deliberately — the first
resolves a whole `parameter` construct, the second builds slug paths plus a canonical, and
neither is a page‑uid‑to‑URL mapping.
- **B‑13 — Appearance columns duplicated inside `data`.** Since the *Appearance* tab was
added (7.7), `Record::toArray()` resolves the six palette fields as part of the record, so
**top‑level** Content Blocks emit `layout`, `frame_class`, `space_before_class`,
`space_after_class`, `sectionIndex` and `linkToTop` raw inside `data` in addition to the
processed `appearance` object. Container children are unaffected, being serialised by
`ContainerChildrenProcessor` rather than `RecordToArray`. Harmless but redundant, and the
raw and processed representations of `layout` differ (B‑14). Removable through a
`ModifyArrayRecursiveToArrayEvent` listener.
- **B‑14 — `appearance.layout` is not represented uniformly.** On the TypoScript path a
`CASE` in `lib.appearance` maps the field to `default` / `layout-1` / `layout-2` /
`layout-3`; container children serialised by `ContainerChildrenProcessor` carry the raw
value (`"0"`). A front end consuming both paths must accept either form. Predates v1.8;
recorded here because the Appearance tab makes the field editable in the first place.
---
## 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.30.*