Zum Inhalt springen
WordPress-Entwicklung

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.

Von Dennis Theis · · 9 Min. Lesezeit

Programmcode auf einem Monitor, Webentwicklung mit modernen Frameworks

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 scrollt
  • load: Sofort beim Seitenaufruf (nur für Above-the-Fold)
  • idle: Wenn der Browser nichts zu tun hat
  • interaction: 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.