19 KiB
VITEC Content Blocks Setup — Step-by-Step
Strategie: Content Blocks (YAML-basiert, deklarativ) als Haupt-Pattern für alle VITEC Content Elements. nb-headless-content-blocks übernimmt automatisch das JSON-Mapping.
Stack-Basis bei dir (bereits vorhanden):
evomedien/vitec— deine Extension- Site Set
evomedien/vitecset friendsoftypo3/headless(headless: 1)nb-headless-content-blocks(Bridge)b13/container+itplusx/headless-container(für Layout-Wrapper, optional)
0. Aufräumen (zwei kleine Issues vorab)
0.1 nb-headless-content-blocks ins Site Set migrieren
Aktuell steht die Extension nur in der Site-Config, nicht im Site Set. Das sollte konsistent sein, damit das Set in sich abgeschlossen ist.
Datei: packages/vitec/Configuration/Sets/VitecSet/config.yaml (oder wo dein Set liegt)
name: evomedien/vitecset
label: VITEC Set
settings:
website:
background:
color: '#386492'
dependencies:
- typo3/fluid-styled-content
- friendsoftypo3/headless
- b13/container
- itplusx/headless-container
- nb-headless-content-blocks/headless-content-blocks # NEU
Dann aus config/sites/<identifier>/config.yaml die Zeile - nb-headless-content-blocks/headless-content-blocks wieder rausnehmen — sie kommt jetzt transitiv über dein Set.
0.2 Version-Mismatch fixen
composer.jsonsagt1.0.1ext_emconf.phpsagt0.0.1
Einfach beide auf den gleichen Wert bringen (Empfehlung: 1.0.0 und bei jedem Deployment hochzählen). Nicht dringend, aber sauber:
// ext_emconf.php
'version' => '1.0.0',
'state' => 'stable', // 'alpha' wirkt im Produktivbetrieb komisch
// composer.json
"version": "1.0.0"
1. Ordnerstruktur anlegen
Content Blocks leben in der Extension unter einer festen Pfad-Konvention:
packages/vitec/
├── ContentBlocks/
│ └── ContentElements/
│ ├── hero-section/ ← erster Content Block (Beispiel)
│ │ ├── config.yaml ← Feld-Definitionen
│ │ ├── assets/
│ │ │ └── icon.svg ← Icon für CE-Wizard
│ │ ├── language/
│ │ │ └── labels.xlf ← Übersetzungen
│ │ └── templates/
│ │ └── EditorPreview.html ← optional: Backend-Preview
│ ├── feature-teaser/
│ ├── cta-banner/
│ └── ...
├── Classes/
├── Configuration/
├── composer.json
└── ext_emconf.php
Ein Content Block = ein Ordner. Name = Ordnername (kleingeschrieben, mit Bindestrichen).
2. Erster Content Block: VITEC Hero Section
Nachgebaut nach einem typischen Template aus deinen Figma-Wireframes: Eyebrow + Heading + Subline + Body + CTA + Hero-Image + Background-Variant.
2.1 config.yaml
Datei: packages/vitec/ContentBlocks/ContentElements/hero-section/config.yaml
name: vitec/hero-section
group: vitec
prefixFields: true
prefixType: full
fields:
- identifier: eyebrow
type: Text
max: 50
- identifier: header
useExistingField: true
required: true
- identifier: subheader
useExistingField: true
- identifier: bodytext
useExistingField: true
enableRichtext: true
- identifier: cta
type: Link
allowedTypes:
- page
- url
- file
- email
- identifier: cta_label
type: Text
default: 'Learn more'
max: 30
- identifier: hero_image
type: File
minitems: 0
maxitems: 1
allowed: common-image-types
extendedPalette: true
- identifier: background_variant
type: Select
renderType: selectSingle
default: none
items:
- label: None
value: none
- label: Orange
value: orange
- label: Blue
value: blue
- label: Graphite
value: graphite
- label: Midnight
value: midnight
- identifier: show_logo_wall
type: Checkbox
default: 0
Was passiert hier:
name: vitec/hero-section→ TCA-Typvitec_hero_section, Tabellett_contentgroup: vitec→ eigene Sektion im CE-Wizard (muss noch registriert werden — siehe 2.5)useExistingField: true→ recycled TYPO3-Core-Felder (header,subheader,bodytext). Keine neuen DB-Spalten, konsistente BedeutungprefixFields: true+prefixType: full→ neue Felder bekommentx_vitec_hero_section_*als DB-Prefix (vermeidet Kollisionen)- Keine TCA-Override-Datei nötig — Content Blocks generiert das TCA automatisch
2.2 language/labels.xlf
Content Blocks erwartet XLF-Files nach einer festen Namens-Konvention. Jedes Feld bekommt automatisch einen Key <vendor>.<element>.<field>.label.
Datei: packages/vitec/ContentBlocks/ContentElements/hero-section/language/labels.xlf
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2">
<file source-language="en" datatype="plaintext" original="messages">
<body>
<!-- Element-Labels -->
<trans-unit id="title">
<source>VITEC · Hero Section</source>
</trans-unit>
<trans-unit id="description">
<source>Haupt-Hero mit Headline, CTA, Background-Variante</source>
</trans-unit>
<!-- Field-Labels -->
<trans-unit id="eyebrow.label">
<source>Eyebrow Text</source>
</trans-unit>
<trans-unit id="eyebrow.description">
<source>Kleiner Label-Text über der Headline</source>
</trans-unit>
<trans-unit id="cta.label">
<source>CTA Link</source>
</trans-unit>
<trans-unit id="cta_label.label">
<source>CTA Button Text</source>
</trans-unit>
<trans-unit id="hero_image.label">
<source>Hero Image</source>
</trans-unit>
<trans-unit id="background_variant.label">
<source>Background Variant</source>
</trans-unit>
<trans-unit id="show_logo_wall.label">
<source>Show Logo Wall below</source>
</trans-unit>
</body>
</file>
</xliff>
Die Core-Felder (
header,subheader,bodytext) brauchen keine Labels — sie erben die vom TYPO3-Core.
2.3 assets/icon.svg
Content Blocks erkennt Icons automatisch, wenn sie unter assets/icon.svg (oder icon.png) liegen.
Datei: packages/vitec/ContentBlocks/ContentElements/hero-section/assets/icon.svg
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#F47937" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<path d="M3 9h18"/>
<circle cx="8" cy="15" r="2"/>
<path d="M13 15h5M13 18h3"/>
</svg>
Platzhalter — tausch später gegen die echten VITEC-Icons aus.
2.4 templates/EditorPreview.html (optional aber empfohlen)
Im Headless-Mode wird kein Frontend-Template gerendert. Aber für die Redakteure ist eine Backend-Preview sinnvoll, damit sie im Seiten-Modul sehen, was sie gerade anlegen.
Datei: packages/vitec/ContentBlocks/ContentElements/hero-section/templates/EditorPreview.html
<div style="padding: 1rem; border-left: 4px solid #F47937; background: #f8f8f8;">
<div style="font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.1em; color: #666;">
VITEC · Hero Section — {data.background_variant}
</div>
<f:if condition="{data.eyebrow}">
<div style="font-size: 0.75rem; color: #F47937; margin-top: 0.5rem;">{data.eyebrow}</div>
</f:if>
<h3 style="margin: 0.25rem 0;">{data.header}</h3>
<f:if condition="{data.subheader}">
<div style="color: #555;">{data.subheader}</div>
</f:if>
<f:if condition="{data.cta_label}">
<div style="margin-top: 0.5rem;">
<span style="display: inline-block; padding: 0.25rem 0.75rem; background: #F47937; color: white; border-radius: 3px; font-size: 0.8rem;">
{data.cta_label} →
</span>
</div>
</f:if>
</div>
2.5 Backend-Gruppe "VITEC" registrieren
Damit group: vitec aus der YAML eine saubere Sektion im CE-Wizard wird, muss die Gruppe einmal registriert sein:
Datei: packages/vitec/Configuration/page.tsconfig (deine existierende Datei — aktuell leer)
mod.wizards.newContentElement.wizardItems.vitec {
header = VITEC
show = *
elements {
}
}
Das reicht — Content Blocks hängt die einzelnen Elemente dann automatisch unter elements ein.
3. Installation & Test
3.1 Caches und TCA flushen
Nach jeder Content-Block-Änderung:
vendor/bin/typo3 cache:flush
Oder im Backend: Admin Tools → Maintenance → Flush all caches.
3.2 Datenbank-Schema updaten
Content Blocks erstellt automatisch neue Spalten (z.B. tx_vitec_hero_section_eyebrow, tx_vitec_hero_section_background_variant etc.):
vendor/bin/typo3 database:updateschema
Oder Backend: Admin Tools → Maintenance → Analyze Database Structure → Run.
3.3 Im Backend anlegen
- Seite im Seitenbaum öffnen → Page-Modul
- "Neues Inhaltselement" → Tab VITEC → "VITEC · Hero Section"
- Felder ausfüllen (Eyebrow, Heading, CTA, Image, Background-Variant "orange")
- Speichern
3.4 JSON prüfen
curl -s https://dev.vitec.com/testseite | jq '.content.colPos0[] | select(.type == "vitec_hero_section")'
Erwarteter JSON-Output (durch nb-headless-content-blocks automatisch erzeugt):
{
"id": 42,
"type": "vitec_hero_section",
"colPos": 0,
"categories": "",
"appearance": {
"layout": "default",
"frameClass": "default",
"spaceBefore": "",
"spaceAfter": ""
},
"content": {
"header": "Enterprise Video Solutions",
"subheader": "Reliable. Scalable. Proven.",
"bodytext": "<p>VITEC delivers mission-critical video...</p>",
"tx_vitec_hero_section_eyebrow": "Why VITEC",
"tx_vitec_hero_section_cta": {
"href": "/success-stories",
"target": null,
"class": null,
"title": null,
"linkText": "t3://page?uid=15",
"additionalAttributes": []
},
"tx_vitec_hero_section_cta_label": "Learn more",
"tx_vitec_hero_section_hero_image": [
{
"publicUrl": "https://dev.vitec.com/fileadmin/.../hero.png",
"properties": { ... }
}
],
"tx_vitec_hero_section_background_variant": "orange",
"tx_vitec_hero_section_show_logo_wall": false
}
}
Struktur passt zu deinem existierenden textpic-Pattern — content.* mit allen Feldern nebeneinander. ✅
4. JSON-Schema aufräumen (Field-Namen kürzen)
Du hast sicher gemerkt: die Feldnamen im JSON sind lang (tx_vitec_hero_section_eyebrow). Das kommt vom prefixFields: true — nötig für DB-Konsistenz, aber unschön fürs Frontend.
Lösung: Via nb-headless-content-blocks EventListener die Keys umbenennen.
4.1 EventListener anlegen
Datei: packages/vitec/Classes/EventListener/NormalizeContentBlockKeys.php
<?php
declare(strict_types=1);
namespace Evomedien\Vitec\EventListener;
use Netzbewegung\NbHeadlessContentBlocks\Event\ModifyArrayRecursiveToArrayEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
#[AsEventListener(identifier: 'vitec/normalize-content-block-keys')]
final class NormalizeContentBlockKeys
{
public function __invoke(ModifyArrayRecursiveToArrayEvent $event): void
{
$key = $event->getKey();
// Strip prefix "tx_vitec_<element>_" from field names
// e.g. "tx_vitec_hero_section_eyebrow" → "eyebrow"
if (preg_match('/^tx_vitec_[a-z_]+?_([a-z_]+)$/', $key, $matches)) {
$event->setKey($matches[1]);
}
}
}
4.2 Services.yaml
Datei: packages/vitec/Configuration/Services.yaml (anlegen falls nicht vorhanden)
services:
_defaults:
autowire: true
autoconfigure: true
public: false
Evomedien\Vitec\:
resource: '../Classes/*'
exclude: '../Classes/Domain/Model/*'
AsEventListenerAttribute + autoconfigure = Event-Listener wird automatisch registriert. Keine weitere Konfiguration nötig.
4.3 Resultat nach Cache-Flush
{
"type": "vitec_hero_section",
"content": {
"header": "Enterprise Video Solutions",
"eyebrow": "Why VITEC",
"cta": { ... },
"cta_label": "Learn more",
"hero_image": [ ... ],
"background_variant": "orange",
"show_logo_wall": false
}
}
Saubere, lesbare Keys — React-Kollege glücklich.
5. Pattern für weitere Content Blocks
Für jeden neuen Content Block brauchst du:
packages/vitec/ContentBlocks/ContentElements/<name>/
├── config.yaml ← Felder definieren
├── language/labels.xlf ← Labels übersetzen
├── assets/icon.svg ← Icon
└── templates/EditorPreview.html ← optional
Dann:
vendor/bin/typo3 database:updateschema
vendor/bin/typo3 cache:flush
Fertig. Keine PHP-Boilerplate, keine TypoScript-Overrides, kein TCA-Gefummel.
Template-Vorschlag für deine 20 Figma-Templates
Nummerierung/Naming-Vorschlag an den Figma-Templates entlang:
| # | Name | Content Block |
|---|---|---|
| 01 | Hero | vitec/hero-section |
| 02 | Feature-Teaser Grid | vitec/feature-teaser-grid |
| 03 | CTA Banner | vitec/cta-banner |
| 04 | Quote / Testimonial | vitec/testimonial |
| 05 | Logo Wall | vitec/logo-wall |
| 06 | Product Card Grid | vitec/product-card-grid |
| ... | ... | ... |
| 16 | Content Page V1 | vitec/content-page-v1 |
Wenn du einen Block gebaut hast, ist jeder weitere 10-20 Minuten Arbeit.
6. Wann brauchst du trotzdem b13/container?
Antwort: Für echte Layout-Wrapper mit nested Content Elements. Beispiele:
- 2-Spalten-Section: Ein Container, in dessen linker Spalte ein
hero-section+ rechts eintestimonialliegt - Tabs / Accordion: ein Tab-Container mit mehreren Content Blocks je Tab
- Grid mit freier CE-Wahl: 3-column-grid, wo der Redakteur pro Spalte frei wählt
Für diese Fälle:
b13/containerContainer registrieren (wie in der vorherigen Anleitung beschrieben, per PHP TCA-Override)itplusx/headless-containermappt ihn automatisch ins JSON- Die Kinder sind dann Content Blocks → das Pattern spielt sauber zusammen
Laut Doku von nb-headless-content-blocks: "Support for EXT:container" ist eingebaut — die zwei Extensions beißen sich nicht.
7. Advanced: Sammlungen (Collections)
Für wiederkehrende Items (z.B. 3 Teaser-Cards in einem Grid) gibt es den Collection-Type:
name: vitec/feature-teaser-grid
group: vitec
fields:
- identifier: header
useExistingField: true
- identifier: teasers
type: Collection
minitems: 1
maxitems: 6
fields:
- identifier: icon
type: File
maxitems: 1
allowed: common-image-types
- identifier: title
type: Text
required: true
- identifier: description
type: Textarea
- identifier: link
type: Link
Im JSON kommt das dann als Array raus:
{
"type": "vitec_feature_teaser_grid",
"content": {
"header": "Our Solutions",
"teasers": [
{ "title": "...", "description": "...", "icon": [...], "link": {...} },
{ "title": "...", "description": "...", "icon": [...], "link": {...} }
]
}
}
8. Cheatsheet: Field-Types
YAML type |
Zweck | JSON-Output-Typ |
|---|---|---|
Text |
Einzeiliger Text | string |
Textarea |
Mehrzeilig; mit enableRichtext: true → RTE |
string (HTML bei RTE) |
Number |
int/float | number |
Checkbox |
Boolean | boolean |
Select renderType: selectSingle |
Dropdown | string (value) |
Select renderType: selectMultipleSideBySide |
Multi-Select | string (comma-sep) |
Radio |
Radio-Button-Gruppe | string |
Link |
TYPO3-Link (Page/URL/File/Email) | object (href, target, linkText…) |
File |
File-Reference | array of file-objects |
Color |
Color-Picker | string (hex) |
DateTime |
Datum/Zeit | string (ISO) |
Collection |
Wiederholbare Feldgruppen | array of objects |
Category |
TYPO3-Kategorien | array |
Relation |
Referenz zu anderen Records | array |
9. Fehlerbild-Cheatsheet
| Symptom | Ursache | Fix |
|---|---|---|
| Content Block erscheint nicht im CE-Wizard | Cache oder Gruppe nicht registriert | cache:flush, dann Backend-User-Session refresh |
Spalten fehlen in DB (column not found) |
database:updateschema nicht ausgeführt |
vendor/bin/typo3 database:updateschema |
| Feld-Label bleibt englisch/identifier | XLF-Key-Konvention falsch | Key muss exakt <identifier>.label heißen (ohne Vendor-Prefix) |
| JSON enthält veraltete Struktur | nb-headless-content-blocks nicht geladen oder Site-Set greift nicht |
TypoScript-Analyzer prüfen |
| EventListener greift nicht | Services.yaml nicht geladen oder autoconfigure: false |
Services.yaml prüfen, Cache flush |
| Icon wird nicht angezeigt | falsche Position oder SVG-Fehler | Pfad muss assets/icon.svg relativ zum CB-Ordner sein |
| "Type does not exist" nach Rename | TCA-Cache veraltet | cache:flush --group=system |
10. Quickstart
# 1. Aufräumen (einmalig)
# - nb-headless-content-blocks ins Site Set migrieren
# - Version in ext_emconf.php / composer.json angleichen
# 2. Ordnerstruktur für ersten Block
mkdir -p packages/vitec/ContentBlocks/ContentElements/hero-section/{assets,language,templates}
# 3. Files anlegen:
# - config.yaml
# - language/labels.xlf
# - assets/icon.svg
# - templates/EditorPreview.html (optional)
# 4. DB + Cache
vendor/bin/typo3 database:updateschema
vendor/bin/typo3 cache:flush
# 5. Backend: CE anlegen, speichern
# 6. Test
curl -s https://dev.vitec.com/testseite | jq '.content.colPos0'
11. Nächste Schritte
Nachdem der erste Content Block läuft:
- EventListener für saubere Keys einbauen (Schritt 4)
- Die 20 Templates aus Figma durchgehen → pro Template ein Content Block
- Wenn Layouts mit nested CEs gebraucht werden →
b13/containerdazupacken - Custom Records (
tx_vitec_market,_solution,_product,_story) — Content Blocks kann auch Record Types. Siehe YAML reference → RecordTypes - m:n Relationen zwischen Records — via
Relation-Field-Type (mitallowedundmaxitems)
Sobald der Hero-Section-Block JSON liefert, melden — dann bauen wir zusammen den ersten Record Type (tx_vitec_market) und die bidirektionalen Relationen.
Referenzen
- Content Blocks: https://docs.typo3.org/p/friendsoftypo3/content-blocks/main/en-us/
- Field Types: https://docs.typo3.org/p/friendsoftypo3/content-blocks/main/en-us/YamlReference/FieldTypes/Index.html
- nb-headless-content-blocks: https://github.com/Netzbewegung-Backend/nb_headless_content_blocks
- Headless Docs: https://docs.typo3.org/p/friendsoftypo3/headless/main/en-us/
- Beispiel-Repo (Content Blocks): https://github.com/friendsoftypo3/content-blocks/tree/main/Build/content_blocks_examples