Der DIVI Module Builder ist nett für schnelle Wrapper – aber wenn du ernsthafte Module mit eigener Logik bauen willst, kommst du am Code-First-Ansatz nicht vorbei. DIVI 5 hat die Spielregeln dafür komplett neu geschrieben: TypeScript statt JSX, Traits statt Mega-Klassen, JSON statt Shortcodes. Klingt nach mehr Arbeit. Ist es auch – aber mit System.
Was du am Ende gebaut haben wirst
Damit das Tutorial nicht im Abstrakten hängt, bauen wir gemeinsam ein konkretes Modul: eine Feature Box. Die kennst du von hundert Landingpages – Icon oben, fetter Titel, kurze Beschreibung, optional ein Call-to-Action-Button.
Klingt simpel. Ist auch simpel. Aber genau deshalb perfekt zum Lernen: Du hast Text-Felder, einen Icon-Picker, ein Link-Feld, Design-Optionen für Farben und Spacing – also alles, was DIVI 5 dir an Settings-APIs anbietet, ohne dass du im Beispiel ertrinkst.
Voraussetzungen
Bevor wir starten, einmal kurz Inventur. Drei Dinge musst du laufen haben:
- WordPress + DIVI 5 (Theme oder Plugin-Variante) auf einer lokalen oder Staging-Umgebung. Niemals direkt auf Live entwickeln.
- Node.js 18+ und npm – für den Build-Prozess. Mit
node -vkurz prüfen. - PHP 8.0+ – DIVI 5 nutzt moderne Sprach-Features wie Constructor Property Promotion und Traits.
Außerdem braucht’s einen Code-Editor mit TypeScript-Support – VS Code mit der TypeScript-Erweiterung reicht völlig. Und Git, weil wir gleich ein offizielles Boilerplate klonen.
Die Anatomie eines DIVI 5 Moduls
DIVI 5 trennt strikt zwischen Frontend (TypeScript/React) und Backend (PHP). Beide Teile leben in derselben Extension – aber in getrennten Verzeichnissen, mit getrennten Verantwortlichkeiten.
feature-box-extension/ │ ├── modules/ # PHP – läuft serverseitig │ └── FeatureBox/ │ ├── FeatureBoxTrait/ │ │ ├── CustomCssTrait.php # Custom-CSS-Definitionen │ │ ├── ModuleClassnamesTrait.php # CSS-Klassen am Wrapper │ │ ├── ModuleStylesTrait.php # dynamische Styles │ │ └── RenderCallbackTrait.php # das eigentliche HTML │ └── FeatureBox.php # Modul-Hauptklasse │ ├── src/ # TypeScript – läuft im Visual Builder │ └── components/ │ └── feature-box/ │ ├── module.json # ← Herzstück: Metadaten │ ├── types.ts # Attribute-Interfaces │ ├── edit.tsx # Visual-Builder-Komponente │ ├── settings-content.tsx # Content-Tab │ ├── settings-design.tsx # Design-Tab │ ├── styles.tsx # CSS-in-JS │ ├── custom-css.ts # Custom-CSS-Slots │ └── index.ts # Export-Sammelstelle │ ├── feature-box-extension.php # Plugin-Bootstrap ├── package.json # npm-Dependencies ├── tsconfig.json # TypeScript-Config └── webpack.config.js # Build-Setup
Das wirkt erstmal viel. Aber jede Datei hat genau eine Aufgabe – und die meisten sind nach dem ersten Modul Copy-Paste-Routine.
PHP-Welt: alles, was die fertige Seite ans Frontend liefert – das HTML, das der Besucher sieht. Beide Welten teilen sich nur die
module.json als Vertrag.Schritt 1: Extension-Boilerplate aufsetzen
Du könntest alles von Hand schreiben. Aber niemand macht das. Elegant Themes pflegt ein offizielles Beispiel-Repo, das dir die ganze Build-Konfiguration abnimmt:
# Offizielles Boilerplate klonen
git clone https://github.com/elegantthemes/d5-extension-example-modules.git feature-box-extension
cd feature-box-extension
# Dependencies installieren
npm install
composer install
Im Boilerplate findest du mehrere Beispiel-Module. Die kannst du als Referenz behalten, bevor du sie löschst. Wir legen jetzt unser eigenes Modul-Verzeichnis daneben an:
# Verzeichnisse für unser Modul
mkdir -p src/components/feature-box
mkdir -p modules/FeatureBox/FeatureBoxTrait
Bevor wir Code schreiben, einmal kurz zur package.json. Im Boilerplate steht da unter scripts bereits alles, was wir brauchen:
{
"scripts": {
"dev": "webpack --mode development --watch",
"build": "webpack --mode production",
"lint": "eslint src --ext .ts,.tsx"
}
}
npm run dev startet einen Watcher, der bei jeder Code-Änderung neu kompiliert. Das ist ab jetzt dein ständiger Begleiter.
Schritt 2: module.json definieren
Die module.json ist das Herzstück deines Moduls. Sie sagt DIVI:
- Wie heißt das Modul (intern und für den Builder)?
- Welche Attribute kann der Nutzer setzen?
- Welche Default-Werte gelten?
- Welche Settings-Komponenten sollen geladen werden?
{
"d5": {
"name": "d5/feature-box",
"d4Name": "",
"label": "Feature Box",
"category": "module",
"description": "Icon, Titel, Beschreibung und CTA-Button in einer Box.",
"icon": "feature-box-icon",
"keywords": ["feature", "box", "card", "cta"],
"styles": [
"./styles.tsx"
],
"settings": {
"content": "./settings-content.tsx",
"design": "./settings-design.tsx"
},
"attributes": {
"title": {
"default": {
"desktop": { "value": "Dein Feature-Titel" }
}
},
"description": {
"default": {
"desktop": { "value": "Eine kurze, knackige Beschreibung des Features." }
}
},
"icon": {
"default": {
"desktop": { "value": "\\f005" }
}
},
"buttonText": {
"default": {
"desktop": { "value": "Mehr erfahren" }
}
},
"buttonUrl": {
"default": {
"desktop": { "value": "#" }
}
}
}
}
}
Drei Felder verdienen besondere Aufmerksamkeit:
name– muss mitd5/beginnen und global eindeutig sein.d4Name– nur relevant, wenn du ein bestehendes DIVI-4-Modul ablöst (z.B."et_pb_my_old_module"). Bei einem komplett neuen Modul: leer lassen.attributes– die Datenstruktur. Jeder Default-Wert ist nach Breakpoints strukturiert (desktop,tablet,phone) – DIVI ist responsive bis ins Mark.
Schritt 3: TypeScript Types
Die types.ts beschreibt deine Attribute als TypeScript-Interface. Klingt nach Bürokratie, ist aber Gold wert: Sobald du irgendwo im Code ein Attribut falsch schreibst, schreit der Compiler – statt dass du den Bug erst im Browser merkst.
import {
ModuleAttrs,
ResponsiveValue,
} from '@divi/types';
export interface FeatureBoxAttrs extends ModuleAttrs {
title: ResponsiveValue<string>;
description: ResponsiveValue<string>;
icon: ResponsiveValue<string>;
buttonText: ResponsiveValue<string>;
buttonUrl: ResponsiveValue<string>;
}
export interface FeatureBoxProps {
attrs: FeatureBoxAttrs;
id: string;
name: string;
parentId?: string;
}
Wichtig: ResponsiveValue<T> wickelt deinen Wert in die responsive Struktur. Du arbeitest später nicht mit attrs.title direkt, sondern liest den passenden Breakpoint heraus – darum kümmern sich aber DIVI-Helper, nicht du.
Schritt 4: Settings (Content & Design Tab)
Im Visual Builder gibt’s für jedes Modul drei Tabs: Content, Design und Advanced. Wir definieren die ersten beiden – Advanced bekommt DIVI automatisch.
Der Content-Tab
Hier landen die inhaltlichen Felder – Texte, Icon, Link. Jedes Field ist eine eigene React-Komponente von DIVI, die du nur konfigurieren musst:
import React from 'react';
import {
GroupContainer,
Field,
TextInput,
TextArea,
IconPicker,
LinkInput,
} from '@divi/field-library';
import { useAttributes } from '@divi/module';
export const SettingsContent = () => {
const [attrs, setAttrs] = useAttributes();
return (
<>
<GroupContainer id="text" title="Text">
<Field label="Titel" attrName="title">
<TextInput />
</Field>
<Field label="Beschreibung" attrName="description">
<TextArea />
</Field>
</GroupContainer>
<GroupContainer id="icon" title="Icon">
<Field label="Icon" attrName="icon">
<IconPicker />
</Field>
</GroupContainer>
<GroupContainer id="button" title="Button">
<Field label="Button-Text" attrName="buttonText">
<TextInput />
</Field>
<Field label="Button-Link" attrName="buttonUrl">
<LinkInput />
</Field>
</GroupContainer>
</>
);
};
Drei Sachen passieren hier:
GroupContainerbündelt verwandte Felder zu einer aufklappbaren Gruppe im Builder.Field attrName="title"verbindet das Feld mit dem Attribut aus dermodule.json.- Die Eingabe-Komponenten (
TextInput,IconPicker, …) kommen aus DIVIs Field-Library – du baust sie nicht selbst.
Der Design-Tab
Für den Design-Tab nutzt DIVI sogenannte Style-Groups. Das sind fertige Bündel an Settings für typische Aufgaben – Spacing, Border, Box-Shadow, Typografie. Du musst sie nur einbinden:
import React from 'react';
import {
BackgroundGroup,
SpacingGroup,
BorderGroup,
BoxShadowGroup,
FontGroup,
ButtonGroup,
} from '@divi/module-library';
export const SettingsDesign = () => (
<>
<FontGroup
groupLabel="Titel-Typografie"
attrName="titleFont"
/>
<FontGroup
groupLabel="Beschreibungs-Typografie"
attrName="descriptionFont"
/>
<ButtonGroup
groupLabel="Button"
attrName="button"
/>
<BackgroundGroup attrName="background" />
<SpacingGroup attrName="spacing" />
<BorderGroup attrName="border" />
<BoxShadowGroup attrName="boxShadow" />
</>
);
Schritt 5: Edit-Component (Visual Builder)
Die edit.tsx ist das, was der Nutzer im Visual Builder sieht. Wichtig: Das ist nicht das endgültige Frontend-HTML – das macht später PHP. Aber es muss visuell so nah wie möglich dran sein, damit WYSIWYG funktioniert.
import React from 'react';
import { ModuleContainer } from '@divi/module';
import { FeatureBoxProps } from './types';
export const FeatureBoxEdit: React.FC<FeatureBoxProps> = ({
attrs,
id,
name,
parentId,
}) => {
const title = attrs?.title?.desktop?.value ?? '';
const description = attrs?.description?.desktop?.value ?? '';
const icon = attrs?.icon?.desktop?.value ?? '';
const buttonText = attrs?.buttonText?.desktop?.value ?? '';
const buttonUrl = attrs?.buttonUrl?.desktop?.value ?? '#';
return (
<ModuleContainer
id={id}
name={name}
parentId={parentId}
classnames="feature-box"
>
{icon && (
<div className="feature-box__icon">
<span data-icon={icon} />
</div>
)}
<h3 className="feature-box__title">{title}</h3>
<p className="feature-box__description">{description}</p>
{buttonText && (
<a href={buttonUrl} className="feature-box__button">
{buttonText}
</a>
)}
</ModuleContainer>
);
};
ModuleContainer wickelt deine Komponente in das Standard-Wrapper-Element, das DIVI für Hover-Controls, Drag&Drop und Sticky-States braucht. Niemals weglassen.
Schritt 6: Styles
Die styles.tsx übersetzt Attribute aus dem Design-Tab in echtes CSS. DIVI nennt das StyleDeclarations: jede Style-Group registriert sich, schaut im attrs-Objekt nach ihrem Wert und pumpt das passende CSS raus.
import React from 'react';
import {
StyleContainer,
CommonStyle,
FontStyle,
BackgroundStyle,
SpacingStyle,
BorderStyle,
BoxShadowStyle,
} from '@divi/module';
import { FeatureBoxAttrs } from './types';
interface StylesProps {
attrs: FeatureBoxAttrs;
orderClass: string;
}
export const FeatureBoxStyles: React.FC<StylesProps> = ({
attrs,
orderClass,
}) => (
<StyleContainer>
{/* Wrapper-Styles */}
<CommonStyle selector={`${orderClass}.feature-box`}>
<BackgroundStyle attr={attrs.background} />
<SpacingStyle attr={attrs.spacing} />
<BorderStyle attr={attrs.border} />
<BoxShadowStyle attr={attrs.boxShadow} />
</CommonStyle>
{/* Titel */}
<CommonStyle selector={`${orderClass} .feature-box__title`}>
<FontStyle attr={attrs.titleFont} />
</CommonStyle>
{/* Beschreibung */}
<CommonStyle selector={`${orderClass} .feature-box__description`}>
<FontStyle attr={attrs.descriptionFont} />
</CommonStyle>
</StyleContainer>
);
Das orderClass ist eine eindeutige Klasse pro Modul-Instanz (z.B. .et_pb_feature_box_0_tb_body) – damit beißen sich Settings unterschiedlicher Modul-Instanzen nicht.
Schritt 7: PHP-Klasse mit Traits
Damit verlassen wir die TypeScript-Welt. Auf der PHP-Seite definierst du die Modul-Klasse, die DIVI im Frontend rendert. In DIVI 4 war das eine 800-Zeilen-Klasse. In DIVI 5 ist es eine schlanke Hauptklasse plus Traits, die die Logik aufteilen.
Die Hauptklasse
<?php
declare(strict_types=1);
namespace MyExtension\Modules\FeatureBox;
use ET\Builder\Framework\Utility\Conditions;
use ET\Builder\Packages\Module\Module;
use ET\Builder\Packages\Module\Options\Css\CssTrait;
use MyExtension\Modules\FeatureBox\FeatureBoxTrait\CustomCssTrait;
use MyExtension\Modules\FeatureBox\FeatureBoxTrait\ModuleClassnamesTrait;
use MyExtension\Modules\FeatureBox\FeatureBoxTrait\ModuleStylesTrait;
use MyExtension\Modules\FeatureBox\FeatureBoxTrait\RenderCallbackTrait;
class FeatureBox
{
use CssTrait;
use CustomCssTrait;
use ModuleClassnamesTrait;
use ModuleStylesTrait;
use RenderCallbackTrait;
public static function load(): void
{
$module_json_folder_path = dirname(__DIR__, 2)
. '/src/components/feature-box/';
add_filter(
'divi_conversion_presets_attrs_map',
[self::class, 'presetAttrsMap'],
10,
2
);
Module::register(
$module_json_folder_path,
[
'render_callback' => [self::class, 'renderCallback'],
]
);
}
public static function presetAttrsMap(array $map, string $module): array
{
if ('d5/feature-box' !== $module) {
return $map;
}
return $map; // hier ggf. Preset-Mapping ergänzen
}
}
Was hier passiert:
Module::register()ist die zentrale API – DIVI liest diemodule.jsonein und verheiratet sie mit deinem Render-Callback.- Die
use-Statements ziehen die Traits rein. Jeder Trait kapselt einen Aspekt: CSS, Klassennamen, Styles, Render-HTML.
Der Klassennamen-Trait
<?php
declare(strict_types=1);
namespace MyExtension\Modules\FeatureBox\FeatureBoxTrait;
use ET\Builder\Packages\Module\Layout\Components\Classnames;
trait ModuleClassnamesTrait
{
public static function moduleClassnames(array $args): void
{
/** @var Classnames $classnamesInstance */
$classnamesInstance = $args['classnamesInstance'];
$attrs = $args['attrs'] ?? [];
$classnamesInstance->add('feature-box', true);
if (! empty($attrs['icon']['desktop']['value'])) {
$classnamesInstance->add('feature-box--has-icon', true);
}
}
}
Hier hängst du conditionale Klassen an den Wrapper – etwa feature-box--has-icon, wenn der Nutzer ein Icon gewählt hat.
Schritt 8: Render-Callback
Der wichtigste Trait: hier entsteht das HTML, das tatsächlich an den Browser geht.
<?php
declare(strict_types=1);
namespace MyExtension\Modules\FeatureBox\FeatureBoxTrait;
use ET\Builder\Packages\Module\Module;
use ET\Builder\Packages\Module\Options\Element\ElementComponents;
trait RenderCallbackTrait
{
public static function renderCallback(
array $attrs,
string $content,
\WP_Block $block,
array $elements
): string {
$title = $attrs['title']['desktop']['value'] ?? '';
$description = $attrs['description']['desktop']['value'] ?? '';
$icon = $attrs['icon']['desktop']['value'] ?? '';
$buttonText = $attrs['buttonText']['desktop']['value'] ?? '';
$buttonUrl = $attrs['buttonUrl']['desktop']['value'] ?? '#';
$iconHtml = $icon
? sprintf(
'<div class="feature-box__icon"><span data-icon="%s"></span></div>',
esc_attr($icon)
)
: '';
$buttonHtml = $buttonText
? sprintf(
'<a href="%s" class="feature-box__button">%s</a>',
esc_url($buttonUrl),
esc_html($buttonText)
)
: '';
$children = sprintf(
'%s<h3 class="feature-box__title">%s</h3>'
. '<p class="feature-box__description">%s</p>%s',
$iconHtml,
esc_html($title),
esc_html($description),
$buttonHtml
);
return Module::render([
'attrs' => $attrs,
'elements' => $elements,
'id' => $block->parsed_block['id'] ?? '',
'name' => $block->block_type->name,
'classnamesFunction' => [self::class, 'moduleClassnames'],
'stylesComponent' => [self::class, 'moduleStyles'],
'children' => $children,
]);
}
}
esc_html(), esc_attr(), esc_url() – pflicht. Sobald du Nutzereingaben unescaped ausgibst, hast du eine XSS-Lücke. WordPress ist hier strikt, der WP Plugin Reviewer auch.Plugin-Bootstrap
Damit WordPress dein Modul kennt, registrierst du es in der Plugin-Hauptdatei:
<?php
/**
* Plugin Name: Feature Box for DIVI 5
* Description: Custom Feature Box module for DIVI 5.
* Version: 1.0.0
* Requires PHP: 8.0
*/
declare(strict_types=1);
if (! defined('ABSPATH')) {
exit;
}
require_once __DIR__ . '/vendor/autoload.php';
add_action('divi_module_library_modules_dependency_tree', function () {
\MyExtension\Modules\FeatureBox\FeatureBox::load();
});
Schritt 9: Build & Test
Jetzt der Moment der Wahrheit. Build starten:
npm run dev
Webpack kompiliert deinen TypeScript-Code, hängt sich an die DIVI-Builder-Bundles und bleibt im Watch-Modus. Bei jeder Änderung baut er neu.
Plugin aktivieren
WordPress-Admin → Plugins → „Feature Box for DIVI 5“ aktivieren.
Test-Seite anlegen
Eine neue Seite mit DIVI-Builder erstellen, im Modul-Picker nach „Feature Box“ suchen.
Settings durchklicken
Title, Description, Icon, Button-URL setzen. Bei jedem Tippen sollte sich die Vorschau live updaten.
Design-Tab prüfen
Spacing, Border, Background ändern. Sieht das im Builder so aus wie auf der gerenderten Seite?
Frontend-Render testen
Seite veröffentlichen, im Inkognito-Tab öffnen. Stimmt das HTML? Werden die Styles korrekt angewendet?
Responsive testen
Im Builder zwischen Desktop, Tablet und Phone wechseln. Werte pro Breakpoint setzen, und kontrollieren, dass das Frontend mitzieht.
Häufige Stolperfallen
Beim ersten Modul rennst du garantiert in mindestens drei davon. Damit das schneller geht:
npm run build erneut ausführen, dann den DIVI-internen Static-Cache leeren (Divi → Theme Options → Builder → Advanced → Static CSS File Generation deaktivieren während Entwicklung).attrName in settings-content.tsx nicht zu dem Schlüssel in der module.json. Tippfehler-Check zuerst.edit.tsx rendert anderes HTML als dein PHP-Render-Callback. Beide müssen strukturell identisch sein – gleiche Klassennamen, gleiche Verschachtelung. Sonst greifen die Style-Selektoren auf einer Seite ins Leere.composer dump-autoload laufen und prüfe, ob das Namespace-Mapping in composer.json mit deiner Verzeichnisstruktur übereinstimmt.Quality-Checkliste
Bevor du das Modul einem Kunden vorsetzt – einmal alles durchgehen:
Fazit
DIVI-5-Module zu bauen ist mehr Arbeit als in DIVI 4 – aber die Architektur ist endlich erwachsen. Du kriegst Typsicherheit, klare Trennung von Frontend und Backend, ein modulares Trait-System statt Mega-Klassen. Wenn du erstmal das erste Modul durch hast, ist das nächste eine Frage von Stunden, nicht Tagen.
Die größte Hürde ist nicht der Code – sondern zu verstehen, dass du jetzt zwei Welten parallel pflegst: TypeScript für den Builder, PHP fürs Frontend. Wer das verinnerlicht, hat die halbe Miete.
Michael Rademacher, realmaker
Ressourcen
- 📦 Official D5 Example Modules (GitHub)
- 🛠️ DIVI Developer Documentation
- 📚 DIVI 5 Help Center
- 📘 TypeScript Handbook
Kein Bock auf das ganze Setup?
Wenn du eigene DIVI-5-Module brauchst, aber nicht selbst Wochen in TypeScript-Boilerplate stecken willst – ich baue dir das. Sauber, performant, dokumentiert.