Meridian handbook
Templates

Tags

Every tag a Meridian template can use (Pongo2's control flow, inheritance and include tags, and the engine's blocks, nav, component, slot, fragment, cache, announce, focus, patch, location and requireMember tags) with syntax, options and errors.

A tag is written {% name ... %}; most have a closing {% endname %}. This page lists every tag a site’s templates can use. engine-dmo template api prints the same list from the engine. A tag that does not exist, or one Meridian removes, is a compile error naming it.

Inheritance and reuse

extends and block

{% extends "base.html" %}
{% block title %}{{ entry.title }} | {{ block.Super }}{% endblock %}

{% extends "name" %} inherits a layout. {% block name %}...{% endblock %} declares a region in a layout and replaces it in a child; {{ block.Super }} prints the parent’s content of the block. Extending the name a file is itself resolved as reaches the next root on the site’s chain. See Inheritance.

include

{% include "partials/footer.html" %}
{% include "partials/card.html" with title=entry.title %}
{% include "partials/card.html" with title=entry.title only %}

Renders another template in place with the current context; with adds variables and only passes those alone, without the globals. engine-dmo check checks an included file on its own and reports a variable only with provides as unknown, so write a partial that needs arguments as a macro or a component.

macro and import

{% macro badge(text, tone="info") export %}<span class="badge badge--{{ tone }}">{{ text }}</span>{% endmacro %}

{% import "partials/macros.html" badge %}
{{ badge("New") }}

A macro is reusable markup with positional arguments and defaults. export makes it importable; {% import "file" name, other %} brings named macros into a template.

component and slot

{% component 'listing-card' listing=item size='large' %}

{% component 'listing-card' listing=item %}
  {% slot 'footer' %}<a href="{{ item.uri|url }}">{{ t('card.visit') }}</a>{% endslot %}
{% endcomponent %}

Calls the component components/<name>/<name>.html, held to its contract in components/<name>/component.yaml by engine-dmo check and at render. Props are name=value pairs. The call has a body, closed by {% endcomponent %}, exactly when the first thing after the tag is a {% slot %}. Inside a component’s markup, {% slot 'name' %}fallback{% endslot %} prints a filled slot. A component’s markup sees the page’s globals, its props, attrs and slots, never the caller’s own variables. See Components.

Control flow

if

{% if items|length > 2 and not devMode %}many{% elif items %}some{% else %}none{% endif %}

elif and else are optional. A value is false when it is empty, zero, false or nothing, and an empty field value is false.

ifequal, ifnotequal

{% ifequal entry.type "event" %}...{% else %}...{% endifequal %}

Equivalent to {% if a == b %} and {% if a != b %}.

for

{% for item in items %}...{% empty %}...{% endfor %}
{% for key, value in settings %}...{% endfor %}
{% for x in list reversed %}...{% endfor %}
{% for x in list sorted %}...{% endfor %}

{% empty %} renders when there is nothing to loop over. Inside the loop, forloop.Counter, Counter0, Revcounter, Revcounter0, First, Last and Parentloop. See Loops.

cycle

<tr class="{% cycle 'odd' 'even' %}">

Prints the next of its values on each pass of a loop.

ifchanged

{% for l in items %}{% ifchanged l.primaryTag.key %}<h2>{{ l.primaryTag.label }}</h2>{% endifchanged %}...{% endfor %}

Renders its body only when the value differs from the previous pass, for group headings in a sorted list.

firstof

{% firstof entry.subtitle entry.summary "" %}

Prints the first value that is not empty.

Variables

set

{% set places = query('listing').limit(6).all() %}

Binds a name for the rest of the current scope.

with

{% with total=pagination.total per=pagination.per %}{{ total }} / {{ per }}{% endwith %}
{% with form = enquiryForm('contact', entry) %}...{% endwith %}

Binds names for its body only.

Output

filter

{% filter upper %}shout{% endfilter %}

Applies filters to the body’s output.

spaceless

{% spaceless %}<p> <b>x</b> </p>{% endspaceless %}

Removes whitespace between tags in the body: <p><b>x</b></p>.

templatetag

{% templatetag openvariable %} name {% templatetag closevariable %}

Prints a delimiter literally: openblock, closeblock, openvariable, closevariable, openbrace, closebrace, opencomment, closecomment.

widthratio

<div class="bar" data-width="{% widthratio score 100 300 %}"></div>

Scales a value to a whole number: value / max * width.

lorem

{% lorem 2 p %}

Placeholder text for a layout in progress: a count, then w (words), p (paragraphs) or b (plain blocks).

comment

{% comment %}Several lines that never render.{% endcomment %}

Content

blocks

{% blocks entry.body %}

