Files
VITEC-website/packages/vitec

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.
  • 🔍 Full-text search - Apache Solr indexes pages, products, stories, news and downloads; the search page answers as JSON (query, paging, highlighted teasers, typed results) - Clause 7.16 of the architecture spec.
  • 🛡️ 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_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 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)

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
vitec:solr-index Work the Solr index queue from the CLI (--initialize, --debug single-stepping) - EXT:solr 14 ships no console commands of its own

All import/migration commands support --dry-run and are safe to re-run; vitec:solr-index has no dry-run (indexing is idempotent anyway).

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

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