Handbuch · 7

Snippets, Komponenten und Layouts

Snippets sind wiederverwendbare Template-Bausteine. Es gibt vier Arten, sie zu nutzen – vom einfachen Einbinden bis zur Komponente mit Slots.

7.1 Einbinden mit include

{% include "header" %}
{% include "teaser" with titel="Neu bei uns" link="/aktuelles" %}

Das Snippet sieht alle Variablen der Seite plus die übergebenen Werte. Änderungen am Snippet wirken sofort auf allen Seiten, die es einbinden. Snippet-Namen dürfen Schrägstriche enthalten, z. B. teile/footer. Ein neues Snippet mit so einem Namen landet in der Liste automatisch im Ordner teile. Den Ordner kannst du neben dem Namen auch frei setzen, er dient nur der Übersicht.

7.2 Komponenten mit Slots

Eine Komponente ist ein Snippet, das Inhalt umschließen kann – wie Komponenten in Vue oder Svelte, nur serverseitig. Das Snippet legt mit {% slot %} fest, wo der Inhalt erscheint; beim Verwenden füllst du die Slots.

Das Snippet card:

<div class="card {{ variante|default:"normal" }}">
  <h3>{% slot "titel" %}Ohne Titel{% endslot %}</h3>
  <div class="card-body">{% slot %}{% endslot %}</div>
  {% if slots.footer %}
    <footer>{% slot "footer" %}{% endslot %}</footer>
  {% endif %}
</div>

Verwenden:

{% component "card" variante="hervorgehoben" %}
  {% fill "titel" %}Unser <em>Angebot</em>{% endfill %}
  <p>Alles, was nicht in einem fill steht, landet im Standard-Slot.</p>
  {% fill "footer" %}<a href="/kontakt">Jetzt anfragen</a>{% endfill %}
{% endcomponent %}

Die Regeln:

Element Bedeutung
{% slot %}…{% endslot %} Standard-Slot im Snippet. Der Inhalt zwischen den Tags ist der Fallback, wenn nichts übergeben wird.
{% slot "name" %}…{% endslot %} Benannter Slot mit Fallback.
{% fill "name" %}…{% endfill %} Füllt beim Verwenden einen benannten Slot.
Inhalt außerhalb von fill Füllt den Standard-Slot.
slots.name / slots.default Im Snippet: true, wenn der Slot gefüllt wurde – für optionale Wrapper.
name=wert nach dem Snippet-Namen Parameter (Props), im Snippet als Variable verfügbar.

Wichtig zu wissen:

  • Der Inhalt sieht die Variablen der Seite. Was zwischen component und endcomponent steht, wird im Kontext der aufrufenden Seite gerendert – Schleifenvariablen funktionieren:

    {% for post in data("blog") %}
      {% component "card" %}
        {% fill "titel" %}{{ post.titel }}{% endfill %}
        {{ post.text|markdown }}
      {% endcomponent %}
    {% endfor %}
    
  • Das Snippet sieht nur seine Parameter plus die globalen Variablen (page, route, nav(), data() …), aber keine lokalen Variablen der Seite. Was die Komponente braucht, gibst du explizit als Parameter mit. Das macht Komponenten vorhersehbar und wiederverwendbar.

  • HTML als Parameter bleibt HTML, wenn es als sicher markiert ist: text=post.text|markdown oder inhalt="<b>x</b>"|safe. Normale Texte werden wie üblich escaped.

  • Verschachteln ist erlaubt – Komponenten in Komponenten, auch in fill-Blöcken:

    {% component "card" %}
      {% fill "titel" %}Kontakt{% endfill %}
      {% component "button" href="/kontakt" variante="primaer" %}Schreib uns{% endcomponent %}
    {% endcomponent %}
    
  • Der Snippet-Name kann auch aus einer Variablen kommen: {% component block.typ %} – nützlich für Seitenbaukästen aus Collection-Daten.

  • Eine Komponente, die sich selbst endlos aufruft, wird nach 30 Ebenen mit einer Fehlermeldung gestoppt.

Snippets mit Slots tragen in der Snippet-Liste das Abzeichen Komponente; im HTML-Editor fügt Snippet → Komponente das Grundgerüst mit allen benannten Slots ein.