Renders each item of a blocks field through blocks/<block key>.html on the site’s chain, with the item as block, its position as blockIndex and the number of items as blockCount, beside every global and the page’s entry. It takes one argument, the field value. A block declared in the schema with no template anywhere on a site’s chain fails engine-dmo check. See Block templates.

<nav aria-label="{{ t('nav.main') }}"><ul>
{% nav item in nav.main %}
  <li{% if item.isActive %} class="active"{% endif %}>
    <a href="{{ item.url|url }}"{% if item.isCurrent %} aria-current="page"{% endif %}>{{ item.label }}</a>
    {% ifchildren %}<ul>{% children %}</ul>{% endifchildren %}
  </li>
{% endnav %}
</ul></nav>

{% nav item in list %} renders its body once per item, with the item bound to the name given. Inside the body, {% children %} renders the same body again for the current item’s children, one level deeper, and {% ifchildren %}...{% endifchildren %} renders only when the current item has children. It works over any list of maps with a children list: a navigation slot (nav.main), or your own tree. A tree deeper than 32 levels is an error.

requireMember

engine-dmo

This feature is part of the DMO bundle (engine-dmo).
{% requireMember %}
<p>{{ member.name }}</p>

Everything after the tag is for a signed-in member, who is then readable as member (id, name). A visitor who is not signed in is redirected (303) to the portal’s sign-in (/members/login unless the portal’s path is configured otherwise) with next= set to return to the page. A member’s response is sent with Cache-Control: private, no-store. The tag takes no arguments. In engine-cms, using it is a check error naming the portal module. See The member portal.

Caching

cache

{% cache 'listing-card' key=listing.id ttl='1h' %}...{% endcache %}
{% cache 'facet-counts' key=[locale.code, page.uri] ttl='10m' vary=['query'] %}...{% endcache %}

Caches its rendered body across requests on public pages. The name is a quoted literal of lower case letters, digits and hyphens, starting with a letter. The options:

OptionValue
keyAn expression, or a list of them, that distinguishes one copy from another
versionAn expression; a new value starts a new copy
ttlHow long a copy lives: a duration such as '90s', '10m', '1h', or '1d' for days. At most seven days
vary'locale', 'path' or 'query', or a list of them. The locale is always part of the key

A copy is cleared when the content its body read is published, without any invalidation code. Outside a public page (a preview, a fragment request, mail) the body renders every time. See Caching.

Interactive pages

These tags work with the engine’s fragment requests and sigmx, the small attribute-driven script the engine uses for in-place updates. They are introduced on Forms.

fragment

{% fragment 'results' %}...{% endfragment %}
{% fragment 'results-list' tag='ol' class='cards' aria-label=t('results.list') %}...{% endfragment %}

Names a region of a page. It renders in place inside a wrapper element with id="f-<name>" and data-fragment="<name>" (a div, or the element tag= names; any other option is an attribute of the wrapper). A request that names fragments in its Meridian-Fragment header gets those regions alone, rendered with the page’s full context. The name is a quoted literal of lower case letters, digits and hyphens, at most 64 characters; a name may render only once per page, and engine-dmo check refuses one declared twice (duplicate-fragment). engine-dmo render <path> --fragment results renders only that fragment.

announce and focus

{% announce 'Saved' %}
{% announce t('form.errors', errors._count) assertive %}
{% focus '#form-errors' %}

announce tells screen readers about a change: in a response to an in-place update it is an event the page’s live region reads out; on a full page load it prints a status message where it stands (role="status", or role="alert" with assertive). focus moves focus to a selector after an in-place update, and prints nothing on a full page.

Patch tags

Only in a declared endpoint’s response template, which composes its own update of the page:

TagWhat it does
{% patchelements selector='#results' mode='inner' %}...{% endpatchelements %}Patch the elements: by the body’s ids, or those selector= matches, with mode= outer (the default), inner, replace, prepend, append, before or after
{% patchsignals searching=false count=results.total %}Merge values into the page’s signals, as name=value pairs (a name may be dotted, form.error=nil), or one expression whose value is a map
{% removeelements '#empty-state' %}Remove the elements a selector matches
{% removesignals 'form.draft', 'form.error' %}Remove signals by path

A template that contains a patch tag anywhere other than an endpoint’s response is a check error (patch-in-page), as is content outside a {% patchelements %} in one (patch-outside). To an ordinary request, patchelements prints its body where it stands and the others print nothing.

location and replaceurl

{% location path('listing') replace=true delay=900 %}
{% replaceurl path('/search') %}

location navigates after an in-place update: to a local path, or to another origin only with external=true (a literal URL on another origin without it is a check error, location-external). replace=true replaces the history entry and delay= waits that many milliseconds. replaceurl changes the address bar to a local path with no navigation.

Removed

{% now %}, {% ssi %} and, unless the site opts in with templates.allow_unescaped, {% autoescape %} are not available: Usage of tag 'now' is not allowed. Use now and the date filters for the time.