DIVI 5 Module selbst entwickeln: Der komplette Schritt-für-Schritt-Guide

Würfel aus Holz- und weißen Bausteinen, lose Bauteile und ein Schraubendreher, daneben eine kobaltblaue KugelKI-generiert
Inhalt

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 -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!

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

  • 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 – 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 die module.json ein 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.

  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 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 Michael

Lieber betreuen lassen?

Updates, Backups und Überwachung übernimmt wp-einfach.de – zum festen Monatspreis.

Zu wp-einfach.de