Seitenbaukasten (Bausteine und Blöcke)
Seiten und Collection-Einträge können ihren Inhalt als Baum aus Blöcken speichern. Ein Block ist ein Baustein – ein Snippet mit Schema (Parameter mit Typen, Slots für Kind-Blöcke). Gespeichert werden nur Daten, nie HTML; gerendert wird über die Komponenten-Technik ({% component %}/{% slot %}) mit dem Tag {% blocks %}. Ein Blockbaum steckt an drei Stellen:
- an Seiten im Feld
blocks, - in Collection-Feldern vom Typ
blocks, - in globalen Blöcken – wiederverwendbaren Bäumen, auf die andere Bäume per
{"global": id}verweisen.
Baustein-Schema
Das Feld schema an einem Snippet macht es zum Baustein (null = kein Baustein; PUT ohne schema lässt es unverändert, "schema": null entfernt es):
{
"block": true,
"label": "Hero",
"description": "Großes Titelbild mit Überschrift",
"icon": "image",
"category": "Inhalt",
"params": [
{ "name": "titel", "label": "Titel", "type": "text", "required": true },
{ "name": "bild", "label": "Bild", "type": "image" },
{ "name": "text", "label": "Text", "type": "markdown" },
{ "name": "variante", "label": "Variante", "type": "select", "options": ["hell", "dunkel"], "default": "hell", "design": true },
{ "name": "produkt", "label": "Produkt", "type": "reference", "collection": 3 }
],
"slots": [
{ "name": "default", "label": "Inhalt", "allow": ["text", "bild"] },
{ "name": "buttons", "label": "Buttons", "allow": ["button"], "max": 3 }
]
}
| Feld | Beschreibung |
|---|---|
block |
true = im Formular-Editor als Baustein anbieten. Ein Schema mit false bleibt gespeichert, der Baustein ist aber nicht wählbar und Blöcke dieses Typs werden beim Speichern abgelehnt |
label, description, icon, category |
Anzeige im Editor (label leer = Snippet-Name) |
params[] |
Parameter wie Collection-Felder: name ([a-zA-Z_][a-zA-Z0-9_]*, eindeutig, nicht block, slots, id), label, type (alle Feldtypen außer blocks), required, options (bei select), collection (bei reference/references, muss existieren) |
params[].default |
Standardwert (muss zum Typ passen): wird beim Rendern genutzt, wenn der Block den Parameter nicht setzt, und erfüllt beim Speichern ein Pflichtfeld |
params[].design |
Design-Parameter (Variante, Abstände, Farben) – im CMS nur Anzeige (eigene Gruppe „Darstellung“); IllusionPublish nutzt es für Feldrechte |
slots[] |
name (default = Standard-Slot {% slot %}), label, allow (erlaubte Bausteine als Snippet-Namen; fehlt/leer = alle), max (höchstens so viele Blöcke, 0 = beliebig) |
Im Snippet stehen die Parameter als Variablen bereit ({{ titel }}), die Slots per {% slot "buttons" %}{% endslot %} (Standard-Slot {% slot %}), slots.<name> ist true, wenn ein Slot Inhalt hat, und block ist { "id": …, "type": … }.
Fehler beim Speichern: 400 mit den Codes block.paramNameInvalid, block.paramTypeInvalid, block.paramLinkMissing, block.paramDefaultInvalid, block.slotNameInvalid (siehe Fehlercodes). Warnungen lehnen nicht ab, sondern stehen in der Antwort von POST/PUT /snippets unter warnings (gleiches Format wie Fehler):
{ "id": 7, "name": "hero", "schema": { "block": true, "…": "…" },
"warnings": [{ "code": "block.slotNotInContent", "error": "slot \"buttons\" is not used in the snippet ({% slot \"buttons\" %})", "params": { "slot": "buttons" } }] }
block.slotNotInContent – ein Slot des Schemas kommt im Snippet nicht als {% slot %} vor; block.slotNotInSchema – das Snippet nutzt einen Slot, den das Schema nicht kennt (nur bei block: true).
GET /blocks/types 🔑
Alle Bausteine (Snippets mit schema.block == true), sortiert nach Name – für Formular-Editor und Integrationen:
[{ "name": "hero", "label": "Hero", "description": "Großes Titelbild mit Überschrift", "icon": "image", "category": "Inhalt",
"params": [{ "name": "titel", "label": "Titel", "type": "text", "required": true }], "slots": [{ "name": "buttons", "label": "Buttons", "allow": ["button"], "max": 3 }] }]
label leer = Snippet-Name, description leer = Beschreibung des Snippets; params/slots sind immer Listen.
Blockbaum
[
{ "id": "b7f3k2", "type": "hero", "params": { "titel": "Willkommen", "bild": 12, "variante": "dunkel" },
"slots": { "buttons": [{ "id": "b9x1aa", "type": "button", "params": { "text": "Mehr", "link": "/ueber-uns" } }] } },
{ "id": "b2m8qq", "global": 4 }
]
| Feld | Beschreibung |
|---|---|
id |
Im Baum eindeutig ([A-Za-z0-9_-]{1,64}), vom Client vergeben und über Speichervorgänge stabil. Der Server ergänzt fehlende und ersetzt ungültige oder doppelte (b + 6 Zeichen) |
type |
Name eines Bausteins (Snippet-Name) |
params |
Werte gemäß Schema: Bild/Datei = Datei-ID, reference = Eintrags-ID, references = Liste von IDs, date = YYYY-MM-DD, boolean, number, select = eine der options. Wird beim Speichern wie ein Collection-Eintrag umgewandelt ("3" → 3); unbekannte Parameter fallen weg. In der Antwort immer ein Objekt |
slots |
Slot-Name → Liste von Blöcken (nur Slots mit Inhalt) |
global |
Verweis auf einen globalen Block – dann ohne type/params/slots |
Prüfung beim Speichern (Seiten, Einträge, globale Blöcke): Baustein existiert und ist einer; Pflichtparameter gesetzt (oder Standardwert); Werte passen zum Typ; Slots stehen im Schema; allow/max eingehalten; höchstens 10 Ebenen; höchstens 1 MB (JSON); verwiesene globale Blöcke existieren. Ein Verweis {"global": id} in einem Slot mit allow-Liste ist nur erlaubt, wenn alle Blöcke der obersten Ebene des globalen Blocks erlaubte Bausteine sind (globale Verweise auf dessen oberster Ebene werden aufgelöst), sonst block.globalNotAllowed; für max zählt ein Verweis als ein Block. Fehler: 400 mit Code und dem Pfad im Baum, z. B. { "code": "block.slotNotAllowed", "params": { "path": "0.slots.buttons.1", "slot": "buttons", "type": "text" } }. Bei Collection-Feldern kommt params.field dazu.
Geprüft wird nur, was mitgeschickt wird: PUT /pages/{id} ohne blocks bzw. ein Teil-Update eines Eintrags ohne das Blöcke-Feld lässt den gespeicherten Baum ungeprüft stehen – auch wenn ein Baustein inzwischen gelöscht oder umbenannt wurde. Gerendert wird so ein Block dann gar nicht (Live) bzw. als Hinweis (Vorschau).
Ausgabe im Template
{% blocks ausdruck %} rendert einen Blockbaum: {% blocks page.blocks %} (Seite) oder {% blocks post.inhalt %} (Feld eines Eintrags aus data()/item()). Je Block wird der Baustein wie eine Komponente gerendert – mit den globalen Variablen (page, site, route, data() …), den Parametern und den gerenderten Kind-Blöcken als Slots. Parameterwerte sind typgerecht aufbereitet:
| Typ | Im Baustein |
|---|---|
text, textarea, url, select, date, number, boolean |
Rohwert (wird wie üblich escaped) |
markdown |
Gerendertes, sicheres HTML (wie |markdown) |
html |
Sicheres HTML |
image, file |
Datei-Objekt wie file(id) (id, url, srcset, webp_srcset, width, height, alt, name, private …), z. B. {{ image(bild.id) }} oder {{ bild.url }}. Private Dateien nur in der Vorschau signiert |
reference, references |
Aufgelöster Eintrag bzw. Liste (eine Ebene, wie bei Collection-Verknüpfungen); für Besucher nur veröffentlichte |
Leere Parameter bekommen den default des Schemas. Ein leerer oder ungültiger Wert hinter {% blocks %} gibt nichts aus. Unbekannte Bausteine (gelöscht, kein Baustein mehr) und fehlende globale Blöcke ergeben live keine Ausgabe (mit Log-Eintrag), in der Vorschau einen sichtbaren Hinweis-Kasten (<div class="ic-block-problem">). CSS/JS, die an einem Baustein hängen, werden wie bei Komponenten auf Seiten geladen, die ihn benutzen.
Vorschau-Markierungen
Nur in Vorschauen (Admin-Vorschau POST /pages/{id}/preview, ?preview=1 mit Admin-Session, Pen-Editor) umschließen HTML-Kommentare jeden Block – für Integrationen wie IllusionPublish, die Blöcke in der Vorschau anklickbar machen. Es gibt bewusst keine Wrapper-Elemente (sie würden CSS wie .grid > .card brechen), und Live-Seiten, Vorschau-Links und der statische Export enthalten nie Markierungen.
<!--ic-block:page.blocks/0:b7f3k2:hero--><section class="hero">…
<!--ic-block:page.blocks/0/slots/buttons/0:b9x1aa:button--><a …>Mehr</a><!--/ic-block:b9x1aa-->
</section><!--/ic-block:b7f3k2-->
<!--ic-block:page.blocks/1:b2m8qq:global:4--><!--ic-block:global:4/0:n1:text-->…<!--/ic-block:n1--><!--/ic-block:b2m8qq-->
Format: <!--ic-block:<pfad>:<id>:<type>--> … <!--/ic-block:<id>-->, für Verweise auf globale Blöcke <!--ic-block:<pfad>:<id>:global:<gid>-->. Der Pfad beginnt mit der Herkunft des Baums – page.blocks, item:<collection-slug>:<eintrags-id>:<feld> (Blöcke-Feld eines Eintrags, auch beim ungespeicherten item der Vorschau) oder global:<gid> (Kinder eines globalen Blocks) –, gefolgt von /<index> und /slots/<name>/<index> je Ebene. Pfade enthalten Doppelpunkte; id und type sind die letzten beiden Teile (bei globalen Blöcken die letzten drei). Wird ein Baum ohne erkennbare Herkunft ausgegeben (z. B. ein zusammengesetzter Wert), beginnt der Pfad mit blocks.
Listen, Slots und Feld-Markierungen
Ebenfalls nur in Vorschauen kommen drei weitere Angaben dazu – für visuelle Editoren wie IllusionPublish, die Inhalte direkt auf der Seite bearbeiten:
<!--ic-blocks:page.blocks-->…<!--/ic-blocks:page.blocks-->
<!--ic-slot:page.blocks/0/slots/buttons-->…<!--/ic-slot:page.blocks/0/slots/buttons-->
<h1 data-ic-field="page.blocks/0/titel">Frisch aus dem Ofen</h1>
- Listen: Jede Ausgabe von
{% blocks … %}steht zwischen<!--ic-blocks:<herkunft>-->und<!--/ic-blocks:<herkunft>-->– auch eine leere Liste, damit ein Editor dort einen Einfügepunkt zeigen kann. Herkunft wie beim Pfad (page.blocks,item:<collection-slug>:<eintrags-id>:<feld>, unbekanntblocks). - Slots: Jeder
{% slot %}eines Blocks steht zwischen<!--ic-slot:<blockpfad>/slots/<name>-->und<!--/ic-slot:…-->, ebenfalls auch leer (bzw. mit seinem Fallback-Inhalt). Steht der Slot im Template in einem{% if slots.x %}, gibt es für den leeren Slot keine Markierung. - Felder:
data-ic-field="<blockpfad>/<parameter>"an dem Element, das einen Parameter darstellt. Automatisch für Parameter vom Typtext,textareaundmarkdown, wenn ihr Wert unverändert den ganzen Inhalt eines Elements bildet (<h1>{{ titel }}</h1>,<div class="text">{{ inhalt }}</div>, Zeilenumbrüche per|linebreaksbrsind erlaubt), und für Bild-Parameter am<img>vonimage(bild.id)bzw. am Element, in dessen Attributen{{ bild.url }}steht (src,style…). Werte in Attributen, Skripten, mitten in anderem Text oder durch Filter verändert (|upper,|truncatechars) bleiben unmarkiert – geraten wird nie, und Attribute bleiben unverändert. Explizit markiert{{ block_field("titel") }}ein Element (gibt in der Vorschaudata-ic-field="…"aus, live nichts):<p class="lead"{{ block_field("titel") }}>{{ titel|upper }}</p>.
Technik: Die Automatik umschließt die Werte beim Rendern mit unsichtbaren Zeichen aus einem privaten Unicode-Bereich (U+F0000 ff.) und entfernt sie nach dem Rendern wieder aus dem ganzen Dokument (auch URL-kodiert und als JS-Escape). Filter, die Zeichen zählen (|length, |truncatechars), zählen diese Zeichen in der Vorschau mit – das Ergebnis kann dort etwas kürzer ausfallen als live.
Globale Blöcke
Wiederverwendbare Blockbäume (z. B. Newsletter-Kasten, Kontaktleiste). Ein Verweis {"global": id} setzt beim Rendern den aktuellen Baum des globalen Blocks ein – Änderungen wirken überall. „Einbetten“ (lokale Kopie) macht der Client: Verweis durch eine Kopie der Blöcke mit neuen IDs ersetzen.
{ "id": 4, "name": "Newsletter-Kasten", "folder": "Kästen", "description": "Anmeldung",
"blocks": [{ "id": "n1", "type": "text", "params": { "inhalt": "Jetzt anmelden" } }],
"created_at": 1791191665, "updated_at": 1791191700 }
| Endpunkt | Zugriff | Beschreibung |
|---|---|---|
GET /global-blocks |
🔑 | Alle globalen Blöcke (mit blocks), sortiert nach Ordner und Name |
POST /global-blocks |
✏️ | Anlegen; Pflicht: name. 201 |
GET /global-blocks/{id} |
🔑 | Ein globaler Block |
PUT /global-blocks/{id} |
✏️ | Merge-Update mit Konfliktprüfung (base_updated_at); ohne blocks bleibt der Baum (ungeprüft). Versionshistorie (kind=globalblock) |
DELETE /global-blocks/{id} |
✏️ | Nur ohne Verwendung, sonst 409 block.globalInUse mit params.count. 204 |
GET /global-blocks/{id}/usage |
🔑 | Verwendungen (direkte Verweise) |
Verwendungen: [{ "kind": "page", "id": 5, "title": "Aktuelles", "path": "/aktuelles" }, { "kind": "item", "id": 12, "title": "Sauerteig", "path": "/blog/sauerteig", "collection": "blog" }, { "kind": "global", "id": 2, "title": "Fußbereich", "path": "" }] – gesucht wird in den Blockbäumen aller Seiten, aller Einträge mit Blöcke-Feldern und der anderen globalen Blöcke (kein Index, bei jedem Aufruf). title bei Einträgen: Feld title/titel/name oder erstes Textfeld; path nur, wenn die Collection eine Detailseite hat.
Überschreibungen je Verwendung: Ein Verweis darf zusätzlich "overrides": { "<Block-ID im globalen Block>": { "<Parameter>": <Wert> } } tragen – einzelne Parameter der Blöcke im globalen Baum werden nur für diese Verwendung ersetzt (Parameter, nie Aufbau oder Slots). Werte werden wie Baustein-Parameter geprüft (block.paramInvalid, params.path = <pfad>.overrides.<id>); null leert den Parameter (es gilt der Standard des Bausteins). Blöcke und Parameter, die im globalen Block nicht (mehr) vorkommen, werden beim Speichern verworfen und beim Rendern ignoriert. Datei-IDs in Überschreibungen zählen als verwendet. Änderungen am globalen Block wirken weiter überall, soweit nicht überschrieben.
Zyklen: Ein globaler Block darf sich weder direkt noch über andere globale Blöcke selbst enthalten – beim Speichern 400 block.globalCycle; beim Rendern bricht ein Zyklus (z. B. nach einem Wiederherstellen) mit einem Hinweis ab.
Eingeschränkte Slots: Würde ein PUT mit neuem blocks einen globalen Block dort unzulässig machen, wo er bereits in einem Slot mit allow-Liste steckt (auch über andere globale Blöcke), lehnt der Server ab: 400 block.globalNowNotAllowed mit params.count (betroffene Stellen).
Globale Blöcke stehen im Bundle (Tabelle global_blocks, IDs bleiben). Datei-IDs in Bild-/Datei-Parametern aller Blockbäume zählen beim Aufräumen als verwendet.
Fehlercodes des Seitenbaukastens
| Code | Parameter | Bedeutung |
|---|---|---|
block.treeInvalid |
path |
Kein Blockbaum (keine Liste, Block kein Objekt, params/slots falsch, global keine ID) |
block.fieldInvalid |
field |
Collection-Feld vom Typ blocks ist keine Liste (auch kein JSON-Text) |
block.typeMissing |
path |
Block ohne type |
block.typeUnknown |
path, type |
Snippet gibt es nicht |
block.notABlock |
path, type |
Snippet ist kein Baustein (schema.block fehlt) |
block.paramRequired |
path, param |
Pflichtparameter fehlt (ohne Standardwert) |
block.paramInvalid |
path, param, error |
Wert passt nicht zum Typ |
block.slotUnknown |
path, slot, type |
Slot steht nicht im Schema |
block.slotNotAllowed |
path, slot, type |
Baustein im Slot nicht erlaubt (allow) |
block.slotMax |
path, slot, max |
Zu viele Blöcke im Slot |
block.tooDeep |
path, max |
Mehr als 10 Ebenen |
block.tooLarge |
max |
Baum größer als 1 MB |
block.globalUnknown |
path, id |
Globaler Block existiert nicht |
block.globalCycle |
id |
Globaler Block enthielte sich selbst |
block.globalNotAllowed |
path, slot, id, type |
Verweis auf einen globalen Block in einem Slot mit allow-Liste, dessen oberste Ebene einen nicht erlaubten Baustein type enthält |
block.globalNowNotAllowed |
count |
Änderung eines globalen Blocks würde ihn an count Verwendungen in eingeschränkten Slots unzulässig machen |
block.globalInUse |
count |
Löschen eines verwendeten globalen Blocks |
block.paramNameInvalid, block.paramTypeInvalid, block.paramLinkMissing, block.paramDefaultInvalid, block.slotNameInvalid |
name (type) |
Fehler im Baustein-Schema |
block.slotNotInContent, block.slotNotInSchema |
slot |
Nur Warnungen (warnings) beim Speichern eines Snippets |
path ist der Pfad im Baum mit Punkten (0.slots.buttons.1, leer = der ganze Baum); bei Collection-Feldern zusätzlich field.