Manual · 7

Snippets, components and layouts

Snippets are reusable template building blocks. There are four ways to use them – from simple inclusion to a component with slots.

7.1 Including with include

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

The Snippet sees all of the page’s variables plus the values passed in. Changes to the Snippet take effect immediately on all pages that include it. Snippet names may contain slashes, e.g. teile/footer. A new Snippet with such a name automatically ends up in the folder teile in the list. You can also set the folder freely next to the name; it’s only for organization.

7.2 Components with slots

A component is a Snippet that can wrap content – like components in Vue or Svelte, only on the server side. The Snippet uses {% slot %} to define where the content appears; when using it, you fill the slots.

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

Using it:

{% 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 %}

The rules:

Element Meaning
{% slot %}…{% endslot %} Default slot in the Snippet. The content between the tags is the fallback if nothing is passed.
{% slot "name" %}…{% endslot %} Named slot with fallback.
{% fill "name" %}…{% endfill %} Fills a named slot when using the component.
Content outside of fill Fills the default slot.
slots.name / slots.default In the Snippet: true if the slot was filled – for optional wrappers.
name=wert after the Snippet name Parameters (props), available as variables in the Snippet.

Good to know:

  • The content sees the page’s variables. Whatever is between component and endcomponent is rendered in the context of the calling page – loop variables work:

    {% for post in data("blog") %}
      {% component "card" %}
        {% fill "titel" %}{{ post.titel }}{% endfill %}
        {{ post.text|markdown }}
      {% endcomponent %}
    {% endfor %}
    
  • The Snippet sees only its parameters plus the global variables (page, route, nav(), data() …), but no local variables of the page. Whatever the component needs, you pass explicitly as a parameter. This makes components predictable and reusable.

  • HTML as a parameter stays HTML if it is marked as safe: text=post.text|markdown or inhalt="<b>x</b>"|safe. Normal texts are escaped as usual.

  • Nesting is allowed – components inside components, also in fill blocks:

    {% component "card" %}
      {% fill "titel" %}Kontakt{% endfill %}
      {% component "button" href="/kontakt" variante="primaer" %}Schreib uns{% endcomponent %}
    {% endcomponent %}
    
  • The Snippet name can also come from a variable: {% component block.typ %} – useful for page builders based on Collection data.

  • A component that calls itself endlessly is stopped after 30 levels with an error message.

Snippets with slots carry the Component badge in the Snippet list; in the HTML editor, Snippet → Component inserts the basic structure with all named slots.

7.3 Layouts with extends and block

A layout defines the scaffolding of a whole page; the page only fills its blocks. The Snippet layouts/standard:

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

In a page’s HTML:

{% extends "layouts/standard" %}

{% block inhalt %}
  <h1>{{ page.title }}</h1>
  <p>…</p>
{% endblock %}
  • extends must be at the very beginning. Everything outside of block is ignored.
  • Blocks that aren’t overridden keep the content from the layout (here, the standard sidebar).
  • Each page has exactly one layout. For repeated building blocks within a page, use components.

Page layouts for new pages

A layout snippet can offer itself as a template for new pages in the snippet editor under the Page layout tab: tick Offer as page layout, add a name and description for the selection and optionally Preselect for new pages. The New page dialog then offers the page layouts next to Blank page (the previous behaviour); a new page with a layout only contains

{% extends "layouts/inhaltsseite" %}

Changes to the layout therefore affect all pages using it, and individual pages can override blocks as shown above. Next to each layout, IllusionCMS shows whether it has a content area for blocks – i.e. whether it outputs {% blocks page.blocks %}, directly or via extends, include and components. Layouts without such an area carry the chip static content: the page shows fixed content from the template, and blocks cannot be maintained there. This is a normal, intended case – for example a contact page with address and form.

Blocks are not inserted automatically, so that you keep full control over the page’s code. It can be intentional to choose layouts that have no editable content areas.

Integrations such as IllusionPublish read the page layouts via GET /api/v1/page-layouts and create pages with layout_id (API documentation).

7.4 Macros

For small building blocks with parameters but no content, there are macros. In the Snippet makros (the export is mandatory):

