VITEC Import module: Products tab is now a searchable/sortable product overview. "Edit Product" opens an XLSX upload with per-field source mapping (component + cell), old/new preview and checkbox apply via DataHandler; the mapping and manual matches are persisted and preselect the next workbook. Category workbooks are detected and rejected. - new: ProductXlsxReader (PhpSpreadsheet), ProductTextImportController, Products/ProductTexts templates, 4 module routes - related products: per-pair card text in new side table tx_vitec_product_related_text (survives MM rewrites), emitted as `cardtext` in the product JSON; missing MM relations added add-only - card copy read from Body Copy (D) with fallback to CTA/Card Copy (E) - the workbooks fill either depending on row type - product detail page <title> now uses seotitle with title fallback (provider made singleton and fed from the JSON renderer) - per-user recent-search badges; last loaded workbook stored per product (tx_vitec_product_workbook), Edit Product reopens on it - composer: add phpoffice/phpspreadsheet ^5.9 - diagnostics: migrations/check_workbook.php
334 lines
15 KiB
Markdown
Executable File
334 lines
15 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_solutionlist` / `vitec_solutionshow` | `SolutionList/SolutionShowJsonRenderer` | `solutions` / `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` |
|
||
|
||
**Events** (`vitec_eventlist`) offer four layouts including `regions`, which groups
|
||
upcoming events by region category — each region tile carries its next event and
|
||
that event's image.
|
||
|
||
**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 |
|
||
| `quotation` | Quote / testimonial (shared header section, header optional) |
|
||
| `featured-content` | Featured teaser with image, colour overlay and CTA |
|
||
|
||
## 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) |
|
||
|
||
Three frontend middlewares run before page resolution: `vitec/form-submission` (the form
|
||
endpoint); `vitec/success-story-path-rewrite`, which lets the public SEO URL
|
||
`/success-stories/<slug>` resolve to the detail subpage without changing the browser URL;
|
||
and `vitec/download-file`, which streams `/download/file/<uid>` as a forced download —
|
||
the target of the link browser's Download record links.
|
||
|
||
## 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. Rows whose CSV name
|
||
differs from the TYPO3 record can be linked by hand (persistable record
|
||
aliases), so they keep matching as updates on every future delivery. 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.
|
||
|
||
**Backend module "WordPress Import"** (Web menu, behind the CSV import): pulls
|
||
posts from a WordPress REST API (VITEC blog and the Datapath site) into news
|
||
records — checkbox selection, featured and inline images are fetched and
|
||
localized so they survive the go-live.
|
||
|
||
**Link browser record links**: two extra tabs (Download, Product) let editors link
|
||
download and product records wherever links are offered. The JSON always carries the
|
||
resolved URL — products point at the detail view (`/product/<slug>`), downloads at
|
||
the forced-download endpoint.
|
||
|
||
| 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 |
|
||
| `vitec:migrate-newspages` | Rewire impexp-imported old-site news pages (FLUX `colPos` nesting, `internalurl`) via `tx_impexp_origuid` |
|
||
| `vitec:import-news` | Import old-site news records from `migrations/news_export.json` |
|
||
|
||
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 rewrite, download streaming
|
||
│ ├── 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>
|