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.

Was dieses Tutorial NICHT ist
Das ist kein DIVI-für-Dummies-Guide. Du solltest WordPress-Hooks, ein bisschen React und PHP-OOP schon mal gesehen haben. Aber: Du musst weder TypeScript-Profi noch Webpack-Magier sein. Wir gehen jeden Begriff einmal sauber durch.

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 -v kurz 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!
Du wirst während der Entwicklung viele Builds laufen lassen, oft die Datenbank zerschießen und Caches plätten. Mach 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

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 mit d5/ 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:

  • GroupContainer bündelt verwandte Felder zu einer aufklappbaren Gruppe im Builder.
  • Field attrName="title" verbindet das Feld mit dem Attribut aus der module.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" />
  </>
);
Style-Groups sind ein Geschenk
In DIVI 4 musstest du Spacing-, Border- und Shadow-Logik manuell pro Modul implementieren. In DIVI 5 sind das fertige Komponenten – inklusive Responsive-Tabs, Hover-States und Sticky-States. Nutze 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 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 die module.json ein 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,
        ]);
    }
}
Escaping nicht vergessen
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.

1

Plugin aktivieren

WordPress-Admin → Plugins → „Feature Box for DIVI 5“ aktivieren.

2

Test-Seite anlegen

Eine neue Seite mit DIVI-Builder erstellen, im Modul-Picker nach „Feature Box“ suchen.

3

Settings durchklicken

Title, Description, Icon, Button-URL setzen. Bei jedem Tippen sollte sich die Vorschau live updaten.

4

Design-Tab prüfen

Spacing, Border, Background ändern. Sieht das im Builder so aus wie auf der gerenderten Seite?

5

Frontend-Render testen

Seite veröffentlichen, im Inkognito-Tab öffnen. Stimmt das HTML? Werden die Styles korrekt angewendet?

6

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:

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 du Werte im Settings-Panel änderst 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: deine 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.
PHP Fatal Error nach Plugin-Aktivierung
Meistens ein fehlender Composer-Autoload. Lass 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

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.

Michael Rademacher

Michael Rademacher

Gründer & Geschäftsführer

CEO, Creative Director & Web Developer Michael Rademacher hat Multimedia und Kommunikation studiert und mit Bachelor of Arts abgeschlossen. Seit 2003 ist er selbstständiger Webentwickler und Filmemacher. Seine Kreativagentur realmaker realisiert Webprojekte, Imagefilme, Webcasts und Luftaufnahmen.