DIVI 5 Module selbst entwickeln: Der komplette Schritt-für-Schritt-Guide
KI-generiertInhalt
- Was Sie am Ende gebaut haben werden
- Voraussetzungen
- Die Anatomie eines DIVI 5 Moduls
- Schritt 1: Extension-Boilerplate aufsetzen
- Schritt 2: module.json definieren
- Schritt 3: TypeScript Types
- Schritt 4: Settings (Content & Design Tab)
- Schritt 5: Edit-Component (Visual Builder)
- Schritt 6: Styles
- Schritt 7: PHP-Klasse mit Traits
- Schritt 8: Render-Callback
- Schritt 9: Build & Test
- Häufige Stolperfallen
- Quality-Checkliste
- Fazit
Der DIVI Module Builder ist nett für schnelle Wrapper – aber wenn Sie ernsthafte Module mit eigener Logik bauen wollen, kommen Sie 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 Sie am Ende gebaut haben werden
Damit das Tutorial nicht im Abstrakten hängt, bauen wir gemeinsam ein konkretes Modul: eine Feature Box. Die kennen Sie 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: Sie haben Text-Felder, einen Icon-Picker, ein Link-Feld, Design-Optionen für Farben und Spacing – also alles, was DIVI 5 Ihnen an Settings-APIs anbietet, ohne dass Sie im Beispiel ertrinken.
Was dieses Tutorial NICHT ist
Das ist kein DIVI-für-Dummies-Guide. Sie sollten WordPress-Hooks, ein bisschen React und PHP-OOP schon mal gesehen haben. Aber: Sie müssen weder TypeScript-Profi noch Webpack-Magier sein. Wir gehen jeden Begriff einmal sauber durch.
Voraussetzungen
Bevor wir starten, einmal kurz Inventur. Drei Dinge müssen Sie 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.
Lokale Umgebung verwenden!
Sie werden während der Entwicklung viele Builds laufen lassen, oft die Datenbank zerschießen und Caches plätten. Machen Sie das auf LocalWP, DDEV oder Docker – nicht auf einer produktiven Seite. Auch nicht auf einer Staging-Umgebung mit echten Kunden-Daten.
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.
Die zwei Welten verstehen
TypeScript-Welt: alles, was im Visual Builder passiert – Settings, Live-Preview, Drag&Drop. Das läuft im Browser.
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
Sie könnten alles von Hand schreiben. Aber niemand macht das. Elegant Themes pflegt ein offizielles Beispiel-Repo, das Ihnen 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 finden Sie mehrere Beispiel-Module. Die können Sie als Referenz behalten, bevor Sie sie löschen. 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 Ihr ständiger Begleiter.
Schritt 2: module.json definieren
Die module.json ist das Herzstück Ihres 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 Sie 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 Ihre Attribute als TypeScript-Interface. Klingt nach Bürokratie, ist aber Gold wert: Sobald Sie irgendwo im Code ein Attribut falsch schreibst, schreit der Compiler – statt dass Sie den Bug erst im Browser merken.
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 Ihren Wert in die responsive Struktur. Sie arbeiten später nicht mit attrs.title direkt, sondern liest den passenden Breakpoint heraus – darum kümmern sich aber DIVI-Helper, nicht Sie.
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 Sie nur konfigurieren müssen:
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 – Sie bauen 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. Sie müssen 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" />
</>
);
Style-Groups sind ein Geschenk
In DIVI 4 mussten Sie Spacing-, Border- und Shadow-Logik manuell pro Modul implementieren. In DIVI 5 sind das fertige Komponenten – inklusive Responsive-Tabs, Hover-States und Sticky-States. Nutzen Sie sie, wo immer es geht.
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 Ihre 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 definieren Sie 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 Ihrem 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ängen Sie 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,
]);
}
}
Escaping nicht vergessen
esc_html(), esc_attr(), esc_url() – pflicht. Sobald Sie Nutzereingaben unescaped ausgeben, haben Sie eine XSS-Lücke. WordPress ist hier strikt, der WP Plugin Reviewer auch.
Plugin-Bootstrap
Damit WordPress Ihr Modul kennt, registrieren Sie 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 Ihren 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 rennen Sie garantiert in mindestens drei davon. Damit das schneller geht:
Modul taucht nicht im Builder auf
99% der Fälle: Build nicht gelaufen oder Cache nicht geleert. 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).
Settings ändern nichts
Wenn Sie Werte im Settings-Panel ändern und im Builder nichts passiert: meistens passt der attrName in settings-content.tsx nicht zu dem Schlüssel in der module.json. Tippfehler-Check zuerst.
Frontend zeigt anderen Output als Builder
Das ist der Klassiker. Ursache: Ihre edit.tsx rendert anderes HTML als Ihr PHP-Render-Callback. Beide müssen strukturell identisch sein – gleiche Klassennamen, gleiche Verschachtelung. Sonst greifen die Style-Selektoren auf einer Seite ins Leere.
PHP Fatal Error nach Plugin-Aktivierung
Meistens ein fehlender Composer-Autoload. Lassen Sie composer dump-autoload laufen und prüfen Sie, ob das Namespace-Mapping in composer.json mit Ihrer Verzeichnisstruktur übereinstimmt.
Quality-Checkliste
Bevor Sie das Modul einem Kunden vorsetzt – einmal alles durchgehen:
Checkliste
Fazit
DIVI-5-Module zu bauen ist mehr Arbeit als in DIVI 4 – aber die Architektur ist endlich erwachsen. Sie kriegen Typsicherheit, klare Trennung von Frontend und Backend, ein modulares Trait-System statt Mega-Klassen. Wenn Sie erstmal das erste Modul durch haben, ist das nächste eine Frage von Stunden, nicht Tagen.
Die größte Hürde ist nicht der Code – sondern zu verstehen, dass Sie jetzt zwei Welten parallel pflegen: TypeScript für den Builder, PHP fürs Frontend. Wer das verinnerlicht, hat die halbe Miete.
Michael Rademacher, realmaker
Ressourcen
Michael Rademacher
Gründer von realmaker
Michael Rademacher gründete realmaker 2003. Er entwickelt WordPress-Websites und Software, produziert Filme und Livestreams und schreibt hier über das, was er in Kundenprojekten lernt.
Über MichaelLieber betreuen lassen?
Updates, Backups und Überwachung übernimmt wp-einfach.de – zum festen Monatspreis.