7.3 Layouts mit extends und block

Ein Layout legt das Gerüst einer ganzen Seite fest; die Seite füllt nur dessen Blöcke. Das Snippet layouts/standard:

{% include "header" %}
<main class="container">
  {% block inhalt %}{% endblock %}
</main>
<aside>{% block seitenleiste %}{% include "standard-seitenleiste" %}{% endblock %}</aside>
{% include "footer" %}

Im HTML einer Seite:

{% extends "layouts/standard" %}

{% block inhalt %}
  <h1>{{ page.title }}</h1>
  <p>…</p>
{% endblock %}
  • extends muss ganz am Anfang stehen. Alles außerhalb von block wird ignoriert.
  • Nicht überschriebene Blöcke behalten den Inhalt aus dem Layout (hier die Standard-Seitenleiste).
  • Pro Seite gibt es genau ein Layout. Für wiederholte Bausteine innerhalb einer Seite nimmst du Komponenten.

Seitenlayouts für neue Seiten

Ein Layout-Snippet kann sich im Snippet-Editor unter dem Reiter Seitenlayout als Vorlage für neue Seiten anbieten: Haken Als Seitenlayout anbieten, dazu Name und Beschreibung für die Auswahl und optional Für neue Seiten vorauswählen. Im Dialog Neue Seite stehen die Seitenlayouts dann neben Leere Seite (das bisherige Verhalten) zur Wahl; eine neue Seite mit Layout enthält nur

{% extends "layouts/inhaltsseite" %}

Änderungen am Layout wirken damit auf alle Seiten, die es nutzen, und einzelne Seiten können wie oben Blöcke überschreiben. Neben jedem Layout zeigt IllusionCMS, ob es einen Content-Bereich für Blöcke hat – also {% blocks page.blocks %} ausgibt, direkt oder über extends, include und Komponenten. Layouts ohne diesen Bereich tragen den Chip statischer Inhalt: Die Seite zeigt festen Inhalt aus dem Template, Blöcke lassen sich dort nicht pflegen. Das ist ein normaler, gewollter Fall – etwa für eine Kontaktseite mit Anschrift und Formular.

Blöcke werden nicht automatisch eingefügt, damit du die volle Kontrolle über den Code der Seite behältst. Es kann gewollt sein, Layouts auszuwählen, die keine pflegbaren Content-Bereiche haben.

Integrationen wie IllusionPublish lesen die Seitenlayouts über GET /api/v1/page-layouts und legen Seiten mit layout_id an (API-Dokumentation).

7.4 Makros

Für kleine Bausteine mit Parametern ohne Inhalt gibt es Makros. Im Snippet makros (das export ist Pflicht):

{% macro preis(betrag, waehrung="€") export %}<span class="preis">{{ betrag|floatformat:2 }} {{ waehrung }}</span>{% endmacro %}
{% import "makros" preis %}
{{ preis(19.9) }}          → 19.90 €
{{ preis(5, "$") }}        → 5.00 $

In den meisten Fällen sind Komponenten flexibler, weil sie HTML-Inhalt aufnehmen können.

7.5 CSS und JS an Snippets

Im Snippet-Editor ordnest du rechts unter CSS & JS dieses Snippets Dateien aus der Bibliothek zu (+ zuordnen) oder legst neue an (+ neu, Name mit .css oder .js). Sie laden nur auf Seiten, die das Snippet beim Rendern tatsächlich ausführen – Styles und Skripte eines Sliders also nicht auf jeder Seite, sondern nur dort, wo der Slider steht.

  • Gezählt wird, was wirklich gerendert wird: {% include %} (auch mit Namen aus einer Variablen), Komponenten, verschachtelte Snippets und Layouts per extends. In einem {% if %}-Zweig, der nicht greift, oder einer leeren Schleife lädt nichts.
  • Makro-Snippets laden ihre Dateien, sobald sie per {% import %} eingebunden werden.
  • Endlosrekursion vermeiden: Rekursive Makros und Includes (z. B. für Baumdarstellungen) sind erlaubt, brauchen aber immer eine Abbruchbedingung. Eine Endlosrekursion – auch über ein include mit Variable, das sich selbst einbindet – beendet den Server-Prozess; ebenso extrem große Werte bei {% lorem %} und den Filtern center, ljust, rjust und floatformat. Beim Testen neuer Makros also erst mit kleinen Werten arbeiten.
  • Nur Seiteninhalt und Head-Feld zählen – ein Snippet im SEO-Titel oder in der Beschreibung lädt nichts.
  • CSS steht im <head>, JS vor </body>, jeweils hinter den Dateien der Seite. Jede Datei lädt nur einmal – auch wenn das Snippet zehnmal vorkommt oder die Seite dieselbe Datei schon hat.
  • Die Vorschau im Seiten-Editor zeigt den gespeicherten Stand des Snippets.

