A plugin can now bring a plugin.php that registers hooks, scheduled tasks, its own tables, public pages and admin screens. Failures stay contained to the plugin; a form on a plugin page can be submitted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 KiB
Plugins schreiben
Ein Plugin bringt eigene Bausteine, Vorlagen und Adressen in den Editor und auf die
Website. Farben, Schriften und Buttons fasst es nie an – das Design gehört der Website (ADR 105). Es ist ein Ordner unter plugins/ mit einer plugin.json, Twig-Vorlagen und optional
Stylesheets und Skripten. Eingeschaltet und eingestellt wird es in der Verwaltung unter Plugins
(Administrator). Hintergrund und Grenzen: ADR 102 und ADR 103 in docs/ARCHITECTURE.md.
Ein Plugin kann reines Manifest plus Twig sein. Braucht es mehr – eigene Tabellen, Aufgaben im
Takt, Seiten aus eigenen Daten, Verwaltungsbildschirme –, bringt es wie bei WordPress eine
plugin.php mit (siehe „Eigener PHP-Code“, ADR 127).
Der Ordner
plugins/
vergleich/ ← Name des Plugins: Kleinbuchstaben und Ziffern, mit Buchstabe beginnend
plugin.json ← das Manifest
blocks/bildvergleich.html.twig
blocks/galerie.html.twig
routes/daten.json.twig
assets/bildvergleich.css ← nur unter assets/ liegt, was die Website ausliefert
assets/bildvergleich.js
Das mitgelieferte Plugin ist die Vorlage zum Abschreiben: plugins/vergleich (Bausteine,
Container, Einstellungen, Adresse, Seitenvorlage).
Das Manifest
{
"name": "vergleich",
"title": "Bildvergleich",
"description": "Vorher und nachher in einem Bild.",
"version": "1.1.0",
"requires": "1.17.0",
"settings": {
"labelBefore": { "kind": "text", "label": "Standardbeschriftung vorher", "default": "Vorher" }
},
"blocks": {
"bildvergleich": {
"label": "Bildvergleich",
"icon": "⇔",
"template": "blocks/bildvergleich.html.twig",
"css": "assets/bildvergleich.css",
"script": "assets/bildvergleich.js",
"props": {
"before": { "kind": "media", "label": "Bild vorher" },
"position": { "kind": "number", "label": "Startposition (%)", "min": 5, "max": 95, "default": 50 }
}
},
"galerie": {
"label": "Vergleichsgalerie",
"template": "blocks/galerie.html.twig",
"container": true,
"children": ["vergleich.bildvergleich"],
"props": { "columns": { "kind": "select", "label": "Spalten", "choices": { "2": "Zwei", "3": "Drei" } } }
}
},
"presets": {
"abschnitt": { "label": "Vorher-Nachher-Abschnitt", "blocks": [ { "id": "s", "type": "section", "children": [ ] } ] }
},
"routes": {
"daten": { "path": "/vergleich/daten", "template": "routes/daten.json.twig", "type": "json" }
}
}
namemuss dem Ordnernamen entsprechen. Der Typ eines Bausteins istname.baustein, hiervergleich.bildvergleich; so heißt er im Dokument und in der Ausgabe.versionin der Form1.2.3. Sie hängt an den Adressen der Dateien (?v=1.1.0), damit der Browser nach einem Update die neue Fassung holt.requires(optional): die kleinste KantSites-Fassung. Passt sie nicht, erscheint das Plugin in der Verwaltung mit dem Grund und lässt sich nicht einschalten.- Ein Manifest braucht mindestens einen Baustein, eine Adresse oder eine Vorlage. Fehlt eine genannte Datei, ist das Manifest ungültig.
Eigenschaften und Einstellungen (Schema)
Eigenschaften (props eines Bausteins) und Einstellungen (settings des Plugins) haben dasselbe
Schema: eine Art (kind), eine Beschriftung (label), optional help und default.
| Art | Feld | Prüfung | Weitere Schlüssel |
|---|---|---|---|
text |
einzeilig | Text bis maxLength (Standard 200) |
maxLength |
textarea |
mehrzeilig | Text bis maxLength (Standard 2000) |
maxLength |
number |
Zahl | zwischen min und max |
min, max |
boolean |
Kästchen | an oder aus | – |
select |
Liste | einer der Werte | choices: {"wert": "Beschriftung"} |
url |
einzeilig | lokaler Pfad, #sprungmarke, https://, mailto:, tel: |
maxLength |
media |
Bild aus der Mediathek | Kennung eines Bildes oder leer | – |
Höchstens 30 Eigenschaften je Baustein. Unbekannte Eigenschaften in einem gespeicherten Dokument werden ignoriert (so kann ein Plugin eine Eigenschaft streichen), bekannte müssen passen, sonst lässt sich die Seite nicht speichern. Einstellungen bearbeitet der Administrator auf dem Bildschirm „Plugins“; sie werden genauso geprüft.
Container
"container": true macht einen Baustein zum Behälter. "children" (optional) zählt auf, was
hinein darf – Kernbausteine wie text oder Bausteine des Plugins; ohne die Liste darf alles hinein.
Der Editor zeigt die Regel im Werkzeugkasten, der Server prüft sie beim Speichern.
Seitenvorlagen (presets)
Fertige Bausteine, die der Editor als Ganzes einfügt – hinter den eingebauten Vorlagen mit demselben
Knopf. blocks ist eine Liste von Bausteinen wie in einem Dokument (höchstens 64 KiB); Kennungen
vergibt der Editor bei jedem Einfügen neu, du kannst also feste schreiben.
Eigene Adressen (routes)
path hat die Form einer Seite (/name oder /name/unter/seite, Slugs) und darf nichts belegen,
was der Kern beantwortet (/admin, /media, /assets, /formular, /beitraege …). type ist
html, json, text oder xml. Eine veröffentlichte Seite mit derselben Adresse geht vor.
Ausgeschaltet antwortet die Adresse mit 404; alles außer html trägt noindex.
Was ein Plugin nicht kann: das Design
Es gibt kein theme im Manifest. Ein Plugin gestaltet seine eigenen Bausteine über deren
Stylesheet und die Design-Tokens des Kerns; Palette, Schriften und Buttons der Website ändert
nur der Betreiber unter Design. So färbt kein Schalter eine Website um, und nichts, was dort
eingestellt wurde, geht durch ein Plugin verloren (ADR 105).
Eigener PHP-Code (plugin.php)
"main": "plugin.php" im Manifest lädt die Datei, solange das Plugin eingeschaltet ist. Sie gibt
eine Funktion zurück und meldet sich dort beim Kern an:
<?php
return static function (App\Plugin\PluginApi $api): void {
$tabelle = $api->table('objekt'); // plugin_<name>_objekt
$api->onInstall(fn () => $api->db()->executeStatement("CREATE TABLE IF NOT EXISTS $tabelle (id VARCHAR(20) PRIMARY KEY)"));
$api->addTask('abgleich', 'Abgleich', 'Was ausbleibt, wenn sie nicht läuft.', 3600, fn (): string => 'Bericht');
$api->addPage('/objekte/{slug}', fn (array $p): ?array => ['title' => 'Titel', 'description' => 'Text', 'blocks' => []]);
$api->addData('liste', fn (array $argumente): array => []);
$api->addFilter('sitemap.entries', fn (array $e, string $origin): array => $e);
$api->addAdminPage('uebersicht', 'Übersicht', 'admin/uebersicht.html.twig', fn (): array => []);
$api->addAdminAction('jetzt', 'Jetzt ausführen', 'abgleich'); // Aufgabenschlüssel oder Callback
};
| Methode | Zweck |
|---|---|
onInstall, onDeactivate |
Beim Einschalten und bei neuer Fassung; beim Ausschalten. Muss wiederholbar sein. Tabellen bleiben beim Ausschalten stehen. |
addAction, addFilter |
Hooks wie bei WordPress, kleinere Priorität zuerst. |
addTask |
Aufgabe im Plan des Kerns (Bildschirm „Aufgaben“); Fehler werfen, Rückgabe ist der Bericht. |
addPage |
Öffentliche Seite im Rahmen der Website. Gibt title, description, blocks und optional seoTitle, robots, status, image zurück oder null für 404. Eine veröffentlichte Seite geht vor. |
addData |
Werte für Vorlagen: kant_plugin_data('name', 'schluessel', {…}). |
addAdminPage, addAdminAction |
Bildschirm und Knopf der Verwaltung, nur für Administratoren, Knöpfe mit CSRF-Schutz. |
settings, db, table, media, fetch, storeImage, directory |
Einstellungen, Datenbank, Mediathek, HTTPS-Abruf, Bild ablegen. |
Der Kern ruft bisher diese Filter auf: sitemap.entries ($entries, $origin) und form.context
($extra, $form, $path: zusätzliche Angaben Name => Text für eine eingehende Anfrage).
Weitere Hooks kommen in den
Kern, wenn ein Plugin sie braucht.
Die SQL-Anweisungen müssen auf SQLite und MySQL laufen. Klassen und Funktionen gehören in einen eigenen Namensraum. Plugin-Tabellen gehören nicht zum Inhaltspaket; bei SQLite sind sie in jeder Sicherung, weil die ganze Datei gesichert wird.
Rechte: Der Code läuft mit den vollen Rechten der Anwendung. Das ist kein Sandkasten. Eine
Ausnahme beim Laden, in einem Hook oder auf einer Seite nimmt nur das Plugin aus dem Spiel und
steht auf dem Bildschirm „Plugins“. Einen echten PHP-Abbruch fängt das nicht ab: Dann den Ordner
unter plugins/ umbenennen.
Die Vorlagen
Bausteine
Die Vorlage eines Bausteins bekommt genau sechs Dinge:
props– die geprüften Eigenschaften, jede vorhanden, fehlende mit ihrem Vorgabewertsettings– die Einstellungen des Plugins, jede vorhandenchildren– bei einem Container die fertig gerenderten Kinder (leer sonst); ausgeben mit{{ children }}block– der Baustein selbst;block.idgehört alsidan das Wurzelelement, sonst greifen eigenes CSS und die Editorauswahl nichtdesignClasses– Klassen aus Sichtbarkeit, Design-Tokens und Einblendungen, an dieclassdes Wurzelelements anhängenediting–truein der Editorvorschau, für Hinweise wie „bitte ein Bild wählen“
Dazu eine Funktion: kant_plugin_media(kennung) liefert ein Bild der Mediathek als
{id, title, alt, variants: [{src, width, height}, …]} (kleinste Fassung zuerst) oder null.
{% set bild = props.before ? kant_plugin_media(props.before) : null %}
<div id="{{ block.id }}" class="block-bildvergleich{{ designClasses }}" data-position="{{ props.position }}">
{% if bild %}<img src="{{ (bild.variants|last).src }}" alt="{{ props.alt ?: settings.labelBefore }}">{% endif %}
{{ children }}
</div>
Adressen
Die Vorlage einer Adresse bekommt settings, query (die Abfrageparameter mit sauberen Namen,
gekürzt) und route. Für json und xml schreibt sie die Antwort selbst, etwa mit json_encode.
Was keine Vorlage bekommt: Datenbank, Einstellungen der Website, Anfrage. Sie läuft mit den Rechten der Anwendung – ein Sandkasten ist das nicht, und so wird es auch nicht genannt.
Stylesheet und Skript
Beide werden nur auf Seiten geladen, auf denen der Baustein vorkommt, über
/plugin-assets/<name>/assets/.... Die Content-Security-Policy erlaubt Skripte vom eigenen
Ursprung, sobald die Seite eines braucht; Inline-Stile und Inline-Skripte bleiben verboten. Ein
Skript setzt Werte deshalb über das CSSOM (element.style.setProperty(...)), nie über ein
style-Attribut in der Vorlage. In der Editorvorschau wird das Stylesheet geladen, das Skript
nicht – die Vorlage muss auch ohne Skript ein brauchbares Bild abgeben.
Was der Kern übernimmt
- Speichern, Veröffentlichen, Fassungen, Undo/Redo – der Baustein ist ein gewöhnlicher Block.
- Eigenes CSS je Block, Design-Tokens (Abstände, Ausrichtung, Ecken), Sichtbarkeit je Gerät,
Einblendungen – alles über
designClassesund dieid. - Der Editor: Eintrag im Werkzeugkasten unter „Erweiterungen“, Eigenschaftenpanel aus dem Schema, Container-Regeln, Vorlagen, Vorschau über den Server.
- Der Erhaltungsvertrag: ist das Plugin aus oder fehlt es, bleibt der Baustein im Dokument, der Editor zeigt einen Platzhalter, die Website einen HTML-Kommentar. Nichts geht verloren.
Updates
Mitgelieferte Plugins liegen im Releasearchiv und werden beim Archiv-Update Datei für Datei
aktualisiert. Ein selbst installiertes Plugin bleibt dabei stehen, weil sein Ordner im Archiv
nicht vorkommt. Beim Git-Update ist ein fremder Ordner unter plugins/ nicht von Git verwaltet;
git checkout lässt ihn in Ruhe. Die Einstellungen liegen in der Datenbank und überleben beides.