{% 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 most cases, components are more flexible because they can take HTML content.

7.5 CSS and JS on Snippets

In the Snippet editor, on the right under CSS & JS of this snippet, you attach files from the library (+ attach) or create new ones (+ new, name with .css or .js). They load only on pages that actually execute the Snippet during rendering – so the styles and scripts of a slider don’t load on every page, only where the slider is.

  • What counts is what is actually rendered: {% include %} (also with names from a variable), components, nested Snippets and layouts via extends. In an {% if %} branch that doesn’t apply, or in an empty loop, nothing loads.
  • Macro Snippets load their files as soon as they are included via {% import %}.
  • Avoid endless recursion: Recursive macros and includes (e.g. for tree views) are allowed but always need a termination condition. Endless recursion – including a include with a variable that includes itself – terminates the server process, as do extremely large values for {% lorem %} and the filters center, ljust, rjust and floatformat. When testing new macros, start with small values.
  • Only page content and the Head field count – a Snippet in the SEO title or in the description loads nothing.
  • CSS goes in the <head>, JS before </body>, in each case after the page’s files. Each file loads only once – even if the Snippet appears ten times or the page already has the same file.
  • The preview in the page editor shows the saved state of the Snippet.

Ready-built widgets from Svelte, Vue, React & co.: You build the module in a separate project (e.g. with Vite) and take over the result from dist/:

From the build Becomes in IllusionCMS
the markup from the <body> of index.html, usually just the mount point (<div id="app"></div>) the content of the Snippet
assets/index-….js a JS file on the Snippet (+ new → e.g. wetter.js, paste the content)
assets/index-….css a CSS file on the Snippet

Then you include the Snippet ({% include "wetter" %}) – CSS and JS load only there. Vite generates the JS for <script type="module">. If the file contains module syntax (import/export at the top level, or import.meta), IllusionCMS includes it that way automatically; without module syntax, as a normal <script> – both without any setting.

So that the build works without rework:

  • One JS file: If Vite splits the code into several files (import(…) for lazy loading, a separate vendor chunk), the others are missing – the file lives at /_assets/js/<id>.js, and relative imports find nothing there. Leave out lazy loading or set build: { rollupOptions: { output: { inlineDynamicImports: true } } } in vite.config.js.
  • Images and fonts from the build: either small enough for inlining (build.assetsInlineLimit), or upload them under Files and use them with an absolute address (/media/…).
  • Several times on one page: The script loads only once. So a build that starts in #app starts only once. For several instances, search for all placeholders in the entry point, e.g. with 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 } })
})

The Snippet then delivers <div class="wetter-widget" data-ort="{{ ort|default:"Berlin" }}"></div> – values from the template get into the widget via data-… attributes (output with {{ … }} is escaped automatically).

  • Modules only run once the page has been parsed – the placeholders are already in the DOM by then.

7.6 Page builder: building blocks and blocks

For pages that others should maintain without HTML, there is the page builder: you build building blocks once (Snippets with a schema), and the page or a collection entry only stores data – a list of blocks, each with a building block, values and, where needed, nested blocks. The HTML stays in your Snippets; the form editor never touches markup.

Creating a building block

A building block is a Snippet with a schema. In the Snippet editor, open the Building block tab, click Define as building block and set:

  • Label, description, icon and category – this is how the building block appears in Add block.
  • Parameters – structured like the fields of a collection: name, label, type, required, default value, the options for Choice, the collection for Link. The Design checkbox marks appearance parameters (variant, spacing, colours); in the form they sit in a separate Appearance group.
  • Slots – places where further blocks may go, optionally limited to certain building blocks and a maximum number.

With Use as building block ticked, the building block appears in the selection; in the Snippet list it carries the Building block badge. An example, the 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>

With the parameters titel (text, required), bild (image), text (Markdown) and variante (choice hell, dunkel, design), plus the slot buttons (allowed: button, at most 3).

Inside the building block, the parameters are available as variables, already prepared:

Parameter type In the building block
Text, multi-line text, URL, choice, date, number, yes/no the value, escaped as usual
Markdown finished HTML – simply {{ text }}
HTML the HTML unchanged
Image, file file object like file(id) with url, srcset, width, height, alt, name … – e.g. {{ image(bild.id) }} or {{ bild.url }}
Link, links the linked entry or the list (only published ones for visitors)