Fertig gebaute Widgets aus Svelte, Vue, React & Co.: Du baust das Modul in einem eigenen Projekt (z. B. mit Vite) und übernimmst das Ergebnis aus dist/:

Aus dem Build Wird in IllusionCMS
das Markup aus dem <body> der index.html, meist nur der Mount-Punkt (<div id="app"></div>) der Inhalt des Snippets
assets/index-….js eine JS-Datei am Snippet (+ neu → z. B. wetter.js, Inhalt einfügen)
assets/index-….css eine CSS-Datei am Snippet

Danach bindest du das Snippet ein ({% include "wetter" %}) – CSS und JS laden nur dort. Vite erzeugt das JS für <script type="module">. Enthält die Datei Modul-Syntax (import/export auf oberster Ebene oder import.meta), bindet IllusionCMS sie automatisch so ein; ohne Modul-Syntax als normales <script> – beides ohne Einstellung.

Damit der Build ohne Nacharbeit läuft:

  • Eine JS-Datei: Teilt Vite den Code in mehrere Dateien (import(…) für Lazy Loading, eigener vendor-Chunk), fehlen die übrigen – die Datei liegt unter /_assets/js/<id>.js, relative Importe finden dort nichts. Lazy Loading weglassen oder in vite.config.js build: { rollupOptions: { output: { inlineDynamicImports: true } } } setzen.
  • Bilder und Schriften aus dem Build: entweder klein genug zum Einbetten (build.assetsInlineLimit), oder unter Dateien hochladen und mit absoluter Adresse (/media/…) verwenden.
  • Mehrfach auf einer Seite: Das Skript lädt nur einmal. Ein Build, der in #app startet, startet also nur einmal. Für mehrere Instanzen im Einstiegspunkt alle Platzhalter suchen, z. B. mit Svelte 5:
// src/main.js
import { mount } from 'svelte'
import Wetter from './Wetter.svelte'

document.querySelectorAll('.wetter-widget').forEach((el) => {
  mount(Wetter, { target: el, props: { ort: el.dataset.ort } })
})

Das Snippet liefert dann <div class="wetter-widget" data-ort="{{ ort|default:"Berlin" }}"></div> – Werte aus dem Template kommen über data-…-Attribute ins Widget (Ausgaben mit {{ … }} sind automatisch escaped).

  • Module laufen erst, wenn die Seite geparst ist – die Platzhalter stehen dann schon im DOM.

7.6 Seitenbaukasten: Bausteine und Blöcke

Für Seiten, die andere ohne HTML pflegen sollen, gibt es den Seitenbaukasten: Du baust einmal Bausteine (Snippets mit Schema), und die Seite oder ein Collection-Eintrag speichert nur noch Daten – eine Liste von Blöcken, jeder mit Baustein, Werten und gegebenenfalls verschachtelten Blöcken. Das HTML bleibt in deinen Snippets; der Formular-Editor fasst nie Markup an.

Baustein anlegen

Ein Baustein ist ein Snippet mit Schema. Im Snippet-Editor öffnest du den Reiter Baustein, klickst Als Baustein definieren und legst fest:

  • Bezeichnung, Beschreibung, Icon und Kategorie – so erscheint der Baustein in der Auswahl Block hinzufügen.
  • Parameter – aufgebaut wie die Felder einer Collection: Name, Bezeichnung, Typ, Pflicht, Standardwert, bei Auswahl die Optionen, bei Verknüpfung die Collection. Der Haken Design markiert Darstellungs-Parameter (Variante, Abstände, Farben); sie stehen im Formular in einer eigenen Gruppe Darstellung.
  • Slots – Stellen, an denen weitere Blöcke stehen dürfen, wahlweise beschränkt auf bestimmte Bausteine und eine Höchstzahl.

