Zum Inhalt

Modul-Vertrag

Diese Seite beschreibt, wie Module aufgebaut sind und welche Regeln zwischen ihnen gelten. Sie ist die Grundlage für Eigenes Modul.

Das Ziel

Ein Modul muss so gekapselt sein, dass sein Entfernen keinen Build-Bruch verursacht und möglichst wenig zurückbleibt. Ordner löschen, Schalter umlegen oder Filter setzen — alle drei Wege müssen zu einem funktionierenden Zustand führen.

Wo Module liegen

Ort Art
includes/theme-options/default/{slug}/{slug}.php Kern-Modul
includes/theme-options/add-ons/{slug}/{slug}.php Add-on-Modul
includes/theme-options/woocommerce/{slug}/{slug}.php Shop-Modul

Ein Modul ist ein Ordner mit einer gleichnamigen PHP-Datei. Gefunden wird es automatisch, ein Eintrag in einer Liste ist nicht nötig.

Wie ein Modul abgeschaltet wird

Vier Wege, in der Reihenfolge ihrer Angemessenheit:

  1. Theme → Module in der Administration — der Normalfall
  2. Die Option twtheme_modules über twtheme_modules_apply_state() setzen
  3. Die Filter twtheme_addons_skip und twtheme_default_addons_skip aus dem Child
  4. Den Ordner löschen

Der Modulzustand

Quelle der Wahrheit ist eine gewöhnliche WordPress-Option auf der jeweiligen Seite:

[
  'schema'     => 1,
  'managed_by' => 'local',   // oder 'hub'
  'source'     => 'admin',   // oder 'twhub', 'cli'
  'updated_at' => '2026-08-05T12:00:00+00:00',
  'modules'    => ['megamenu' => true, 'slider' => false],
]

includes/theme-options/modules.php liest sie, bevor default/default.php und add-ons/add-ons.php ihre Suche starten. Die Datenbank steht zu diesem Zeitpunkt bereits zur Verfügung.

Fehlt ein Slug, gilt defaultEnabled aus der module.json. Ein neu ausgeliefertes Modul ist damit automatisch an.

Nur über eine Funktion schreiben

Geschrieben wird ausschließlich über twtheme_modules_apply_state() — von der Admin-Seite, vom TW Hub Agent und von WP-CLI gleichermaßen. Dort sitzen die Prüfung gegen die Registry und die Auflösung der Abhängigkeiten.

Das Theme ruft dabei nirgends nach Hause. Es gibt keinen ausgehenden Request und keine Lizenzprüfung. Ein Hub ist nur einer von drei möglichen Schreibern dieser Option; fällt er weg, bleibt der Zustand bestehen und über Theme → Module änderbar.

Metadaten in der module.json

{
  "name": "Megamenü",
  "description": "Mehrspaltiges Hauptmenü auf Basis der WordPress-Menüs.",
  "requires": ["hyperlinks"],
  "required": false,
  "defaultEnabled": true
}
Feld Bedeutung
name Anzeigename auf der Modul-Seite
description Erklärung auf der Modul-Seite
requires Slugs, die aktiv sein müssen
required nie abschaltbar
defaultEnabled Zustand, wenn nichts gespeichert ist
exclusive Gruppenname; pro Gruppe darf nur ein Modul aktiv sein
tab Das Modul trägt nur einen Reiter und erscheint nicht in der Liste

requires: Solange ein abhängiges Modul an ist, bleibt die Voraussetzung an und ihr Schalter ist gesperrt — mit Begründung in der Oberfläche.

required: Gilt für die Struktur-Reiter general, website, system, utilities und integration, an die andere Module ihre Felder hängen, sowie für layout-blocks und meta-blocks.

exclusive: Derzeit zwei Gruppen — consent für Complianz und Cookiebot, maps für Google Maps und OpenStreetMap. Beim gleichzeitigen Aktivieren gewinnt das neu hinzugekommene Modul.

Abhängigkeiten zwischen Modulen

Hier steckt die häufigste Fehlerquelle. Da Module zur Laufzeit abschaltbar sind, ist ein ungeschützter Aufruf über Modulgrenzen hinweg ein Fatal Error, kein theoretisches Risiko.

Deshalb gilt: Aufrufe über Modulgrenzen nur weich, mit sinnvollem Rückfall.

if (function_exists('twtheme_icons_available') && twtheme_icons_available()) {
    echo twtheme_inline_icon('phone');
}

Harte Voraussetzungen gehören zusätzlich in die module.json, damit die Oberfläche den Schalter sperrt. Beides zusammen: der Filter für die Laufzeit, die module.json für die Bedienbarkeit.

