Files
VITEC-website/packages/vitec
Oliver Rasche 10c0328bf2 Backend module: CSV import for the VITEC domain models
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.

Deliberately out of v1: import log with three-way compare (protection
against overwriting manual edits), images/relations, multiple saved
mappings per model.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 12:53:21 +02:00
..
2026-05-18 14:46:25 +02:00
2026-05-18 14:46:25 +02:00
2026-08-05 16:53:02 +02:00

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 PHP Mode Content Blocks License Status


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

  • 🧩 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 as the reference).

📖 The full, normative architecture & interface specification lives in 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/ShowJsonRendererUsecaseSerializer 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 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

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 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) 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)

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 (JSONLD)

PageJsonLdRenderer + StructuredDataService assemble a schema.org @graph per page: Organization, WebSite (root only), BreadcrumbList, Product, VideoObject, FAQPage, ExhibitionEvent and NewsArticle.

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

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):

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. SerializerClasses/Service/<Domain>Serializer with serializeListItem() / serializeDetail().
  3. RendererClasses/UserFunc/<Domain><Kind>JsonRenderer with #[AsAllowedCallable] render() + renderForRecord(), delegating to the serializer.
  4. RegistrationconfigurePlugin() in ext_localconf.php, a FlexForm, an icon and a wizard entry in Configuration/page.tsconfig.
  5. TypoScript — in Configuration/Sets/Vitecset/setup.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. Deploydatabase:updateschema "*.add,*.change" & 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

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 Authoritative architecture & JSON interface specification (ISOstyle)
Documentation/HeadlessIntegration.md Informal stepbystep howto

License

GPL2.0orlater — © evomedien.

Built for the VITEC relaunch · TYPO3 headless + React