OXID7 Child-Themes auf Basis von APEX-Parent erstellen

Ein Kommentar zum inzwischen sieben Jahre alten Beitrag OXID6 Child-Themes auf Basis von WAVE-Parent erstellen, habe ich zum Anlass genommen das Theme-Thema auf den aktuellen Stand zu bringen. OXID7 hat inzwischen Minor-Version 5 (Stand August 2026) erreicht, also allerhöchste Zeit hier nachzuziehen.

Was hat sich in den sieben Jahren geändert:

  • APEX hat Wave und Flow als Basis in OXID7 abgelöst
  • Das neue Theme basiert grundlegend auf Twig und löst somit die alte Template-Engine Smarty ab
  • Im Basis-Theme steckt Bootstrap 5 (in Wave noch BS4)
  • jQueryUi ist endlich weg
  • Vite als moderne Build-Engine hat Grunt abgelöst
  • Das Theme ist grundsätzlich barrierefrei aufgebaut

Da sich der grundsätzliche Weg der herangehensweise nicht geändert hat, folge ich einfach noch einmal dem Weg aus dem älteren Beitrag. D.h. als erstes das APEX-Theme lokal pullen und analysieren. Die drei wichtigen Dateien sind composer.json, package.json und vite.config.js sowie der Ordner build.

APEX: composer.json

Gegenüber der alten composer.json aus dem wave-theme ist ein require-Package hinzugekommen: die twig-component von OXID. Das ist aber nur Formsache. Die component steckt auch offiziell in dem OXID-metapackage-composer-repo.

APEX: package.json

In der neuen package.json fällt sofort auf, das die Anzahl der devDependencies deutlich geschrumpft sind. Hintergrund ist, das ein großer Teil der einzelnen Grunt-Komponenten logischerweise nicht mehr gebraucht werden. Die Arbeit muss aber dennoch getan werden. Das passiert in den vite-Scripten, dazu weiter unten dann mehr.

APEX: vite.config.js

Diese Datei ist Neuland. Sie wirkt erst einmal kompakter und übersichtlicher als die alte Gruntfile.js. Der Grund liegt im Grundprinzip: Grunt als Taskrunner beschreibt Schritt für Schritt, welche Dateien wohin kopiert, konkateniert, kompiliert und minifiziert werden. Vite ist dagegen ein Bundler. Du gibst nur noch die Entry-Points an, alles Weitere findet Vite über die import-Anweisungen im Code selbst. Die Quelle der Wahrheit wandert also aus der Config in den Quellcode – was vorher eine Liste in einem concat- oder copy-Task war, ist jetzt ein import in der Entry-Datei. So ist jedenfalls die Idee.
Denn beim Lesen kommt einem dann doch alles vertraut vor. Und das ist der eigentlich interessante Punkt an dieser Datei: Fast jede Option darin ist dazu da, einen Vite-Default zurückzudrehen. Das ist gut und schlecht zugleich.
Der viteStaticCopy-Block ist im Kern der alte Grunt-copy-Task. Bootstrap und die beiden Fonts werden weiterhin 1:1 ins out-Verzeichnis kopiert, statt über einen import in den Build zu wandern. Und wenn man einmal genauer hinsieht, zeigt sich auch, was man dabei verliert: Der Copy-Task holt sowohl bootstrap.min.js als auch bootstrap.bundle.min.js ins Theme – eingebunden wird in den Templates aber ausschließlich die Bundle-Variante. Rund 60 KB liegen also ungenutzt im out-Verzeichnis. Ein Modul-Graph hätte das nie durchgelassen, weil dort nur landet, was auch tatsächlich importiert wird. Ähnlich bei den JS-Entries: Die drei Zoom-Skripte auf der Detailseite beginnen alle mit demselben DOMContentLoaded-Gerüst und wiederholen zeichengleiche Berechnungen – ein gemeinsames Helper-Modul wäre der naheliegende Schritt, aber genau dazu lädt eine Architektur aus 16 unabhängigen Entry-Points nicht ein.
Unterm Strich benutzt Apex von Vite im Wesentlichen die Asset-Pipeline: SCSS kompilieren, JS transformieren, minifizieren, an einen festen Ort schreiben. Der Modul-Graph existiert, wird aber kaum genutzt – ganze drei import-Anweisungen finden sich im gesamten build/js-Verzeichnis. Die Asset-Auflösung für Fonts und Bilder, das Aufteilen in gemeinsame Chunks, der Dev-Server mit HMR: all das bleibt außen vor. Das Ergebnis ist erstaunlich nah an dem, was die alte Gruntfile.js produziert hat, nur eben mit einem moderneren, schnelleren und deutlich besser gewarteten Werkzeug darunter. Dass in der package.json noch matchdep steht – ein Paket, dessen einziger Zweck das Nachladen von Grunt-Tasks war – passt ins Bild.
Und trotzdem finde ich das die richtige Entscheidung. Nach so vielen Jahren Grunt geht es beim Technologiewechsel erst einmal darum, das Werkzeug zu tauschen, ohne das Ergebnis zu verändern – die Templates, die Pfade und die Erwartungen der Child-Themes bleiben, wie sie sind. Der Schritt darüber hinaus wäre ein zweiter, und der geht nur zusammen mit den Templates: echte Imports statt Copy-Tasks, Fonts und Bilder über den Modul-Graphen auflösen lassen, gemeinsamen Code in Chunks auslagern und über die manifest.json einbinden. Wo diese Grenze genau liegt, sieht man an einer Kleinigkeit: Mehrere Templates erzeugen Inline-JavaScript mit new bootstrap.Modal(…). Solange Twig JavaScript ausspuckt, muss bootstrap global erreichbar sein – ein ES-Modul kann es nicht sein. Der Modul-Graph endet also dort, wo PHP anfängt, JavaScript zu schreiben. Und genau diese Grenze zu verschieben ist einer der interessantesten Freiheitsgrade, die ein eigenes Child-Theme mitbringt. Wir bleiben in diesem Beitrag bei der konservativen Variante – aber es lohnt sich, im Kopf zu behalten, dass hier noch Luft ist.