Beispiele im Theme:

  • Search greift auf Loops & Filter nur über twtheme_loops_filter_enqueue_frontend_assets() zu.
  • Die Block-Freigabeliste für den Editor nimmt nur registrierte twtheme/*-Blöcke auf.
  • Für sechs häufig genutzte Funktionen gibt es Stubs in includes/template-fallbacks.php — siehe Template-Funktionen.

Namen ohne Präfix beanspruchen

Ein Modul, das eine Funktion ohne Modulpräfix belegen will — etwa twtheme_map() — darf sie nicht beim Laden deklarieren. Module laden mit der functions.php des Parents, Projektdateien erst bei after_setup_theme. Eine ungeschützte Deklaration wäre dann ein Fatal Error, kein Überschreiben.

Der richtige Weg: die Ausgabe unter eigenem Namen anbieten (twtheme_gmaps_render()) und den historischen Namen erst bei after_setup_theme mit Priorität 99 nachreichen, wenn er noch frei ist.

Der Build-Teil der module.json

Dieselbe Datei trägt die Build-Einträge:

{
  "scss": [
    ["assets/foo.scss", "assets/foo.css"],
    ["assets/bar.scss", "theme:assets/css/bar.css"]
  ],
  "js": [
    { "src": "assets/foo.js", "bundle": true },
    { "src": "source/admin.js", "outfile": "admin.js", "bundle": true }
  ]
}
Regel Bedeutung
Pfad ohne Präfix relativ zum Modulordner
Präfix theme: relativ zur Theme-Wurzel, für Ausgaben außerhalb des Moduls

Gefunden werden die Einträge von scripts/module-discovery.mjs, genutzt von sass-build.mjs und esbuild-frontend-js.mjs. Ein neues Modul mit Assets muss also nirgends in der Build-Konfiguration eingetragen werden.

Fehlende Quelldateien werden übersprungen. Ein entferntes Modul bricht npm run build:css und build:frontend-js nicht.

Feature-CSS außerhalb von main.css

Nicht jedes Stylesheet gehört ins Haupt-Bundle. News, Stock, Mockup, Breadcrumb und Download liegen als source/scss/feature-*.scss und werden bedingt geladen, gesteuert von feature-css.php.

Das Frontend-Bundle des Megamenüs liegt im Modul selbst: megamenu/assets/megamenu-frontend.scss wird zu megamenu-frontend.css und in megamenu.php eingebunden. Hülle und Offcanvas kommen weiter über das style-Feld der block.json als megamenu-shell.css.

Diese Dateien dürfen nicht in source/scss/style.scss importiert werden — sonst landen sie doch im Haupt-Bundle.

Editor-Assets und Frontend-Assets

Die häufigste Ursache dafür, dass Editor-CSS im Frontend landet.

Ziel Feld oder Hook
Nur Editor editorScript, editorStyle (editor.css), enqueue_block_editor_assets
Editor-Iframe enqueue_block_assets nur mit Guard wp_should_load_block_editor_scripts_and_styles()
Frontend style, viewScript, wp_enqueue_scripts, Enqueue in render.php

Verboten:

  • "script" in der block.json — lädt im Editor und im Frontend
  • editor.css oder *-editor.css unter "style"
  • Gemeinsames CSS doppelt in "style" und "editorStyle"; Gemeinsames gehört nur in "style"

Prüfen lässt sich das mit npm run audit:editor-assets — siehe Qualität prüfen.

Versionen laufen automatisch

npm run release:package schreibt Kern-, Icon- und Modulpakete samt Manifest ins Paketverzeichnis. Angehoben wird über Inhalts-Hashes, nicht von Hand:

Was Version Anhebung
Modul module.json → version Patch, sobald sich im Modulordner etwas ändert. Die module.json zählt mit; nur die eingesetzte version selbst bleibt außen vor.
Kern style.css und TWTHEME__VERSION Patch, sobald sich am Kernpaket etwas ändert. Beide Dateien werden geschrieben und wollen committet werden.

Eine von Hand gesetzte Kernversion gewinnt: Steht in style.css bereits mehr als zuletzt ausgeliefert, springt sie nicht ein zweites Mal.

Ausgelöst wird das nach jedem Commit am Theme, über den Hook aus scripts/git-hooks/. Einmalig einhängen:

npm run hooks:install

Der Hook läuft im Hintergrund und protokolliert nach .git/twtheme-release.log. Überspringen mit TWTHEME_SKIP_RELEASE=1 git commit ….

Siehe Release-Pakete.

Prüfen

npm run audit:modules
npm run audit:editor-assets
Befehl Findet
audit:modules tote Build-Quellen, leere Add-ons, fehlende Pfade
audit:editor-assets Editor-Assets, die ins Frontend gelangen, und ungeschützte enqueue_block_assets-Hooks

Weiter