Mit dem Haken Als Baustein verwenden erscheint der Baustein in der Auswahl; in der Snippet-Liste trägt er das Abzeichen Baustein. Ein Beispiel, das Snippet hero:

<section class="hero hero--{{ variante|default:"hell" }}" id="{{ block.id }}">
  {% if bild %}{{ image(bild.id, "100vw") }}{% endif %}
  <h1>{{ titel }}</h1>
  {{ text }}
  {% if slots.buttons %}<div class="hero-actions">{% slot "buttons" %}{% endslot %}</div>{% endif %}
</section>

Dazu die Parameter titel (Text, Pflicht), bild (Bild), text (Markdown) und variante (Auswahl hell, dunkel, Design) sowie der Slot buttons (erlaubt: button, höchstens 3).

Im Baustein stehen die Parameter als Variablen bereit, schon passend aufbereitet:

Parameter-Typ Im Baustein
Text, Mehrzeiliger Text, URL, Auswahl, Datum, Zahl, Ja/Nein der Wert, wie üblich escaped
Markdown fertiges HTML – einfach {{ text }}
HTML das HTML unverändert
Bild, Datei Datei-Objekt wie file(id) mit url, srcset, width, height, alt, name … – z. B. {{ image(bild.id) }} oder {{ bild.url }}
Verknüpfung, Verknüpfungen der verknüpfte Eintrag bzw. die Liste (für Besucher nur veröffentlichte)

Dazu kommt block mit block.id und block.type. Einen Parameter vom Typ Blöcke gibt es nicht – verschachtelt wird über Slots. Die Slot-Namen müssen zu {% slot %} im Snippet passen (default ist der Standard-Slot); der Reiter warnt, wenn ein Slot nur im Schema oder nur im Code steht, und übernimmt Slots aus dem Code per Klick.

Blöcke auf einer Seite

Im HTML der Seite legst du fest, wo die Blöcke erscheinen:

{% include "header" %}
<main>
  {% blocks page.blocks %}
</main>
{% include "footer" %}

Steht der Tag im HTML oder hat die Seite schon Blöcke, zeigt der Seiten-Editor den Reiter Blöcke. Sonst fügt der Knopf Blöcke in der Werkzeugleiste des HTML-Tabs den Tag ein (am Cursor, sonst am Ende) und öffnet den Reiter.

So arbeitest du im Formular-Editor:

  • Block hinzufügen öffnet die Auswahl nach Kategorien mit Suche. In einem Slot stehen nur die dort erlaubten Bausteine.
  • Ein Klick auf die Titelzeile klappt den Block auf: das Formular nach Schema, darunter die Slots mit ihren eigenen Blöcken. Die Titelzeile zeigt den ersten ausgefüllten Text als Kurzinfo.
  • Hoch/Runter, Duplizieren und Löschen über die Knöpfe rechts. Ziehen an der Titelzeile verschiebt Blöcke innerhalb einer Liste und zwischen Slots, sofern der Baustein dort erlaubt ist.
  • Die Vorschau zeigt den ungespeicherten Stand. Ist der Reiter Blöcke offen, öffnet ein Klick auf einen Block in der Vorschau ihn im Formular; der gewählte Block ist in der Vorschau umrandet.

Gespeichert wird mit der Seite (Speichern, Strg+S) – mit Versionshistorie und Konfliktprüfung wie beim HTML.

Collections mit Blöcken

Der Feldtyp Blöcke macht ein Collection-Feld zum Seitenbaukasten, etwa den Inhalt eines Blogartikels. Im Eintragsformular erscheint dafür derselbe Formular-Editor; hat die Collection eine Detailseite, wählt auch hier ein Klick in der Vorschau den Block aus. Ausgegeben wird das Feld mit demselben Tag:

{% with post=item("blog", "slug", route.slug) %}
  <h1>{{ post.titel }}</h1>
  {% blocks post.inhalt %}
{% endwith %}

Im CSV-Export steht der Blockbaum als JSON-Text; beim CSV-Import werden ungültige Werte mit einer Warnung übergangen.

Globale Blöcke