Das eigene Child-Theme

Auch hier ist wieder die Basis ein eigenes Repo für das zukünftige Child-Theme.

Aus dem APEX-Theme kopiere ich:

  • build-Ordner
  • composer.json
  • package.json
  • vite.config.js
  • theme.php

CHILD: composer.json

In der composer.json name, description und assets-directory anpassen. Bei der Gelegenheit aus dem blacklist-filter das Grunt-Artefakt löschen.

CHILD: package.json

Auch hier können name und description angepasst werden. Desweiteren entfernt Ihr das Grunt-Artefakt „matchdep“. Im aktuellen b-7.5-Branch des Themes, steckt vite in Version 5 drin. Das kann problemlos im eigenen theme auch auf die aktuelle Version 7 hochgezogen werden.
Dann erfolgt die erste Installation mit: npm install

CHILD: vite.config.js

Sucht einmal nach „apex„. Ihr werdet das in der outDir Zeile finden. Passt diese an, danach erfolgt der erste Probebau mit npm run build

CHILD: theme.php

Das Config-Array in der theme.php bekommt erst einmal grundlegend ein paar neue Keys und Values:

$aTheme = [
    'id'            => 'fancychild',
    'title'         => 'Fancy Child',
    'description'   => 'Child-Theme based on APEX - Bootstrap 5 TWIG Theme',
    'thumbnail'     => 'fancychild.svg',
    'version'       => '1.0.0',
    'author'        => 'the fantastic four',
    'parentTheme'   => 'apex'
    'parentVersion' => '3.1.0'
];

Auch in OXID7 gilt noch die Regel, das ein Child-Theme die Einstellungen des Parent-Themes erben !kann! Darum entfernen wir im Child-Theme erst einmal den settings-Bereich.

Jetzt ist das Theme für die erste Aktivierung bereit. D.h. commited und pushed hier einmal und bindet das Theme in die root-composer.json Eures Shops ein. Nach der Installation via composer müsst Ihr das Theme noch aktivieren. Das geht entweder über den „alten“ Weg im OXID-Admin im Bereich Themes oder über den OXID-Console:

vendor/bin/oe-console o:t:a fancychild

