Custom Gutenberg Blocks mit Timber & Twig: So bauen wir WordPress-Websites
Page Builder wie Elementor oder WPBakery sind bequem, aber sie erzeugen aufgeblähten Code, sind langsam und ein Wartungs-Albtraum. Wir setzen stattdessen auf maßgeschneiderte Gutenberg-Blöcke mit PHP 8.2, Timber/Twig und einem eigenen Block-Framework. Hier erfahren Sie, warum und wie das funktioniert.
Das Problem mit Page Buildern
Page Builder haben WordPress demokratisiert. Jeder kann damit Seiten gestalten. Doch für
professionelle Websites haben sie gravierende Nachteile: 500–800 KB JavaScript
allein für den Builder, verschachtelte <div>-Strukturen ohne Semantik,
und eine Abhängigkeit vom Plugin, die bei Updates regelmäßig für Probleme sorgt.
Elementor-Seiten erreichen selten einen Lighthouse-Score über 70 auf Mobile. Das ist kein Zufall. Es ist ein strukturelles Problem. Page Builder rendern auf der Client-Seite und laden Dutzende Assets, die für die meisten Seiten unnötig sind.
Unser Ansatz: Block-basiertes WordPress
Statt eines Page Builders setzen wir auf den nativen WordPress Block-Editor (Gutenberg), erweitert um eigene Blöcke, die exakt das tun, was die Website braucht. Nicht mehr, nicht weniger.
Der Tech Stack:
- PHP 8.2+ mit Bedrock als WordPress-Struktur
- Timber 2 / Twig als Template-Engine (trennt Logik von Markup)
- Native Gutenberg Block API mit React-basierter Editor-UI (
edit.js) - ACF Pro nur für globale Optionen, allgemein übersetzbare Strings und Custom Post Types
- Vite als Build-Tool (Hot Module Replacement, CSS-Bundling)
- Eigenes Block-Framework mit Traits, automatischer Asset-Verwaltung und Theme-System
Anatomie eines Custom Blocks
Jeder Block besteht aus einer klaren Dateistruktur. Am Beispiel eines „MediaText"-Blocks, einer Kombination aus Bild und Text, die in verschiedenen Layouts dargestellt werden kann:
components/block/MediaText/
├── block.json # Metadaten, Attribute, Presets
├── template.php # PHP-Klasse mit Datenaufbereitung
├── index.twig # Twig-Template (HTML-Output)
├── style.css # Scoped CSS (PostCSS, @layer)
├── edit.js # React-basierte Editor-UI
└── index.js # Block-Registrierung block.json: Metadaten und Attribute
Die block.json definiert den Block für WordPress: Name, Attribute mit Typen und Defaults,
Editor-Unterstützung und sogenannte Presets, vordefinierte Konfigurationen für
Spacing und Theming. Alle Block-Daten leben hier als native Gutenberg-Attribute, nicht als ACF-Felder.
edit.js: React-basierte Editor-UI
Die Editor-Oberfläche wird komplett in React gebaut, mit den nativen Gutenberg-Komponenten
InspectorControls und PanelBody. Eigene Shared Components wie
ImageUploadPanel, VideoUploadPanel und BackgroundThemePanel
sorgen für eine konsistente Bedienung über alle Blöcke hinweg.
Der Vorteil gegenüber ACF-basierten Blöcken: Die edit.js rendert eine
Live-Vorschau des Blocks direkt im Editor. Der Redakteur sieht sofort,
wie der Block auf der Website aussehen wird, nicht nur eine Liste von Formularfeldern.
template.php: Datenaufbereitung in PHP
Die PHP-Klasse erbt von BlockComponent und nutzt Traits für
wiederkehrende Funktionalität: HasImages für Bildverarbeitung,
HasBackgroundTheme für Farbvarianten, WithComponentClasses für
dynamische CSS-Klassen. Die Block-Attribute aus block.json werden hier
für das Twig-Template aufbereitet, inklusive Schema-Daten für JSON-LD.
index.twig: Sauberes Markup mit Twig
Hier zeigt sich der größte Vorteil von Timber: Das HTML-Template ist komplett von der
PHP-Logik getrennt. Kein <?php echo ... ?> im Markup, keine verschachtelten
Conditionals. Stattdessen sauberes Twig mit Makros für wiederkehrende Elemente:
{% from 'macros/renderImage.twig' import renderImage %}
{% block content %}
<div class="media">
{{ renderImage(image, {aspectRatio: '16 / 9'}) }}
</div>
<div class="content">
<h2>{{ i18n.heading|e }}</h2>
<p>{{ i18n.text|e }}</p>
</div>
{% endblock %}
Der renderImage-Makro kümmert sich automatisch um srcset, sizes,
WebP/AVIF-Konvertierung, Lazy Loading und Alt-Texte. Kein Entwickler muss sich um
Bild-Optimierung kümmern. Das Framework erledigt das.
Das CSS Layer System
Anstatt CSS-Spezifitätskonflikte mit !important zu lösen, nutzen wir
CSS Cascade Layers:
@layer reset, base, themes, vendor, layout, components, utilities
Jede Ebene hat eine definierte Priorität. Block-CSS lebt in @layer components
und kann nie versehentlich Reset- oder Theme-Styles überschreiben. Das Ergebnis:
keine Spezifitätskonflikte, kein !important, vorhersagbare Kaskade.
Die kritischen Layer (reset, base, themes, layout,
utilities) werden direkt im <head> als Inline-CSS ausgeliefert.
Kein render-blockierender CSS-Download für den initialen Paint.
Theme-System: 5 Varianten, null Komplexität
Jeder Block unterstützt automatisch fünf Farbvarianten: Standard, Light, Dark, Grey und Reset. Der Redakteur wählt im Editor eine Variante, und der Block passt sich über CSS Custom Properties an:
<devslab-block name="MediaText" data-theme="dark">
<!-- Alle Farbtokens werden automatisch überschrieben -->
</devslab-block>
Das data-theme-Attribut überschreibt ~20 CSS-Variablen: Hintergrund, Text, Buttons,
Borders. Kein Block-CSS muss Theme-spezifische Styles definieren. Das Theme-System erledigt das global.
Lazy Script Loading: JavaScript nur wenn nötig
Nicht jeder Block braucht JavaScript. Und wenn doch, dann erst wenn der Block sichtbar wird.
Unser Framework nutzt ein load:on-Attribut mit vier Strategien:
visible(Standard): Script lädt wenn der Block in den Viewport scrolltload: Sofort beim Seitenaufruf (nur für Above-the-Fold)idle: Wenn der Browser nichts zu tun hatinteraction: Erst bei Klick oder Touch (für schwere Libraries)
Das Ergebnis: Eine typische Seite mit 8 Blöcken lädt initial nur 1–2 Scripts. Der Rest wird nachgeladen wenn der Nutzer scrollt. Das verbessert den INP-Wert drastisch.
Warum native Gutenberg-Attribute statt ACF-Blöcke?
Viele Entwickler nutzen ACF Pro, um Gutenberg-Blöcke zu erstellen, mit acf_register_block_type()
und ACF-Feldern als Datenquelle. Das funktioniert, hat aber Nachteile gegenüber nativen
Gutenberg-Attributen mit einer eigenen edit.js:
- Live-Vorschau: Native Blöcke rendern eine React-basierte Vorschau direkt im Editor. ACF-Blöcke zeigen entweder PHP-gerendertes HTML (langsam, kein Echtzeit-Feedback) oder eine Feldliste
- Unabhängigkeit: Die Block-Daten leben in
block.json, kein ACF-Plugin-Lock-in für die Block-Struktur. ACF bleibt für das, wofür es gebaut wurde: globale Optionen und Custom Post Types - Shared Components: Eigene React-Panels (
ImageUploadPanel,BackgroundThemePanel) werden über alle Blöcke wiederverwendet, konsistenter als individuelle ACF-Feldgruppen pro Block - Performance: Block-CSS wird pro Block geladen, nicht global. Keine ACF-spezifischen Assets im Frontend
- Zukunftssicherheit: Die Block API ist WordPress-Core. ACF-Block-Registration ist ein Plugin-Feature, das bei API-Änderungen brechen kann
ACF Pro ist trotzdem Teil unseres Stacks, aber gezielt eingesetzt: Translatable Options für mehrsprachige Strings und globale Konfiguration, Options Pages für Seitenübergreifende Einstellungen, und Feldgruppen auf CPTs mit festem Layout (z.B. Team-Mitglieder, Referenzen). Die Block-Inhalte selbst kommen über native Gutenberg-Attribute.
Was bedeutet das für die Wartung?
Custom Blocks sind stabiler als Page Builder, aber sie brauchen trotzdem Pflege. WordPress-Core-Updates, PHP-Updates und Gutenberg-Updates können Breaking Changes mitbringen. Deshalb gehört ein professioneller Wartungsservice zu jedem Projekt: Updates werden auf einer Staging-Umgebung getestet, bevor sie live gehen.