Skip to content

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

Ordnername = 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
<?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
<?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

MethodeSignaturZweck
getVersion()public static function getVersion(): stringSemver des Widgets. Wird derzeit nur informativ genutzt
getName()public static function getName(): stringAnzeigename. 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:

php
// {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.vue

Bei 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

json
{
  "widget_title":    "Picker-Label",
  "widget_icon":     "icon.png",
  "widget_subtitle": "Kurzbeschreibung im Picker",
  "widget_template": "exactFolderName",
  "widget_menu": [ ... Feldtypen ... ]
}
FeldPflichtZweck
widget_titlejaHeader im Picker + in der StructureTree
widget_iconjaDateiname relativ zum Widget-Ordner — konventionell icon.png
widget_subtitleneinZusatz-Zeile im Picker
widget_templatejaMuss exakt dem Widget-Ordnernamen entsprechen
widget_menujaArray 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:

ts
interface WidgetProp {
  data?: Record<string, any>     // Formular-Werte (title, subtitle, image, …)
  content?: any                  // Rueckgabe von getWidgetContent()
  // weitere Felder: id, type, visibility, spacing, ...
}

Minimalbeispiel:

vue
<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):

vue
<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.vue

Beim 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