Practical Mirage Template Syntax for Layout Control
The boundary of the template layer
Mirage is intended for HTML and text presentation. It can rearrange sections, modify cards and headings, add decorative containers, control classes, and render prepared lists and localized labels. A template must not execute SQL, determine permissions, modify data, or replace module-level route validation. If a required field is absent from the input context, a layout change alone cannot create it reliably.
Public templates are stored in skins/<Layout>/<Skin>/<Module>/, administrative templates in adminskins/..., and moderator templates in moderatorskins/.... Identify the active Layout and Skin before editing. The same filename may exist in several trees, and changing an inactive copy will not affect the page.
Escaped output
The primary and safest output form uses double curly braces:
<h1>{{ Context.Table("page").Data("title") }}</h1>
<p>{{ Context.Table("page").Data("summary") }}</p>Text is HTML-escaped, so characters from the data do not become tags or attributes. Use this form for titles, names, URLs in attributes, labels, search strings, and any value that a user could have entered.
Raw output is marked with an exclamation point:
{{! Former.BuildHTML(Context.Table("form").Data("submit_ir")) }}Use it only for a source that already owns a safe HTML contract, such as a control produced by Former, an Echelon zone, or pre-serialized JSON-LD. Do not switch ordinary output to raw merely to make a tag stored in a user field work. That would turn data into active markup.
Named Context tables
Use Context.Table for new templates. One page can receive meta, page, assets, and module tables with a __ prefix. The main calls are:
Context.HasTable("items")checks whether a table exists;Context.Table("items").Data("title")reads a field from the current row;RowCount()returns the number of rows;Rewind()resets the cursor;NextRow()advances to the next row.
The compatible Table.Data(...) syntax remains in older single-table templates, but do not mix it with named context without a reason.
Conditions, values, and comments
{% if Context.Table("page").Data("status") == "published" %}
<span class="badge badge--live">{{ Boardwords("status_published") | default("Published") }}</span>
{% elif Context.Table("page").Data("status") == "draft" %}
<span class="badge badge--draft">{{ Boardwords("status_draft") | default("Draft") }}</span>
{% else %}
<span class="badge">{{ Boardwords("status_unknown") | default("Unknown") }}</span>
{% endif %}Flags often arrive as the strings "1" and "0", so compare them explicitly. Use {% set name = value %} for a template variable. A comment has the form {# comment #} and is not included in the HTML.
Two forms are available for fallback values:
{{ Context.Table("meta").Data("title") ?? "Untitled" }}
{{ Boardwords("catalog_title") | default("Catalog") }}A ternary expression is convenient for a short class value; use a regular if for larger sections:
{{ Context.Table("page").Data("compact") == "1" ? "card--compact" : "card--wide" }}A safe row loop
A named table does not automatically become an array of row objects. Use the complete cursor pattern:
{% if Context.Table("products").RowCount() > 0 %}
{% set __ = Context.Table("products").Rewind() %}
<div class="product-grid">
{% for _ in range(0, Context.Table("products").RowCount(), 1) %}
{% if Context.Table("products").NextRow() %}
<article class="product-card{% if loop.even %} product-card--even{% endif %}">
<h3>{{ Context.Table("products").Data("title") }}</h3>
<p>{{ Context.Table("products").Data("summary") }}</p>
</article>
{% endif %}
{% endfor %}
</div>
{% else %}
<p>{{ Boardwords("products_empty") | default("Nothing here yet") }}</p>
{% endif %}The order matters: check the rows, call Rewind(), iterate over the range, and call NextRow() before reading Data(). Inside the loop, loop.index, loop.first, loop.last, loop.odd, and loop.even are available.
Localization, forms, and zones
Render static user-facing labels through Boardwords with a clear fallback. This preserves translatability when a design is copied between skin trees. If a form field is supplied as *_ir, do not assemble the input manually:
<label>{{ Boardwords("profile_name") | default("Profile name") }}</label>
{{! Former.BuildHTML(Context.Table("profile").Data("name_ir"), "class", "profile-form__input") }}Mirage controls the container, grid, and class, while Former owns the HTML control. Echelon Zone content is also rendered raw only because the zone is a trusted fragment producer; an arbitrary table field has no such status.
Verifying a change
- Make a narrow change in the active template without copying the entire shell.
- Check the page with data, an empty list, and missing optional fields.
- Test special characters in text and attributes.
- Check all available languages and Boardwords fallbacks.
- Test desktop and mobile, and validation errors for an administrative form.
- Confirm that raw output is used only for trusted HTML producers.
If the template requires a new permission, SQL query, or business rule, stop: the task has crossed the Mirage boundary. The correct extension prepares the data in the module first, while the template remains a predictable presentation layer.
Page composition and dynamic fragments
Edit the common shell, navigation, and application-module content separately. Do not move the entire shell into a card template; that creates duplication and breaks skin switching. For a fragment updated by a filter, pager, or tab, use the established AJAX contour with Mirage. Keep canonical public text that matters for first render and SEO in the normal page render.
If a template uses an additional callback-pack namespace, for example for date formatting, verify that the pack is present in the target runtime. Such a callback is a dependency of that template. Provide a clear fallback where the design permits it, and do not attempt to imitate a missing callback with complex string expressions.
Safe review practice
Compare the resulting HTML before and after the change, not just the .mirage source. Check balanced tags, unique ids, accessible labels, valid links, and the absence of nested interactive elements. Test lists with one and several rows because cursor errors may be invisible with a single record. When moving a template between skin trees, recheck table and field names; visually similar modules are not required to receive identical context.