Manche Blöcke gehören auf viele Seiten – ein Newsletter-Kasten, ein Kontakt-Teaser. Die legst du unter Snippets → Globale Blöcke an: Name, Beschreibung, Ordner und ein Blockbaum im selben Editor. Eingefügt werden sie über Block hinzufügen → Globale Blöcke; auf der Seite steht dann nur ein Verweis, und Änderungen am globalen Block wirken überall.

  • Einbetten ersetzt den Verweis durch eine Kopie seiner Blöcke. Danach bearbeitest du sie nur noch an dieser Stelle.
  • Neben dem Editor steht, wo der globale Block verwendet wird (Seiten, Einträge, andere globale Blöcke), mit Links dorthin.
  • Löschen geht nur, solange er nirgends verwendet wird.
  • Ein globaler Block darf sich nicht selbst enthalten, auch nicht über Umwege – das lehnt schon das Speichern ab.
  • In Slots, die nur bestimmte Bausteine erlauben, stehen globale Blöcke nicht zur Auswahl.
  • Parameter anpassen: Unter dem Verweis kannst du für diese eine Verwendung einzelne Parameter der Blöcke im globalen Block überschreiben – etwa die Überschrift des Newsletter-Kastens nur auf dieser Seite. Schalte dazu Parameter dieser Verwendung anpassen auf, setze bei dem Parameter den Haken anpassen und gib den neuen Wert ein; ohne Haken gilt weiter der Wert des globalen Blocks (und ändert sich mit ihm). Angepasst werden nur Parameter, nie Aufbau, Reihenfolge oder Slots. Entfernt jemand den Block oder Parameter später aus dem globalen Block, wird die Anpassung ignoriert.

Grenzen und Hinweise

  • Beim Speichern wird geprüft: unbekannter Baustein, fehlende Pflichtparameter, Werte vom falschen Typ, unbekannte Slots, nicht erlaubte Bausteine oder zu viele Blöcke in einem Slot. Die Meldung nennt die Stelle im Baum.
  • Höchstens 10 Ebenen tief und 1 MB je Blockbaum. Parameter, die das Schema nicht (mehr) kennt, fallen beim Speichern weg.
  • Ein Block, dessen Baustein gelöscht wurde, bleibt gespeichert, gibt auf der Live-Seite aber nichts aus; in der Vorschau erscheint ein Hinweis. Leere Felder geben nichts aus.
  • Bilder und Dateien in Blöcken zählen beim Aufräumen als verwendet. CSS/JS, das an einem Baustein-Snippet hängt, wird auf Seiten mit diesem Block geladen.
  • Die Markierungen für die Auswahl in der Vorschau (<!--ic-block:…-->) stehen nur in der Vorschau, nie auf der Live-Seite.
  • Für visuelle Editoren (IllusionPublish) markiert die Vorschau außerdem, welches Element welchen Parameter zeigt (data-ic-field). Das klappt automatisch, wenn ein Text-, Textbereich- oder Markdown-Parameter allein und unverändert in einem Element steht – also <h2>{{ ueberschrift }}</h2> und <div class="text">{{ inhalt }}</div> statt Überschrift und Markdown direkt nebeneinander in einem <section>; Bilder über image(bild.id) oder {{ bild.url }}. Wo das nicht passt (z. B. {{ titel|upper }}), markierst du das Element mit {{ block_field("titel") }}: <p{{ block_field("titel") }}>{{ titel|upper }}</p>. Live gibt es keine dieser Markierungen. Details in der API-Dokumentation.
  • Für Integrationen: blocks an Seiten, der Feldtyp blocks, GET /api/v1/blocks/types und /api/v1/global-blocks – Details in der API-Dokumentation.

Was nehme ich wofür?

Situation Werkzeug
Seiten, die andere ohne HTML aus fertigen Bausteinen zusammensetzen Bausteine + {% blocks page.blocks %}
Gleicher Header/Footer auf allen Seiten include (oder ein Layout)
Seitengerüst für viele ähnliche Seiten extends + block
Wiederholter Baustein mit eigenem Inhalt (Karte, Box, Akkordeon, Button) component + slot
Kleiner Formatierer ohne Inhalt Makro
Text, der danach unabhängig weiterentwickelt wird Snippet → Inhalt (Kopie)