Zum Inhalt springen

Custom Gutenberg Blocks mit Timber & Twig: So baue ich WordPress-Websites

Page Builder wie Elementor oder WPBakery sind bequem, aber sie erzeugen aufgeblähten Code, sind langsam und ein Wartungs-Albtraum. Ich setze stattdessen auf maßgeschneiderte Gutenberg-Blöcke mit PHP 8.2, Timber/Twig und einem eigenen Block-Framework. Warum, und wie der Aufbau im Einzelnen aussieht.

Von Dennis Theis aktualisiert 7 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: mehrere hundert Kilobyte JavaScript allein für den Builder, in den Installationen, die ich übernommen habe, meist zwischen 400 und 800 KB. Dazu verschachtelte <div>-Strukturen ohne Semantik und eine Abhängigkeit vom Plugin, die bei Updates regelmäßig für Probleme sorgt.

Die Builder-Seiten, die ich zum Optimieren bekomme, liegen beim Lighthouse-Score auf Mobile fast immer unter 70. Das ist kein Zufall, sondern strukturell: Page Builder rendern auf der Client-Seite und laden Assets, die für die einzelne Seite unnötig sind.

Mein Ansatz: Block-basiertes WordPress

Statt eines Page Builders setze ich 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, nutze ich CSS Cascade Layers. Der Unterschied: Bei klassischem CSS entscheidet die Spezifität des Selektors, wer gewinnt, weshalb Projekte mit der Zeit immer längere Selektorketten ansammeln. Mit Cascade Layers entscheidet stattdessen die Reihenfolge der Layer, unabhängig von der Spezifität. Ein Block-Style in einem späteren Layer überschreibt einen Reset im früheren, auch wenn der Reset der spezifischere Selektor wäre. Damit verschwindet der häufigste Grund für !important:

@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. Mein 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 meines 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.

Wie sich das für Redakteure anfühlt

Der technische Vergleich ist die eine Hälfte. Die andere ist die Frage, ob damit täglich gearbeitet werden kann. Eine der Agenturen, für die ich seit Jahren entwickle, pflegt ihre Projekte auf genau diesem Setup, und die Rückmeldung fällt konsistent in drei Punkte.

Der Umstieg von Elementor ist zunächst ungewohnt. Das ist die ehrliche Kehrseite: Wer es gewohnt ist, Elemente frei auf eine Seite zu ziehen, findet sich in einer Struktur wieder, die bewusst Grenzen setzt. Diese Umstellung dauert ein bis zwei Sitzungen.

Danach ist die Orientierung besser, nicht schlechter. Weil jeder Block dieselbe Logik hat, dieselben Panels und dieselben Bezeichnungen, ist nach kurzer Einarbeitung klar, wo was liegt. Ein Page Builder bietet mehr Möglichkeiten pro Element, aber jede Seite kann anders aufgebaut sein. Ein Blocksystem bietet weniger Möglichkeiten und dafür Wiedererkennbarkeit.

Änderungen gehen schnell, auch ungewöhnliche. Inhaltliche Anpassungen erledigen die Redakteure selbst, in der Regel in Minuten. Und wenn ein Projekt etwas braucht, das kein Block abbildet, ist das kein Fall für ein weiteres Plugin, sondern für einen neuen Block. Genau das ist der Unterschied zwischen eigenem Code und einem Produkt: Die Grenze verläuft nicht dort, wo der Hersteller sie gezogen hat.

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.

Ein aktuelles Beispiel dafür ist der erzwungene iframed Editor in WordPress 7.1. Er betrifft jeden Block, dessen Styles über admin_enqueue_scripts statt über enqueue_block_assets eingebunden werden, und fällt im Frontend zunächst gar nicht auf.

Für Agenturen, die diese Expertise nicht inhouse haben, biete ich langfristige Entwicklungspartnerschaften an. Du lieferst Design und Kundenbeziehung, ich liefere die technische Umsetzung mit Custom Blocks und Timber/Twig.


Dennis Theis

Freiberuflicher Webentwickler aus Berlin. Schwerpunkt sind Websites, die an die Systeme angebunden sind, in denen die Daten schon liegen.

Mehr über meine Arbeitsweise

Weiterlesen

Custom WordPress-Entwicklung gesucht?

Ich entwickle individuelle Gutenberg-Blöcke mit Timber, Twig und nativer Block API, performant, wartbar und exakt auf deine Anforderungen zugeschnitten.

theis@devslab.de +49 174 7284457 WhatsApp: schnelle Nachricht

Anliegen Projekt besprechen