In addition there is block with block.id and block.type. There is no parameter of type Blocks – nesting works through slots. Slot names must match {% slot %} in the Snippet (default is the default slot); the tab warns when a slot exists only in the schema or only in the code, and takes slots over from the code with one click.

Blocks on a page

In the page HTML you decide where the blocks appear:

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

If the tag is in the HTML or the page already has blocks, the page editor shows the Blocks tab. Otherwise the Blocks button in the HTML tab’s toolbar inserts the tag (at the cursor, otherwise at the end) and opens the tab.

Working in the form editor:

  • Add block opens the selection by category, with search. Inside a slot, only the building blocks allowed there are offered.
  • Clicking the title bar expands the block: the form according to the schema, below it the slots with their own blocks. The title bar shows the first filled-in text as a summary.
  • Up/down, duplicate and delete via the buttons on the right. Dragging the title bar moves blocks within a list and between slots, as long as the building block is allowed there.
  • The preview shows the unsaved state. While the Blocks tab is open, clicking a block in the preview opens it in the form; the selected block is outlined in the preview.

It is saved together with the page (Save, Ctrl+S) – with version history and conflict check just like the HTML.

Collections with blocks

The field type Blocks turns a collection field into a page builder, for example the content of a blog article. The entry form shows the same form editor; if the collection has a detail page, clicking in the preview selects the block here too. The field is output with the same tag:

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

In the CSV export the block tree is written as JSON text; the CSV import skips invalid values with a warning.

Global blocks

Some blocks belong on many pages – a newsletter box, a contact teaser. Create them under Snippets → Global blocks: name, description, folder and a block tree in the same editor. You insert them via Add block → Global blocks; the page then only holds a reference, and changes to the global block apply everywhere.

  • Embed replaces the reference with a copy of its blocks. From then on you edit them only in this place.
  • Next to the editor you see where the global block is used (pages, entries, other global blocks), with links.
  • Deleting is only possible as long as it is not used anywhere.
  • A global block must not contain itself, not even indirectly – saving already refuses that.
  • In slots that only allow certain building blocks, global blocks are not offered.
  • Adjust parameters: Below the reference you can override individual parameters of the blocks inside the global block for this one usage – for example the heading of the newsletter box on this page only. Open Adjust parameters of this usage, tick adjust at the parameter and enter the new value; without the tick the value of the global block applies (and changes with it). Only parameters can be adjusted, never structure, order or slots. If the block or parameter is later removed from the global block, the adjustment is ignored.

Limits and notes

  • Saving checks for: unknown building block, missing required parameters, values of the wrong type, unknown slots, building blocks that are not allowed, or too many blocks in a slot. The message names the position in the tree.
  • At most 10 levels deep and 1 MB per block tree. Parameters the schema does not know (anymore) are dropped when saving.
  • A block whose building block was deleted stays stored but outputs nothing on the live site; the preview shows a notice. Empty fields output nothing.
  • Images and files in blocks count as used for Clean up. CSS/JS attached to a building block Snippet is loaded on pages using that block.
  • The markers for selecting in the preview (<!--ic-block:…-->) only exist in the preview, never on the live site.
  • For visual editors (IllusionPublish) the preview also marks which element shows which parameter (data-ic-field). This works automatically when a text, text area or Markdown parameter stands alone and unchanged in an element – i.e. <h2>{{ ueberschrift }}</h2> and <div class="text">{{ inhalt }}</div> instead of heading and Markdown right next to each other in one <section>; images via image(bild.id) or {{ bild.url }}. Where this does not fit (e.g. {{ titel|upper }}), mark the element with {{ block_field("titel") }}: <p{{ block_field("titel") }}>{{ titel|upper }}</p>. None of these markers exist on the live site. Details in the API documentation.
  • For integrations: blocks on pages, the field type blocks, GET /api/v1/blocks/types and /api/v1/global-blocks – details in the API documentation.

What do I use for what?

Situation Tool
Pages that others assemble from ready-made building blocks without HTML Building blocks + {% blocks page.blocks %}
Same header/footer on all pages include (or a layout)
Page scaffolding for many similar pages extends + block
Repeated building block with its own content (card, box, accordion, button) component + slot
Small formatter without content Macro
Text that will be developed independently afterwards Snippet → Content (copy)