Apache Solr 10 runs on a dedicated VPS behind a Caddy HTTPS proxy (vitecsolr.evomedien.de) because the managed webserver only allows outgoing standard ports. Credentials stay out of git: the site config carries %env()% placeholders resolved via putenv() in the git-ignored config/system/additional.php. - composer: apache-solr-for-typo3/solr 14.0.0-RC1 (the only line compatible with TYPO3 14; final 14.0.0 will arrive via composer update) - config.yaml: read connection https/443, core_en per language, env placeholder credentials; .gitignore covers additional.php - setup.typoscript: config.index_enable = 1 (page indexing silently refuses without it), solr_pi_results JSON renderer registration, results highlighting, and content field extraction from tt_content rows - the headless JSON output has no TYPO3SEARCH markers, so the default page content extraction indexed an empty content field - SearchJsonRenderer (renderer #27): JSON output for the search plugin on /search. GET q/page in; { query, page, resultsPerPage, numFound, totalPages, results[], suggestions } out. Disables the page cache per request: config.no_cache is gone in TYPO3 v14, and q/page are excluded from cHash, so cached variants would collide - ext_localconf.php: q and page added to cacheHash excludedParameters - SolrIndexCommand (vitec:solr-index): works the index queue from the CLI with connection diagnostics and a --debug single-step mode - EXT:solr 14 ships no console commands and the backend button indexes one item per click
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.
What is this?
evomedien/vitecis 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
- Architecture at a glance
- Content elements & plugins
- Content Blocks
- Layout containers
- Forms
- Page‑level fields
- Structured data (JSON‑LD)
- Editorial tooling
- Requirements
- Installation
- Adding a new headless plugin
- Project structure
- Documentation
- 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
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/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_simplecardis 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 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) 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
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):
- Model / TCA / SQL — create the domain table; declare table & field names as constants.
- Serializer —
Classes/Service/<Domain>SerializerwithserializeListItem()/serializeDetail(). - Renderer —
Classes/UserFunc/<Domain><Kind>JsonRendererwith#[AsAllowedCallable] render()+renderForRecord(), delegating to the serializer. - Registration —
configurePlugin()inext_localconf.php, a FlexForm, an icon and a wizard entry inConfiguration/page.tsconfig. - TypoScript — in
Configuration/Sets/Vitecset/setup.typoscript:Skipping this step is exactly what leaves a plugin payload‑less (seett_content.<ctype> < lib.contentElementWithHeader tt_content.<ctype>.fields.content.fields.<key> = USER tt_content.<ctype>.fields.content.fields.<key>.userFunc = Evomedien\Vitec\UserFunc\<Class>->rendervitec_simplecard). - Nesting — if it may sit inside a container, register it in
PLUGIN_RENDERERS(in bothContentElementResolverandContainerChildrenProcessor). - Deploy —
database:updateschema "*.add,*.change"&cache:flush.
Conventions: list keys are plural, detail keys singular; page‑id via the
frontend.page.informationrequest 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 (ISO‑style) |
Documentation/HeadlessIntegration.md |
Informal step‑by‑step how‑to |
License
GPL‑2.0‑or‑later — © evomedien.