Files
VITEC-website/packages/vitec/README.md
Oliver Rasche 7bd1161abf Product text import from agency XLSX workbooks
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
2026-08-21 11:00:45 +02:00

334 lines
15 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.
<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>
![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)
</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 &amp; plugins](#content-elements--plugins)
- [Content Blocks](#content-blocks)
- [Layout containers](#layout-containers)
- [Forms](#forms)
- [Pagelevel fields](#pagelevel-fields)
- [Structured data (JSONLD)](#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 toplevel page content and as
children inside layout containers.
- 🎛️ **Editorfriendly Content Blocks** — hero, cards, CTA, FAQ, video, intro and a
twocolumn 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 serverside validation of the submission.
- 🔎 **SEO built in** — a schema.org `@graph` (Organization, Product, FAQ, Events,
News …) is emitted per page.
- 🛡️ **Failsoft** — 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 toplevel 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 &amp; interface specification lives in
> **[`Documentation/Headless-JSON-Architecture.md`](Documentation/Headless-JSON-Architecture.md)**.
## Content elements &amp; 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 B8 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` | Fullwidth hero with background image/video, overlays and CTA |
| `card` | Flexible card (image/icon, CTAs, many layout variants) |
| `cta-banner` | Calltoaction 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` | Twocolumn layout (50/50 · 66/33 · 33/66) with peritem 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 percolumn flex (`align`/`justify`) and a wholegrid `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 serverside.
```
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`.
## Pagelevel 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` — readytorender `<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 (JSONLD)
`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 &amp; Annex A):
1. **Model / TCA / SQL** — create the domain table; declare table &amp; 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 payloadless (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"` &amp; `cache:flush`.
> **Conventions:** list keys are plural, detail keys singular; pageid 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 &amp; JSON interface specification (ISOstyle) |
| [`Documentation/HeadlessIntegration.md`](Documentation/HeadlessIntegration.md) | Informal stepbystep howto |
## License
[GPL2.0orlater](https://www.gnu.org/licenses/gpl-2.0.html) — © evomedien.
<div align="center"><sub>Built for the VITEC relaunch · TYPO3 headless + React</sub></div>