# VITEC ### Headless TYPO3 extension powering the VITEC website The editorial backend of a decoupled platform: TYPO3 v14 authors the content, this extension turns every content element into clean **JSON** for a React front end.
![TYPO3](https://img.shields.io/badge/TYPO3-14.3-FF8700?logo=typo3&logoColor=white) ![PHP](https://img.shields.io/badge/PHP-8.x-777BB4?logo=php&logoColor=white) ![Mode](https://img.shields.io/badge/mode-headless_JSON-0A3D62) ![Content Blocks](https://img.shields.io/badge/Content_Blocks-v2-2ea44f) ![License](https://img.shields.io/badge/license-GPL--2.0--or--later-blue) ![Status](https://img.shields.io/badge/status-active_development-yellow)
--- > **What is this?** > `evomedien/vitec` is the custom TYPO3 extension behind the VITEC relaunch. The site > runs **headless**: TYPO3 does not render HTML — it emits a JSON document per page that > a separate React front end consumes. This extension provides the domain models, > content elements, layout containers and the renderers that produce that JSON, plus > schema.org structured data for SEO. ## Table of contents - [Highlights](#highlights) - [Architecture at a glance](#architecture-at-a-glance) - [Content elements & plugins](#content-elements--plugins) - [Content Blocks](#content-blocks) - [Layout containers](#layout-containers) - [Forms](#forms) - [Page‑level fields](#pagelevel-fields) - [Structured data (JSON‑LD)](#structured-data-json-ld) - [Editorial tooling](#editorial-tooling) - [Requirements](#requirements) - [Installation](#installation) - [Adding a new headless plugin](#adding-a-new-headless-plugin) - [Project structure](#project-structure) - [Documentation](#documentation) - [License](#license) ## Highlights - 🧩 **One JSON envelope for everything** — every content element, plugin, container and Content Block is exposed with the same predictable outer shape. - ⚡ **Pure JSON output** — domain payloads are built in PHP, fully decoupled from TypoScript and templates, so the logic is testable and versionable. - 🏗️ **Nestable by design** — plugins render both as top‑level page content and as children inside layout containers. - 🎛️ **Editor‑friendly Content Blocks** — hero, cards, CTA, FAQ, video, intro and a two‑column layout block, all with a unified header section. - 📮 **Forms without a form framework** — one PHP definition drives both the JSON the React app renders *and* the server‑side validation of the submission. - 🔎 **SEO built in** — a schema.org `@graph` (Organization, Product, FAQ, Events, News …) is emitted per page. - 🛡️ **Fail‑soft** — a failing element yields empty output, never a broken page. ## Architecture at a glance ``` HTTP request (headless: 1) │ ▼ [L1] Page renderer friendsoftypo3/headless → { meta, content[], jsonLd } │ ▼ [L2] Content-element envelope lib.contentElement(WithHeader) → id, type, appearance, content{header…} │ ▼ [L3] Payload injection Classes/UserFunc/*JsonRenderer → content. (products, news, …) │ ▼ [L4] Layout containers vitec_cols_* = JSON → items[].contentElements[] │ ▼ [L5] Normalisation & nesting ContentElementResolver / ContainerChildrenProcessor │ ▼ [L6] Structured data PageJsonLdRenderer + StructuredDataService → @graph │ ▼ [L7] Page-level fields MenuProcessor + FaviconsJsonRenderer → menus, favicons ``` Each renderer follows one pattern — an `#[AsAllowedCallable] render()` for top‑level use plus a `renderForRecord(array $row)` for reuse inside containers — and delegates serialisation to a service (see [`UsecaseSerializer`](Classes/Service/UsecaseSerializer.php) as the reference). > 📖 The full, normative architecture & interface specification lives in > **[`Documentation/Headless-JSON-Architecture.md`](Documentation/Headless-JSON-Architecture.md)**. ## Content elements & plugins | Domain | CType | Renderer | JSON key | |---|---|---|---| | Products | `vitec_productlist` / `vitec_productshow` | `ProductList/ProductShowJsonRenderer` | `products` / `product` | | Success Stories | `vitec_usecaselist` / `vitec_usecaseshow` | `UsecaseList/ShowJsonRenderer` → `UsecaseSerializer` | `usecases` / `usecase` | | Markets | `vitec_marketlist` / `vitec_marketshow` | `MarketList/MarketShowJsonRenderer` | `markets` / `market` | | Solutions | `vitec_solutionshow` | `SolutionShowJsonRenderer` | `solution` | | Downloads | `vitec_downloadcard` / `vitec_downloadcardcollection` | `Downloadcard*JsonRenderer` | `downloadcard` / `downloadcardcollection` | | Datasheets | `vitec_datasheets` | `DatasheetsJsonRenderer` | `datasheets` | | Events | `vitec_eventlist` | `EventlistJsonRenderer` | `eventlist` | | Locations | `vitec_locationlist` | `LocationsJsonRenderer` | `locations` | | Customer logos | `vitec_customerlogos` | `CustomerlogosJsonRenderer` | `customerlogos` | | Cards | `vitec_modelcard` | `ModelcardJsonRenderer` | `card` | | Forms | `vitec_contactform` / `vitec_demoform` / `vitec_helpdeskform` | `FormsJsonRenderer` | `form` | | News | `news_pi1` (+ 8 variants) | `NewsJsonRenderer` | `news` | **Cards** (`vitec_modelcard`) are one plugin for four model types — the FlexForm picks `product`, `story`, `market` or `solution` plus a record, and every card field comes from that record. Image resolution is delegated to `UsecaseSerializer::image()`. > ⚠️ `vitec_simplecard` is registered as a plugin and offered in the wizard, but has > no JSON renderer — it emits no payload in headless mode. See Annex B‑8 of the spec. ## Content Blocks Declarative content elements (`friendsoftypo3/content-blocks`), serialised to JSON by `nb-headless-content-blocks`. All share the unified **header section**. | Block | Purpose | |---|---| | `hero-section` | Full‑width hero with background image/video, overlays and CTA | | `card` | Flexible card (image/icon, CTAs, many layout variants) | | `cta-banner` | Call‑to‑action banner | | `intro-paragraph` | Rich intro text with optional media | | `video` | YouTube or uploaded video with poster | | `faq` | Accordion; also feeds the `FAQPage` structured data | | `columns` | Two‑column layout (50/50 · 66/33 · 33/66) with per‑item content | ## Layout containers Nested column grids (`b13/container`) that own child content elements and emit them as `items`, with per‑column flex (`align`/`justify`) and a whole‑grid `gap`. | CType | Layout | |---|---| | `vitec_cols_50_50` | Two equal columns | | `vitec_cols_66_33` / `vitec_cols_33_66` | Asymmetric two columns | | `vitec_cols_33_33_33` | Three columns | | `vitec_cols_25_25_25_25` | Four columns | | `vitec_container` | Single column with a custom CSS class | | `vitec_cards_carousel` | Carousel of card elements | ## Forms Three form plugins (contact · demo · helpdesk) share one renderer and one definition. [`FormDefinitions`](Classes/Forms/FormDefinitions.php) is the single source of truth: the same field list produces the JSON the React app renders **and** validates the submission server‑side. ``` GET page JSON → content.form = { formKey, title, endpoint, honeypot, fields[] } POST /api/vitec/form/ → { "success": true } | 422 { success:false, errors{} } ``` `FormSubmissionMiddleware` handles the endpoint: honeypot → validation → store in `tx_vitec_form_submission` → deliver. Delivery is a strategy ([`DeliveryInterface`](Classes/Forms/Delivery/DeliveryInterface.php)) with `EmailDelivery` (active) and `SalesforceDelivery` (**prepared stub — `deliver()` always throws**), chosen per form via the FlexForm. Because the submission is stored *before* delivery is attempted, a failed delivery never loses data — it is recorded as `delivery_status = failed` on the record and the endpoint still answers `success: true`. ## Page‑level fields Beyond `content[]`, every page response carries: | Field | Source | |---|---| | `mainNavigation` / `footerMenu` / `metaMenu` | headless `MenuProcessor`; the curated menus are driven by the site settings `menu.footer.pageUids` / `menu.meta.pageUids` | | `favicons` | `FaviconsJsonRenderer` — ready‑to‑render `` descriptors plus `themeColor` | | `jsonLd` | `PageJsonLdRenderer` (see below) | Two frontend middlewares run before page resolution: `vitec/form-submission` (the form endpoint) and `vitec/success-story-path-rewrite`, which lets the public SEO URL `/success-stories/` resolve to the detail subpage without changing the browser URL. ## Structured data (JSON‑LD) `PageJsonLdRenderer` + `StructuredDataService` assemble a schema.org `@graph` per page: `Organization`, `WebSite` (root only), `BreadcrumbList`, `Product`, `VideoObject`, `FAQPage`, `ExhibitionEvent` and `NewsArticle`. ## Editorial tooling **Backend module "VITEC Import"** (Web menu): CSV import per domain model (Market, Solution, Product) with a persistable column mapper and a unified review list (new / update / unchanged / db-only) — what gets written is the editable per-row payload, applied through DataHandler. A fourth tab **SEO Research** stores each delivery of the recurring keyword-research CSV, diffs it against the previous one and checks the CSV structure against the page tree and the domain records. | CLI command | Purpose | |---|---| | `vitec:import-success-stories` | One-time migration of the old-site success stories | | `vitec:import-downloads` | Import old-site downloads (Collateral only, idempotent, filename normalization) | | `vitec:create-markets` | Create market records the SEO structure check reports missing, incl. sys_category assignment | | `vitec:market-dummy-image` | Assign the shared placeholder image to markets without an image | All commands support `--dry-run` and are safe to re-run. ## Requirements | Component | Version | |---|---| | TYPO3 CMS | `^14.3` | | PHP | `8.x` | | `friendsoftypo3/headless` | `^5.0` | | `friendsoftypo3/content-blocks` | `^2.4` | | `netzbewegung/nb-headless-content-blocks` | `^0.0.23` | | `b13/container` | `^3.1` | | `georgringer/news` | `^14.0` | ## Installation ```bash composer require evomedien/vitec # apply database schema and clear caches vendor/bin/typo3 database:updateschema "*.add,*.change" vendor/bin/typo3 cache:flush ``` Enable headless mode in the site configuration (`config/sites//config.yaml`): ```yaml headless: 1 dependencies: - friendsoftypo3/headless - friendsoftypo3/headless-mixed - nb-headless-content-blocks/headless-content-blocks - georgringer/news ``` ## Adding a new headless plugin The short version (full normative rules in the architecture spec, Clause 9 & Annex A): 1. **Model / TCA / SQL** — create the domain table; declare table & field names as constants. 2. **Serializer** — `Classes/Service/Serializer` with `serializeListItem()` / `serializeDetail()`. 3. **Renderer** — `Classes/UserFunc/JsonRenderer` with `#[AsAllowedCallable] render()` + `renderForRecord()`, delegating to the serializer. 4. **Registration** — `configurePlugin()` in `ext_localconf.php`, a FlexForm, an icon and a wizard entry in `Configuration/page.tsconfig`. 5. **TypoScript** — in `Configuration/Sets/Vitecset/setup.typoscript`: ```typoscript tt_content. < lib.contentElementWithHeader tt_content..fields.content.fields. = USER tt_content..fields.content.fields..userFunc = Evomedien\Vitec\UserFunc\->render ``` Skipping this step is exactly what leaves a plugin payload‑less (see `vitec_simplecard`). 6. **Nesting** — if it may sit inside a container, register it in `PLUGIN_RENDERERS` (in both `ContentElementResolver` and `ContainerChildrenProcessor`). 7. **Deploy** — `database:updateschema "*.add,*.change"` & `cache:flush`. > **Conventions:** list keys are plural, detail keys singular; page‑id via the > `frontend.page.information` request attribute; container CTypes derive with `<` (copy), > never `=<`. ## Project structure
packages/vitec/ — click to expand ``` packages/vitec/ ├── Classes/ │ ├── UserFunc/ # JSON renderers (one per plugin) — headless entry points │ ├── Service/ # Serializers, ContentElementResolver, StructuredDataService │ ├── DataProcessing/ # ContainerChildrenProcessor (container → items) │ ├── Forms/ # FormDefinitions + Delivery/ (email, salesforce) │ ├── Middleware/ # form endpoint, success-story path rewrite │ ├── Domain/Model|Repository/ │ ├── Controller/ # Extbase controllers (non-headless / backend) │ └── Backend/ · View/ · Hook/ · EventListener/ · Tca/ · Preview/ ├── ContentBlocks/ │ └── ContentElements/ # card, cta-banner, columns, faq, hero-section, intro-paragraph, video ├── Configuration/ │ ├── Sets/Vitecset/ # setup.typoscript — the single headless entry point │ ├── TypoScript/Headless/ # containers, menus, news JSON definitions │ ├── TCA/ · FlexForms/ · Services.yaml ├── Documentation/ │ ├── Headless-JSON-Architecture.md # normative spec (start here) │ └── HeadlessIntegration.md # informal how-to ├── Resources/ └── ext_tables.sql · ext_localconf.php · composer.json ```
## Documentation | Document | Purpose | |---|---| | [`Documentation/Headless-JSON-Architecture.md`](Documentation/Headless-JSON-Architecture.md) | **Authoritative** architecture & JSON interface specification (ISO‑style) | | [`Documentation/HeadlessIntegration.md`](Documentation/HeadlessIntegration.md) | Informal step‑by‑step how‑to | ## License [GPL‑2.0‑or‑later](https://www.gnu.org/licenses/gpl-2.0.html) — © evomedien.
Built for the VITEC relaunch · TYPO3 headless + React