Bludit 3.22.x Dashboard anpassen: Drag & Drop Widgets wie bei WordPress
Warum dashboard() in Bludit 3.22.x nicht mehr reicht und wie ein updatefestes Plugin mit Drag & Drop, Widget-Zonen, CSRF und Flat-File-Layouts entsteht.
Das Dashboard ist ein gutes Beispiel dafür, warum man bei Bludit-Versionen nicht nur auf vorhandene Hook-Namen schauen darf. Ein älteres Plugin kann eine Methode dashboard() besitzen und trotzdem unter Bludit 3.22.x nichts anzeigen. In diesem Tutorial bauen wir deshalb ein vollständiges, WordPress-artiges Dashboard-Plugin mit Drag & Drop, mehreren Widget-Zonen, ein- und ausblendbaren Karten und benutzerspezifischer Flat-File-Speicherung.
Das fertige Beispiel heißt MD Dashboard Layout. Es verändert keine Core-Datei. Der komplette Quellcode liegt im Projekt unter bl-plugins/md-dashboard-layout/ und zusätzlich als installierbares ZIP vor: MD Dashboard Layout 1.0.0 herunterladen.
Warum alte Dashboard-Tutorials bei Bludit 3.22.x nicht mehr reichen
In Bludit 3.16.1 war das Dashboard noch relativ kompakt aufgebaut. Nach Begrüßung und Schnellsuche stand im linken Dashboard-Bereich ausdrücklich Theme::plugins('dashboard'). Damit war die klassische Erweiterung einfach: Ein Plugin implementierte public function dashboard() und Bludit gab den Rückgabewert an dieser Stelle aus.
In Bludit 3.22.0 wurde die Dashboard-View deutlich erweitert. Sie enthält heute eine eigene Quick-Search-Oberfläche, Metrik-Karten für Inhalte, Kategorien und Tags, optionale Analytics sowie Benachrichtigungen. Entscheidend ist aber: Der frühere Aufruf Theme::plugins('dashboard') ist in dieser View nicht mehr vorhanden. Gleichzeitig registriert bl-kernel/boot/rules/60.plugins.php den Hook-Namen dashboard weiterhin. Dadurch kann ein Plugin korrekt geladen und sogar dem Hook-Array zugeordnet werden, ohne dass seine Methode im Dashboard jemals aufgerufen wird.
| Stand | Dashboard-Anbindung | Folge |
|---|---|---|
| Bludit 3.16.1 | dashboard() wird in der Dashboard-View aufgerufen | Plugin kann direkt HTML in den Dashboard-Bereich ausgeben |
| Bludit 3.22.0 | Hook ist noch registriert, View ruft ihn aber nicht auf | dashboard() allein bleibt wirkungslos |
| Unser Plugin | dashboard() + adminBodyEnd()-Fallback | läuft auf älteren 3.x-Versionen und auf 3.22.x ohne Core-Patch |
Die wichtige Lektion lautet: Ein Hook-Name in der Plugin-Registry garantiert noch nicht, dass die aktuelle View diesen Hook auch tatsächlich ausführt.
Warum wir nicht einfach dashboard.php patchen
- Ein Core-Patch an
bl-kernel/admin/views/dashboard.phpwäre beim nächsten Bludit-Update gefährdet. - Man vermischt eigenes Projektverhalten mit dem CMS-Core und erschwert spätere Vergleiche.
- Andere Admin-Themes könnten eine eigene Dashboard-Struktur besitzen.
- Ein Plugin lässt sich deaktivieren, aktualisieren, sichern und separat verteilen.
- Das Tutorial zeigt damit eine Technik, die sich auch auf andere Admin-Anpassungen übertragen lässt.
Ziel des Beispielplugins
- WordPress-artige Dashboard-Karten, die per Drag & Drop verschoben werden können.
- Drei Drop-Zonen: Hauptbereich, Seitenbereich und ein breiter Bereich darunter.
- Widgets können über „Ansicht“ ein- und ausgeblendet werden.
- Die Anordnung wird pro Bludit-Benutzer gespeichert.
- Die Speicherung bleibt Flat-File: eine JSON-Datei pro Benutzer im Plugin-Workspace.
- CSRF-geschützte POST-Anfragen speichern oder setzen das Layout zurück.
- Ältere Bludit-3.x-Versionen dürfen weiterhin den klassischen
dashboard()-Hook nutzen. - Bludit 3.22.x erhält über
adminBodyEnd()einen Fallback, ohne eine einzige Core-Datei zu ändern.
Dateistruktur des Plugins
bl-plugins/md-dashboard-layout/
├── plugin.php
├── metadata.json
├── languages/
│ ├── de_DE.json
│ ├── de.json
│ └── en.json
├── css/
│ └── dashboard.css
└── js/
└── dashboard.js
Nach der Aktivierung legt die Basisklasse außerdem den üblichen Plugin-Datenbankordner und Workspace an. Unsere persönlichen Dashboard-Layouts landen später beispielsweise unter bl-content/workspaces/md-dashboard-layout/layouts/admin.json.
Schritt 1: metadata.json
{
"author": "MD Bludit Praxis",
"version": "1.0.0",
"releaseDate": "2026-09-18",
"license": "MIT",
"compatible": "3.22",
"notes": "Drag-and-Drop-Dashboard für Bludit 3.22.x"
}
Die compatible-Angabe sagt Bludit, für welchen Versionszweig das Paket gedacht ist. Sie ersetzt aber keinen echten Test. Gerade unser Dashboard-Beispiel zeigt, dass sich innerhalb derselben Major-Version konkrete Views und Hook-Aufrufe ändern können.
Schritt 2: Plugin-Grundgerüst und Einstellungen
class pluginMDDashboardLayout extends Plugin
{
private $rootRendered = false;
public function init()
{
$this->dbFields = array(
'replaceCoreDashboard' => true,
'showDropHints' => true
);
}
}
Mit dbFields speichern wir nur globale Plugin-Einstellungen: Soll das Standard-Dashboard optisch ersetzt werden und sollen leere Drop-Zonen Hinweise anzeigen? Die persönliche Reihenfolge gehört bewusst nicht hier hinein, weil mehrere Benutzer unterschiedliche Dashboards haben dürfen.
Schritt 3: Nur auf dem Dashboard laden
private function onDashboard()
{
return isset($GLOBALS['ADMIN_CONTROLLER'])
&& $GLOBALS['ADMIN_CONTROLLER'] === 'dashboard';
}
Admin-Hooks wie adminHead() und adminBodyEnd() laufen grundsätzlich auf vielen Adminseiten. Würden wir dort immer CSS, JavaScript und Dashboard-HTML laden, hätten wir unnötigen Ballast auf „Inhalte“, „Themes“, „Benutzer“ usw. Deshalb prüfen wir den aktiven Controller.
Schritt 4: Rückwärtskompatibel dashboard() anbieten
public function dashboard()
{
if (!$this->onDashboard()) {
return '';
}
$this->rootRendered = true;
return $this->renderDashboardRoot();
}
Unter älteren Bludit-3.x-Versionen wird diese Methode über Theme::plugins('dashboard') ausgegeben. Unter 3.22.0 bleibt sie ungerufen. Trotzdem behalten wir sie: Das Plugin dokumentiert damit den alten offiziellen Weg und bleibt zugleich kompatibler.
Schritt 5: Bludit 3.22.x über adminBodyEnd() bedienen
public function adminBodyEnd()
{
if (!$this->onDashboard()) {
return '';
}
global $security;
$config = array(
'saveUrl' => HTML_PATH_ADMIN_ROOT.'plugin/'.Text::lowercase(__CLASS__).'/save-layout',
'resetUrl' => HTML_PATH_ADMIN_ROOT.'plugin/'.Text::lowercase(__CLASS__).'/reset-layout',
'tokenCSRF' => $security->getTokenCSRF(),
'layout' => $this->readLayout()
);
$out = '';
if (!$this->rootRendered) {
$out .= $this->renderDashboardRoot();
$this->rootRendered = true;
}
$out .= '<script>window.MDDashboardLayoutConfig='.
json_encode($config, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE).
';</script>';
$out .= $this->includeJS('dashboard.js');
return $out;
}
adminBodyEnd() wird weiterhin vom Bludit-Admin-Theme ausgeführt. Das Plugin hängt dort sein Dashboard-HTML und die Konfiguration an. Das JavaScript verschiebt den erzeugten Root anschließend in #dashboard und ersetzt dort – wenn konfiguriert – die Standarddarstellung.
Wichtig ist $rootRendered: Falls eine ältere Bludit-Version vorher schon dashboard() ausgeführt hat, erzeugen wir den Root kein zweites Mal.
Schritt 6: CSS nur im Dashboard laden
public function adminHead()
{
if (!$this->onDashboard()) {
return '';
}
return $this->includeCSS('dashboard.css');
}
Die Plugin-Basisklasse erzeugt mit includeCSS() eine korrekte URL zum eigenen Pluginordner. Dadurch gibt es keine hart codierten Domains oder Unterverzeichnis-Probleme.
Schritt 7: Die drei Widget-Zonen erzeugen
<div class="row md-dash-grid">
<div class="col-lg-8">
<div class="md-dash-zone" data-zone="main"></div>
</div>
<div class="col-lg-4">
<div class="md-dash-zone" data-zone="side"></div>
</div>
<div class="col-12">
<div class="md-dash-zone" data-zone="bottom"></div>
</div>
</div>
Die Zonen selbst kennen noch keine Reihenfolge. Jedes Widget trägt nur eine stabile Kennung wie content-stats, recent-content oder system-info. Erst die gespeicherte Layout-JSON entscheidet, in welcher Zone und an welcher Position eine Karte landet.
Schritt 8: Welche Widgets das Beispiel mitbringt
| Widget | Inhalt |
|---|---|
quick-actions | Neuer Inhalt, Inhalte, Kategorien, Plugins, Themes, Benutzer, Website |
content-stats | veröffentlichte Inhalte, Entwürfe, geplante und statische Seiten |
recent-content | die zuletzt geänderten veröffentlichten Inhalte |
categories | Kategorien und Anzahl zugeordneter Seiten |
tags | Tag-Cloud mit Anzahl |
notifications | die letzten Syslog-/Dashboard-Benachrichtigungen |
system-info | Bludit-Version, PHP, Theme, aktive Plugins, Benutzerrolle |
gutenberg-status | Status und Einstellungen unseres MD-Gutenberg-Plugins |
Damit ist das Beispiel mehr als eine leere Drag-and-Drop-Demo. Es zeigt gleichzeitig, wie man typische Bludit-Objekte wie $pages, $categories, $tags, $syslog und $site aus einem Admin-Plugin heraus verwendet.
Schritt 9: Ein Widget als wiederverwendbare Karte rendern
private function widget($id, $title, $icon, $body)
{
return '<section class="md-dash-widget card" data-widget-id="'.
$this->esc($id).'">'
.$this->widgetHeader($id, $title, $icon)
.'<div class="md-dash-widget-body">'.$body.'</div>'
.'</section>';
}
Der entscheidende Teil ist data-widget-id. Nicht der sichtbare Titel wird gespeichert, sondern eine stabile technische ID. Du kannst einen Widget-Titel später übersetzen oder umbenennen, ohne das vorhandene Layout zu zerstören.
Schritt 10: Inhalte aus Bludit lesen
$published = count($pages->getPublishedDB());
$drafts = count($pages->getDraftDB());
$scheduled = count($pages->getScheduledDB());
$static = count($pages->getStaticDB());
Für das Statistik-Widget brauchen wir keine SQL-Abfrage. Pages arbeitet mit Bludits Flat-File-Datenbank und liefert die passenden Teilmengen. Dasselbe Prinzip gilt für Kategorien und Tags.
$categoryKeys = $categories->keys();
foreach ($categoryKeys as $key) {
$category = new Category($key);
$name = $category->name();
$count = count($category->pages());
}
Schritt 11: Rollen berücksichtigen
Ein Dashboard-Plugin darf nicht davon ausgehen, dass jeder eingeloggte Benutzer Administrator ist. Unser Schnellzugriff zeigt „Plugins“, „Themes“ und „Benutzer“ nur für die Rolle admin. Autoren und Editoren behalten dagegen Inhaltsschaltflächen. Das ist zusätzlich zur serverseitigen Rechteprüfung wichtig: Eine ausgeblendete Schaltfläche ist nur UX – echte Sicherheit muss immer im Controller erfolgen.
Schritt 12: Persönliches Layout als JSON speichern
private function defaultLayout()
{
return array(
'zones' => array(
'main' => array('quick-actions', 'recent-content', 'notifications'),
'side' => array('content-stats', 'categories', 'system-info'),
'bottom' => array('tags', 'gutenberg-status')
),
'hidden' => array()
);
}
Für jeden Benutzer entsteht eine eigene Datei. Beispiel:
{
"zones": {
"main": ["quick-actions", "recent-content", "notifications"],
"side": ["content-stats", "categories", "system-info"],
"bottom": ["tags", "gutenberg-status"]
},
"hidden": []
}
Die Datei liegt im Plugin-Workspace, nicht im Core. Die Speicherung verwendet LOCK_EX, damit parallele Schreibvorgänge nicht unnötig kollidieren.
Warum wir nicht nur localStorage benutzen
localStoragewäre browsergebunden. Ein anderer Browser oder Rechner hätte wieder das Standardlayout.- Das serverseitige JSON gehört zur Bludit-Installation und kann mitgesichert werden.
- Mehrere Benutzer können unterschiedliche Layouts besitzen.
- Ein Zurücksetzen ist serverseitig eindeutig: Layoutdatei löschen und Default verwenden.
Schritt 13: Gespeicherte Layouts normalisieren
Ein oft übersehener Punkt: Was passiert, wenn Version 1.1 später ein neues Widget ergänzt? Würden wir eine alte JSON-Datei blind übernehmen, könnte das neue Widget für immer verschwinden. Deshalb validiert normaliseLayout() jede ID gegen eine Allowlist, entfernt Duplikate und hängt neue, noch unbekannte Widgets automatisch im Hauptbereich an.
Schritt 14: Drag & Drop im Browser
header.addEventListener('dragstart', function (event) {
dragged = widget;
widget.classList.add('is-dragging');
event.dataTransfer.effectAllowed = 'move';
});
zone.addEventListener('dragover', function (event) {
event.preventDefault();
if (!dragged) return;
var after = findInsertionTarget(zone, event.clientY);
if (after) zone.insertBefore(dragged, after);
else zone.appendChild(dragged);
});
zone.addEventListener('drop', function (event) {
event.preventDefault();
if (dragged) {
zone.appendChild(dragged);
saveLayout();
}
});
Wir verwenden hier die native HTML5-Drag-and-Drop-API und brauchen deshalb kein npm-Paket und kein CDN. Die Widget-Kopfzeile ist der Drag-Handle. Beim Überfahren einer Zone wird die Karte bereits an die passende Zwischenposition verschoben. Nach drop wird die aktuelle DOM-Reihenfolge gespeichert.
Schritt 15: „Ansicht“ wie WordPress
Oben rechts besitzt das Beispiel einen Button Ansicht. Er öffnet eine Liste aller Widgets mit Checkboxen. Wird eine Checkbox deaktiviert, bleibt das Widget technisch in seiner Zone, erhält aber hidden. Die ID wird zusätzlich im Array hidden gespeichert. Dadurch kann der Benutzer ein Widget jederzeit wieder einblenden, ohne seine vorherige Position zu verlieren.
Schritt 16: Layout per Fetch speichern
var data = new URLSearchParams();
data.set('tokenCSRF', cfg.tokenCSRF);
data.set('layout', JSON.stringify(collectLayout()));
fetch(cfg.saveUrl, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
body: data.toString()
});
Der CSRF-Token stammt aus Bludits $security->getTokenCSRF(). Er wird nicht erfunden und nicht dauerhaft in einer JavaScript-Datei hart codiert. Bludits Admin-Bootstrap prüft POST-Anfragen bereits über seine Security-Regel, bevor der Plugin-Controller ausgeführt wird.
Schritt 17: Eigene Admin-Route als JSON-Endpunkt
public function adminController()
{
global $layout;
checkRole(array('admin', 'editor', 'author'));
$action = trim((string)$layout['parameters'], '/');
if ($_SERVER['REQUEST_METHOD'] === 'POST'
&& $action === 'save-layout') {
$raw = isset($_POST['layout'])
? html_entity_decode((string)$_POST['layout'], ENT_QUOTES, 'UTF-8')
: '';
$decoded = json_decode($raw, true);
$ok = $this->writeLayout($decoded);
header('Content-Type: application/json; charset=UTF-8');
echo json_encode(array('ok' => $ok));
exit;
}
}
Die Route sieht beispielsweise so aus: /admin/plugin/pluginmddashboardlayout/save-layout. Weil sie innerhalb des Bludit-Admin-Bootstraps läuft, profitieren wir von Loginprüfung und CSRF-Verarbeitung. Zusätzlich prüft das Plugin erlaubte Rollen.
Schritt 18: Warum JSON vom Browser nie blind gespeichert wird
- Nur bekannte Zonen
main,sideundbottomwerden akzeptiert. - Nur IDs aus der internen Widget-Allowlist dürfen in die Datei.
- Doppelte IDs werden entfernt.
- Unbekannte Widgets aus manipulierten Requests werden verworfen.
- Neue Widgets einer späteren Pluginversion werden automatisch wieder ergänzt.
Schritt 19: Das Standard-Dashboard in 3.22.x ersetzen
var root = document.getElementById('md-dashboard-layout-root');
var core = document.getElementById('dashboard');
if (cfg.replaceCoreDashboard) {
root.parentNode.removeChild(root);
core.innerHTML = '';
core.appendChild(root);
}
Genau dieser Schritt macht das Plugin ohne Core-Patch möglich. Bludit erzeugt zuerst sein normales Dashboard. Unser Plugin hängt seinen Root über adminBodyEnd() an. Danach wird der eigene Root in den vorhandenen #dashboard-Container verschoben. Die URL, der Controller und die restliche Admin-Navigation bleiben Bludit.
Schritt 20: Eigene Plugin-Seite und Sidebar-Link
public function adminSidebar()
{
return '<a class="nav-link" href="'.
HTML_PATH_ADMIN_ROOT.'plugin/'.Text::lowercase(__CLASS__).
'">Dashboard Layout</a>';
}
Die zusätzliche Seite zeigt die aktuell gespeicherte JSON-Struktur und verlinkt zurück zum Dashboard sowie zur normalen Plugin-Konfiguration. Das ist für Debugging nützlich, weil man sofort erkennt, ob ein Problem in der Speicherung oder nur in der Darstellung liegt.
Schritt 21: Ein eigenes Widget hinzufügen
Angenommen, du möchtest ein Widget „Offene Aufgaben“. Dann sind vier Stellen relevant:
- Neue ID, zum Beispiel
open-tasks, in die interne Widget-Allowlist aufnehmen. - Einen Titel für die „Ansicht“-Checkbox ergänzen.
- In
buildWidgets()den HTML-Inhalt erzeugen. - Die ID im
defaultLayout()an der gewünschten Standardposition eintragen.
$body = '<ul class="mb-0">'
.'<li>Backup prüfen</li>'
.'<li>Entwürfe veröffentlichen</li>'
.'</ul>';
$widgets['open-tasks'] = $this->widget(
'open-tasks',
'Offene Aufgaben',
'check-square-o',
$body
);
Weil normaliseLayout() neue IDs automatisch erkennt, taucht ein später hinzugefügtes Widget auch bei Benutzern auf, die schon eine ältere Layoutdatei besitzen.
Schritt 22: Plugin installieren
Du kannst das ZIP klassisch entpacken und den Ordner nach bl-plugins/ kopieren. In diesem Handbuchprojekt kannst du alternativ den bereits gebauten MD Plugin Uploader verwenden: ZIP hochladen, Paket prüfen lassen und anschließend unter „Plugins“ aktivieren.
Schritt 23: Testplan nach der Aktivierung
- Dashboard öffnen und kontrollieren, ob „Mein Dashboard“ erscheint.
- Ein Widget von Hauptbereich nach Seitenbereich ziehen.
- Seite neu laden: Position muss erhalten bleiben.
- Unter „Ansicht“ ein Widget ausblenden und neu laden.
- Layout zurücksetzen: Standardreihenfolge muss wieder erscheinen.
- Mit einem zweiten Benutzer anmelden: dessen Layout muss unabhängig sein.
- Als Autor/Editor prüfen, dass keine unzulässigen Plugin-/Theme-/Benutzeraktionen im Schnellzugriff erscheinen.
- Plugin deaktivieren: Das normale Bludit-Dashboard muss ohne Dateireparatur wieder da sein.
Debugging: dashboard() wird nicht ausgeführt
Wenn ein älteres Tutorial nur diese Methode zeigt, ist unter 3.22.x genau das erwartbare Fehlerbild: Das Plugin ist aktiviert, PHP meldet keinen Fehler, aber das Dashboard bleibt unverändert. Prüfe dann nicht als Erstes CSS oder Cache, sondern ob die aktuelle Dashboard-View den Hook überhaupt aufruft.
public function dashboard()
{
return '<div>Mein Widget</div>';
}
Auf 3.16.1 konnte das reichen. Auf 3.22.0 reicht es nicht mehr. Unser Dual-Path ist deshalb der zentrale Teil dieses Tutorials.
Debugging: Layout speichert nicht
- Browser-Netzwerkansicht öffnen und POST auf
.../save-layoutprüfen. - Bei 302/Weiterleitung zum Login zuerst den CSRF-Token prüfen.
- Schreibrechte auf
bl-content/workspaces/md-dashboard-layout/kontrollieren. - JSON-Antwort auf
{"ok":true}prüfen. - Plugin-Seite „Dashboard Layout“ öffnen und die gespeicherte JSON-Struktur ansehen.
Debugging: Widget ist nach einem Update verschwunden
Das sollte in unserem Beispiel gerade nicht passieren. Neue IDs werden bei der Normalisierung in den Hauptbereich ergänzt. Falls du eigene Widgets entwickelst, dürfen ihre technischen IDs nicht bei jedem Release geändert werden. Der sichtbare Titel darf sich ändern; die ID sollte stabil bleiben.
Was wir bewusst nicht tun
- Keine Änderung an
bl-kernel/admin/views/dashboard.php. - Keine SQL-Tabelle für Widgetpositionen.
- Keine ungeschützte öffentliche AJAX-Datei.
- Keine Speicherung beliebiger HTML-Fragmente aus dem Browser.
- Kein CDN und keine externe Drag-and-Drop-Bibliothek.
- Keine Annahme, dass nur Administratoren das Dashboard aufrufen.
Weiterentwicklung: Dashboard als echtes Widget-System
Das Beispiel kann noch weiter abstrahiert werden. Eine spätere Version könnte eine registrierbare Widget-API bereitstellen, über die andere Plugins eigene Dashboard-Karten anmelden. Dann wäre MD Dashboard Layout nur noch der Layout-Manager, während Statistik-, Radio-, Kontakt- oder Backup-Plugins ihre Widgets selbst liefern. Für ein produktives System sollte man dafür eine stabile Widget-Schnittstelle definieren: ID, Titel, Icon, erlaubte Rollen, Render-Callback und Standardzone.
Quellen und Versionsvergleich
- Bludit 3.16.1 – dashboard.php mit Theme::plugins('dashboard')
- Bludit 3.22.0 – aktuelles dashboard.php ohne diesen Hook-Aufruf
- Bludit 3.22.0 – Plugin-Registry, in der dashboard weiterhin registriert ist
- Offizielle Bludit-Dokumentation: Plugin basics
- Offizielle Bludit-Dokumentation: Hooks list
Weiterführung ab MD Dashboard Layout 1.1.0
Ab Version 1.1.0 besitzt MD Dashboard Layout zusätzlich eine Widget-Registry. Andere Bludit-Plugins können damit eigene Dashboard-Karten registrieren, ohne dieses Plugin zu patchen. Siehe MD Dashboard Layout: Was das Plugin kann und das Tutorial für ein eigenes Widget-Plugin.