In den Theme-Einstellungen des neuen Child-Themes sieht man erst einmal keine Einstellungen. Die werden vom Parent geerbt. D.h. sie werden im Moment auch im Parent-Theme (Apex) gepflegt. Wer das gern ändern möchte, wirft einen Blick in die Datenbank in die Tabellen oxconfig und oxconfigdisplay. In beiden Tabellen findet man die typischen Configs des themes zu erkennen an den Einträgen „theme:apex“ in der Spalte oxmodule bzw. oxcfgmodule. Die müssen einfach für das Child-Theme zur Verfügung gestellt werden. Hilfreich ist folgendes SQL:

SET @parent := 'theme:apex';
SET @child  := 'theme:fancychild';
SET @shopId := 1;

-- values
INSERT INTO oxconfig (OXID, OXSHOPID, OXMODULE, OXVARNAME, OXVARTYPE, OXVARVALUE)
SELECT
    MD5(CONCAT(@child, '#', p.OXSHOPID, '#', p.OXVARNAME)),
    p.OXSHOPID,
    @child,
    p.OXVARNAME,
    p.OXVARTYPE,
    p.OXVARVALUE
FROM oxconfig p
LEFT JOIN oxconfig ex
       ON ex.OXMODULE  = @child
      AND ex.OXSHOPID  = p.OXSHOPID
      AND ex.OXVARNAME = p.OXVARNAME
WHERE p.OXMODULE = @parent
  AND p.OXSHOPID = @shopId
  AND ex.OXID IS NULL;

-- display/grouping
INSERT INTO oxconfigdisplay (OXID, OXCFGMODULE, OXCFGVARNAME, OXGROUPING, OXVARCONSTRAINT, OXPOS)
SELECT
    MD5(CONCAT(@child, '#', p.OXCFGVARNAME)),
    @child,
    p.OXCFGVARNAME,
    p.OXGROUPING,
    p.OXVARCONSTRAINT,
    p.OXPOS
FROM oxconfigdisplay p
LEFT JOIN oxconfigdisplay ex
       ON ex.OXCFGMODULE  = @child
      AND ex.OXCFGVARNAME = p.OXCFGVARNAME
WHERE p.OXCFGMODULE = @parent
  AND ex.OXID IS NULL;

Anschließend hat auch Dein Child eigene vom Parent unabhängige Config-Einträge im Admin. Etwas Gutes kommt in OXID7.6: Dann ist das Thema Config in der Datenbank auch erledigt. Nach den Modulen werden auch die Theme-Optionen in yaml-Files unter var/configuation abgelegt.

CHILD: Templates

Auch hier gilt wie in OXID6 das Vererbungsprinzip. D.h. angenommen Ihr wollt ein Template des Parents in Eurem Child-Theme anpassen, dann kopiert Euch die Datei und packt sie exakt in die gleiche Ordner-Struktur in Eurer Child-Theme wie im Parent. Damit gewinnt Euer Template.

Einige Dinge funktionieren in OXID7 jetzt anders. In OXID6 konnte man z.B. ein Template eines Moduls einfach kopieren und in die gleiche Ordnerstruktur die im Modul definiert war, einfach im Child-Theme ablegen und anpassen. Das Child-Theme-Template hat so in der Hierachie gewonnen. Das geht nun nicht mehr. Jetzt müssen Anpassungen an Modul-Templates auch durch Template-Extensions in Modulen erledigt werden. Übrigens können auch Theme-Template-Anpassungen durch Module erfolgen. Beide Techniken sind identisch und ausführlich hier, in dem etwas versteckten Tutorial von OXID erklärt.

CHILD: Sprachdateien

Bei den Sprachdateien hat sich an der Funktionsweise nichts geändert. Auch hier gilt, die Sprachdateien im Child-Theme haben das letzte Wort und können sämtliche Translations aus dem Parent-Theme, dem Shop und der Module überschreiben. Legt also wie gewohnt für jede Eurer Sprachen ein Verzeichnis an (z.B. de, en) und platziert dort eine *lang.php Datei. Schaut Euch die lang.php-Files aus dem parent an und stellt nach dem Muster ein $aLang-Array in Euren Files bereit.

CHILD: Dokumentation

Zum guten Ton gehört es immer noch, das Ihr zwei Dateien in Euer Repo packt in dem Ihr das Theme und Eure Änderungen beschreibt:

  • README.md
  • CHANGELOG.md

Und nun Happy THEMEing …