New "VITEC Import" module under Web: one import page per domain model (v1: Market, Solution, Product - registry-driven, adding a model is one config entry). Workflow: upload a CSV (delimiter and encoding are auto-detected, including German-Excel semicolon/Windows-1252), map CSV columns to DB fields, persist the mapping per model together with the identity field used for matching (new table tx_vitec_import_mapping, no TCA - pure tool configuration), review a unified list of CSV rows matched against the DB records (new / update with differing fields / unchanged / db-only), then apply the checked rows through DataHandler. Each importable row carries an editable JSON payload textarea - what is written is the textarea content, not the raw CSV, so editors can fix values right in the review step. The parsed CSV travels through the form as a hidden JSON field: no session state, no temp files. Importable fields are derived from TCA at runtime (scalar types only; files, categories and other relations are excluded - a flat CSV cannot carry them). Payloads are whitelisted against that field list on apply; new records require a storage pid (prefilled from existing records). BE user permissions apply via DataHandler. The module ships its own CSS (backend-import.css, loaded only by this module) using the frontend button palette from _vitec.scss: orange #f47937 for primary actions, navy #26358c for secondary actions and structure. The stray <h2>Hi</h2> debug leftover in the shared backend layout is removed (also affects the OG Image module). A fourth tab "SEO Research" handles the recurring keyword-research CSV. It is deliberately not an import mask - the file carries research only (no meta title/description yet). Each upload is persisted as a delivery (tx_vitec_seo_research, never deleted) and evaluated: diff against the previous delivery keyed by URL, a structure check of the CSV tree against TYPO3 (pages by slug path; market/solution/product rows against the domain tables, matched by slug then normalized title), and the three work lists from the SEO flags (quick wins by GSC impressions, shared terms grouped by keyword, already ranking). CsvReader now deduplicates repeated header names, which that CSV has. New CLI command vitec:create-markets: creates the market records the structure check reports as missing, sourced from the stored delivery and matched through the same SeoResearchService - what the module lists is what the command creates. Each market gets a sys_category of the same title, found anywhere under the auto-detected market category root or created; sub-market categories are created under the parent market's category, so the category tree carries the hierarchy the flat market model cannot. Idempotent, dry-run first. New CLI commands vitec:create-markets and vitec:market-dummy-image. create-markets creates the market records the structure check reports as missing (root detection three-staged: option, auto-detect, find or create a "Markets" category). market-dummy-image assigns a shared placeholder (white logo on brand navy, fileadmin/placeholders/) to every market without an image - one sys_file for all, replacing the file restyles every placeholder at once. Both idempotent. Deliberately out of v1: import log with three-way compare (protection against overwriting manual edits), images/relations, multiple saved mappings per model.
312 lines
14 KiB
Markdown
Executable File
312 lines
14 KiB
Markdown
Executable File
<div align="center">
|
||
|
||
# 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.
|
||
|
||
<br>
|
||
|
||

|
||

|
||

|
||

|
||

|
||

|
||
|
||
</div>
|
||
|
||
---
|
||
|
||
> **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.<key> (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/<formKey> → { "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 `<link>` 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/<slug>` 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/<site>/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/<Domain>Serializer` with `serializeListItem()` / `serializeDetail()`.
|
||
3. **Renderer** — `Classes/UserFunc/<Domain><Kind>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.<ctype> < lib.contentElementWithHeader
|
||
tt_content.<ctype>.fields.content.fields.<key> = USER
|
||
tt_content.<ctype>.fields.content.fields.<key>.userFunc = Evomedien\Vitec\UserFunc\<Class>->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
|
||
|
||
<details>
|
||
<summary><strong>packages/vitec/</strong> — click to expand</summary>
|
||
|
||
```
|
||
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
|
||
```
|
||
|
||
</details>
|
||
|
||
## 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.
|
||
|
||
<div align="center"><sub>Built for the VITEC relaunch · TYPO3 headless + React</sub></div>
|