Widget-Anatomie
Diese Seite beschreibt den Aufbau eines Pagebuilder-Widgets im Detail: Ordner-Struktur, PHP-Klasse, Pflicht-Methoden und die Interaktion mit Plugin-bootstrap und Pagebuilder-Runtime.
Ordner-Struktur
Ein Widget lebt immer unter widgets/ seines Plugins:
_public/extensions/core/backend/{plugin}/
├── bootstrap.php # Plugin-bootstrap (registriert Widgets via $this->pagebuilder_widgets)
└── widgets/
└── {widget_template}/
├── bootstrap.php # Widget-Klasse {widget_template}_PageBuilderPlugin
├── icon.png # Widget-Icon fuer den Picker (64x64 oder 128x128 PNG empfohlen)
└── template/
└── index.vue # Frontend Vue-KomponenteOrdnername = widget_template
Der Name des Widget-Unterordners muss exakt dem Wert widget_template aus dem JSON in $this->pagebuilder_widgets entsprechen. Ebenso der Klassenname ({widget_template}_PageBuilderPlugin). Abweichungen: Widget laedt nicht, Picker zeigt es nicht an.
Die _PageBuilderPlugin-Klasse
Anders als Admin-Plugins erbt die Widget-Klasse nichts — sie hat nur drei statische Methoden:
<?php
// _public/extensions/core/backend/{plugin}/widgets/{widget_template}/bootstrap.php
class myWidget_PageBuilderPlugin
{
public static function getVersion(): string
{
return "1.0.0";
}
public static function getName(): string
{
return "My Widget";
}
public static function getWidgetContent($widget_array, $backend = false)
{
// null → kein Daten-Load (rein Formular-basiertes Widget)
return null;
}
}Echtes Minimal-Beispiel aus widgetsBase/widgets/image/bootstrap.php:
<?php
class image_PageBuilderPlugin
{
public static function getVersion()
{
return "1.0.0";
}
public static function getName()
{
return "Image";
}
public static function getWidgetContent($widget_array, $backend = false)
{
return null;
}
}Keine Konstanten, kein Konstruktor, kein extends. Nur drei statische Methoden.
Pflicht-Methoden
| Methode | Signatur | Zweck |
|---|---|---|
getVersion() | public static function getVersion(): string | Semver des Widgets. Wird derzeit nur informativ genutzt |
getName() | public static function getName(): string | Anzeigename. Kann vom widget_title im Picker abweichen |
getWidgetContent($widget_array, $backend) | public static function getWidgetContent(array $widget_array, bool $backend = false) | Liefert dynamische Daten oder null. Wird von PageService bei jedem Rendering aufgerufen |
Details zur getWidgetContent()-Signatur und den uebergebenen Daten: getWidgetContent().
Zusammenspiel Plugin ↔ Widget
Die Registrierung lebt im Plugin, nicht im Widget-Ordner:
// {plugin}/bootstrap.php — install()-Methode
$this->pagebuilder_widgets = [
'{
"widget_title": "My Widget",
"widget_icon": "icon.png",
"widget_subtitle": "Short description for the picker",
"widget_template": "myWidget",
"widget_menu": [
{"row": "title", "type": "text", "placeholder": "Title"}
]
}'
];Und separat die Widget-Klasse:
{plugin}/widgets/myWidget/bootstrap.php # class myWidget_PageBuilderPlugin
{plugin}/widgets/myWidget/icon.png
{plugin}/widgets/myWidget/template/index.vueBei installPlugin() liest der Core das JSON aus $this->pagebuilder_widgets, legt einen Eintrag pro Widget in page_widgets an und ruft installWidgets($pluginID) auf. Das Widget steht dann im Picker zur Verfuegung.
Widget-JSON — Pflicht- und Optional-Felder
{
"widget_title": "Picker-Label",
"widget_icon": "icon.png",
"widget_subtitle": "Kurzbeschreibung im Picker",
"widget_template": "exactFolderName",
"widget_menu": [ ... Feldtypen ... ]
}| Feld | Pflicht | Zweck |
|---|---|---|
widget_title | ja | Header im Picker + in der StructureTree |
widget_icon | ja | Dateiname relativ zum Widget-Ordner — konventionell icon.png |
widget_subtitle | nein | Zusatz-Zeile im Picker |
widget_template | ja | Muss exakt dem Widget-Ordnernamen entsprechen |
widget_menu | ja | Array von Feldtypen — bestimmt die Eingabemaske im Pagebuilder |
Details zu widget_menu und den Feldtypen: Widget-Menu-Feldtypen.
Die Vue-Komponente template/index.vue
Die Komponente bekommt ein einziges Prop widget: Object. Der Shape ist:
interface WidgetProp {
data?: Record<string, any> // Formular-Werte (title, subtitle, image, …)
content?: any // Rueckgabe von getWidgetContent()
// weitere Felder: id, type, visibility, spacing, ...
}Minimalbeispiel:
<template>
<div class="my-widget">
<h2>{{ widget.data?.title }}</h2>
<div v-html="widget.data?.text"></div>
</div>
</template>
<script setup lang="ts">
defineProps<{ widget: { data?: Record<string, any>; content?: any } }>()
</script>Realbeispiel aus shop/widgets/categoryBannerNoLink/template/index.vue (gekuerzt):
<template>
<div class="categorySlider">
<Swiper :modules="modules" :pagination="{ clickable: true }">
<SwiperSlide v-for="item in widget.content?.items" :key="item.url_rewrite">
<NuxtLink :to="`/${currentView}/${item.url_rewrite}`">
<NuxtImg :src="item.image" :alt="item.title" format="avif" />
<h4>{{ item.title }}</h4>
</NuxtLink>
</SwiperSlide>
</Swiper>
</div>
</template>widget.data.* sind Formular-Werte, widget.content.* kommt aus getWidgetContent().
Wie die Vue-Komponente geladen wird
PagebuilderWidget.vue nutzt eine automatisch generierte Widget-Registry:
1. vueRegistry.php (Cache-Rebuild) scannt alle widgets/*/template/index.vue
2. Generiert plugin-registry.js pro Theme mit Mapping { widget_template → Vue-Komponente }
3. Runtime: <component :is="widgetRegistry[widget.type]" :widget="widget" />Das bedeutet: neues Widget hinzufuegen → Cache-Rebuild erforderlich (unter /admin/cache). Bestehende template/index.vue-Aenderungen brauchen keinen Rebuild, weil Vue/Vite live reloaded, aber das Registrieren neuer Widget-Ordner tut es.
Custom-Theme-Overrides
Ein Theme kann jedes Widget-Template uebersteuern, ohne das Plugin zu forken:
_theme/{theme}/app/custom/{plugin}/widgets/{widget_template}/template/index.vueBeim Registry-Rebuild wird diese Datei vorgezogen, wenn sie existiert. Mehr dazu in Theme-Doku › Struktur.
Haeufige Fehler
Ordnername ≠ widget_template
Das Widget liegt unter widgets/MyWidget/, aber das JSON sagt "widget_template": "myWidget" — der Registry-Scan findet die Komponente nicht. Beide Namen exakt abgleichen (Gross-/Kleinschreibung zaehlt).
Klassenname passt nicht zum Ordner
Die Klasse in widgets/myWidget/bootstrap.php muss myWidget_PageBuilderPlugin heissen — nicht MyWidget_PageBuilderPlugin, nicht myWidgetPlugin. Bei Abweichung wirft getWidgetContent() Class not found.
Icon-Datei fehlt
widget_icon: "icon.png" erwartet eine icon.png im Widget-Root. Fehlt die Datei, zeigt der Picker ein Broken-Image. Platzhalter: 64x64 oder 128x128 PNG.
Neues Widget nicht im Picker
Nach Hinzufuegen eines neuen Widgets ist ein /admin/cache-Rebuild Pflicht. Nur dann wird plugin-registry.js neu generiert. Bestehende Widgets updaten braucht den Rebuild nicht.
getWidgetContent() soll dynamische Daten liefern — return type
Rueckgabe null → kein content verfuegbar. Rueckgabe-Array → steht im Vue-Template unter widget.content. Die Methode muss statisch sein und $widget_array, $backend = false als Signatur haben — sonst kommt der Core-Aufruf nicht an.
Widget im falschen Plugin-Verzeichnis
Ein Widget muss im Plugin liegen, das es auch in $this->pagebuilder_widgets registriert. Legt man es in einen fremden Plugin-Ordner, findet der Loader die Klasse nicht.
Siehe auch
- Widgets-Uebersicht — Konzept und Lebenszyklus
- Widget-Menu-Feldtypen — text/textarea/select/filemanager/items
- getWidgetContent() — dynamische Daten in PHP
- Custom-CSS-Konvention —
.widget_{id}-Wrappers - Beispiel: Testimonial-Widget — komplettes Widget von null
- Plugin-Anatomie —
$this->pagebuilder_widgetsin Plugin-install()