---
title: "Template Syntax · RUAL Documentation"
description: "Values, conditions, loops, translations, comments and every filter of the web template engine: money, dates, numbers, text and lists."
canonical: https://docs.rual.nl/web/templates
language: en
---

# Template Syntax

Web templates are Mustache, the same syntax as mail templates and page templates, with the platform's additions: `{{name}}` writes a value, `{{if}}` and `{{for}}` decide and repeat, and `{{price | money "EUR"}}` formats. A template that does not compile is an error the block reports, not an empty page.

In short

- A value is escaped for where it stands in your markup. Triple braces write it as it is.

- A name that is not there prints nothing and is not an error. A filter that does not exist is an error when the template compiles.

- A template sees the fields of `data`, every custom pin by name, and the `web` object.

## Values are escaped unless you ask otherwise

| Write | Does |
| --- | --- |
| `{{name}}`, `{{user.email}}` | A value, escaped for where it stands (see [Escaping and Security](https://docs.rual.nl/web/security#where-a-value-lands)). A missing name prints nothing. |
| `{{.}}`, `{{this}}` | The current item itself, for a list of plain values. |
| `{{{html}}}`, `{{& html}}` | A value written as it is. Only for HTML you built yourself, such as the output of [`web_foreach`](https://docs.rual.nl/block-types/web/web_foreach). |
| `{{items.0.name}}` | An item of a list by its position, counting from 0. |
| `{{items.length}}`, `{{name.length}}` | How many items a list has, or how many characters a text has. |
| `{{@root.title}}` | A name read from the outermost data, past any loop or section. |
| `{{! a comment }}` | A comment. It leaves nothing in the page. |
| `\{{` | Literal braces. |

A name is looked up from the innermost context outward, the Mustache way, so inside a section the outer values are still visible. Numbers print without a needless `.0`, and a list or object prints as JSON. Changing the delimiters with `{{=...=}}` is not supported.

## Conditions compare and combine

```
{{if status == 'paid'}}
  <span class="ok">Paid</span>
{{else if status == 'open' && total > 0}}
  <span class="warn">Open</span>
{{else}}
  <span>Nothing to pay</span>
{{/if}}
```

- **Operators:** `==` `!=` `>` ` =` `<=`, combined with `&&`, `||` and `!`.

- **Literals:** `'text'`, `"text"`, numbers, `true`, `false` and `null`.

- **Filters work in a condition:** `{{if items | length > 3}}`.

- **Closing:** `{{/if}}`, `{{end}}`, or the name for a single value, as in `{{if user}} ... {{/user}}`. `{{elseif c}}` is the same as `{{else if c}}`.

- **A comparison section** also works: `{{#n == 2}} ... {{/n == 2}}`, and `{{^n == 2}}` for its opposite.

A bare value counts as false when it is missing, `null`, `false`, `0`, an empty text or an empty list. Anything else counts as true, including an empty object and the text `"0"`.

Numbers compare as numbers, also when one side is a text that holds a number, so `{{if qty > 3}}` works on a form field. Two texts compare as text, so `"10" > "9"` is false: compare with a number when you mean a number.

> **A branch must end where it began** Open and close a tag inside one branch. ` ` does not compile, because the branch ends in the middle of an attribute. Write ` `. [What refuses to compile](https://docs.rual.nl/web/security#what-refuses-to-compile) has the rule.

## Loops repeat, and know where they are

```
{{for product in products}}
  <li class="{{if @odd}}odd{{/if}}">{{@number}}. {{product.name}}</li>
{{else}}
  <li>Nothing here yet.</li>
{{/for}}

{{#tags}}<span>{{.}}</span>{{/tags}}
{{^tags}}<span>No tags</span>{{/tags}}

{{for tags}}<span>{{.}}</span>{{/tags}}
```

| Write | Does |
| --- | --- |
| `{{for item in items}} ... {{else}} ... {{/for}}` | A loop with a name for the item. `{{else}}` renders when the list is empty. Also closes with `{{end}}`. |
| `{{for v in object}} ... {{/for}}` | The values of an object, in the order of their keys. `{{@key}}` is the key. |
| `{{#items}} ... {{/items}}` | A Mustache section: a list repeats and each item becomes the context, an object becomes the context, a true value shows the contents. It may hold `{{else}}`. |
| `{{^items}} ... {{/items}}` | An inverted section: shows when the value counts as false. |
| `{{for items}} ... {{/items}}` | The section form, as the platform's older templates write it. Closes with `{{/for}}` too. |

Inside a named loop, read the item by its name. `{{.}}` there is the context around the loop, not the item.

| Inside a loop | Is |
| --- | --- |
| `{{@index}}` | The position, counting from 0. |
| `{{@number}}` | The position, counting from 1. |
| `{{@first}}`, `{{@last}}` | True on the first or the last item. |
| `{{@odd}}`, `{{@even}}` | True on odd or even positions, counted from `{{@index}}`: the first item is even. |
| `{{@length}}` | How many items there are. |
| `{{@key}}` | The key, when looping over an object. |

## Partials print another template

| Write | Does |
| --- | --- |
| `{{> product-card}}` | Another template by name, with the current data. |
| `{{> product-card p}}` | The same, with `p` as its context. |
| `{{define "badge"}} ... {{/define}}` | A small template local to this one, used with `{{> badge}}`. |

A partial may stand only where text goes: not inside a tag, an attribute or a ` `. [Layouts and Partials](https://docs.rual.nl/web/layouts-and-partials) covers storing them.

## Translations come from your translation keys

| Write | Does |
| --- | --- |
| `{{t "cart.items" count=cart.count}}` | A translation in the page's language; `{count}` in the text is filled in and escaped. |
| `{{t key}}`, `{{key \| t}}` | The same, with the key read from a value. |
| `{{translate}}cart.title{{/translate}}` | The platform's older form. |

[Languages](https://docs.rual.nl/web/languages#translations) explains the fallback order and where the keys live.

## Filters change a value before it is written

A filter follows a pipe: `{{price | money "EUR"}}`. They chain from left to right, and an argument may be a value too: `{{price | times qty | money "EUR"}}`. Naming a filter that does not exist is an error when the template compiles, so a typo never reaches a visitor as an empty value.

### Numbers, money and dates

Numbers and money follow the page's language, so the same template writes `€ 1.234,50` on the Dutch page and `€1,234.50` on the English one.

| Filter | Example | Writes |
| --- | --- | --- |
| `money "EUR"` | `{{1234.5 \| money "EUR"}}` | `€1,234.50` in English, `€ 1.234,50` in Dutch, `1.234,50 €` in German, `1 234,50 €` in French. Without a code it is EUR. JPY, KRW, ISK and HUF have no decimals, and a currency without a symbol prints its code. |
| `number` | `{{1234.5 \| number}}` | `1,234.5`: thousands grouped, up to 6 decimals as the number has them. |
| `number 2` | `{{1234.5 \| number 2}}` | `1,234.50`: a fixed number of decimals, 0 to 10. |
| `date` | `{{created \| date}}` | `2026-03-04`. Reads Unix seconds, milliseconds or an ISO date, and writes in the entity's time zone. |
| `date "DD-MM-YYYY HH:mm"` | `{{created \| date "D/M/YY"}}` | `4/3/26`. Tokens: `YYYY YY MM M DD D HH H mm ss`. |
| `plus`, `minus` | `{{qty \| plus 1}}` | Arithmetic. A value that is not a number, or a division by 0, gives an empty value. |
| `times`, `divide` | `{{cents \| divide 100 \| money "EUR"}}` | For amounts held in cents, as `money` takes whole units. |
| `round`, `round 2` | `{{1234.567 \| round 2}}` | `1234.57`. Without an argument it rounds to a whole number. |

### Text

| Filter | Example | Writes |
| --- | --- | --- |
| `upper`, `lower` | `{{name \| upper}}` | `ADA LOVELACE` |
| `capitalize` | `{{name \| capitalize}}` | `Ada lovelace`: the first letter only. |
| `trim` | `{{name \| trim}}` | Whitespace taken off both ends. |
| `truncate 80` | `{{text \| truncate 9}}` | `The quick…`. A second argument replaces the `…`: `{{text \| truncate 9 "..."}}`. |
| `default "-"` | `{{nickname \| default "-"}}` | The fallback when the value counts as false. That includes `0`: `{{count \| default "-"}}` writes `-` for 0. |
| `nl2br` | `{{description \| nl2br}}` | The text escaped, with line breaks as ` `. No triple braces needed. |
| `slug` | `{{title \| slug}}` | `héllo-wörld` for `Héllo Wörld!`: lower case, with dashes between words. |
| `urlencode` | `{{name \| urlencode}}` | `ada+lovelace`. A value after a `?` in a link is already encoded for you, so adding this there encodes it twice. |
| `t` | `{{status_key \| t}}` | The translation of a key held in a value. |

### Lists and data

| Filter | Example | Writes |
| --- | --- | --- |
| `length` | `{{items \| length}}` | How many items a list or object has, or the characters in a text. Also in conditions: `{{if items \| length > 3}}`. |
| `join ", "` | `{{tags \| join " / "}}` | `a / b / c`. Without an argument the separator is a comma and a space. |
| `first`, `last` | `{{tags \| first}}` | The first or the last item. |
| `json` | `{{cart \| json}}` | The value as JSON. A value in a ` ` is written as JSON anyway, so you need this only to show JSON as text. |

## The variables a template sees

The fields of the `data` pin come first, then every custom pin by its name, which wins over a field of the same name. On top of that, [`web_render`](https://docs.rual.nl/block-types/web/web_render) adds the document's own values for the page template and its layout.

| Name | Is | Available in |
| --- | --- | --- |
| Your custom pins and `data` fields | Whatever you wire in. | Every template |
| `web.language` | The language this render runs in. | Every template |
| `web.nonce` | The response's nonce, for inline scripts. | Every template |
| `web.url` | The page's URL without its query. | Every template |
| `web.path`, `web.origin` | The path alone, and the scheme and host alone. | Every template |
| `web.action` | The name of the running action, empty on a page request. | Every template |
| `web.signed_in` | Whether the visitor is signed in. | Every template |
| `title`, `description` | The render block's pins, unless `data` or a custom pin already has that name. | Page and layout |
| `nonce`, `language` | The same as `web.nonce` and `web.language`. | Page and layout |
| `viewport` | `width=device-width, initial-scale=1` | Page and layout |
| `head`, `tags` | Where the generated head tags go. Print with `{{{head}}}`. | Page and layout |
| `body` | The rendered page. Print with `{{{body}}}`. | Layout |

A [`web_template`](https://docs.rual.nl/block-types/web/web_template) or a [`web_foreach`](https://docs.rual.nl/block-types/web/web_foreach) sees your values and `web` only, so write `{{web.nonce}}` and `{{web.language}}` there. Partials see whatever the template around them sees.

## Whitespace and limits

- A block tag standing alone on a line leaves no blank line behind, as in Mustache.

- One render may write at most 16 MB. Partials may nest 48 levels deep, so a partial that includes itself fails with a message and does not hang.

- An error names its line and column, for example `line 2 col 1: this block is never closed`.

## Next steps

- [Layouts and Partials](https://docs.rual.nl/web/layouts-and-partials): Store a template once and use it on every page.

- [Escaping and Security](https://docs.rual.nl/web/security): How a value is written in each place, and what refuses to compile.

- [Lists and Tables](https://docs.rual.nl/web/lists): When to use a web for each block and when a template loop.

## Frequently asked

**Which template syntax do RUAL web pages use?**

Mustache, the same syntax as mail templates, plus named loops with for and else, conditions that compare values with if, partials, translations with t, and filters. A template that does not compile is an error the block reports, not an empty page.

**How do I format a price or date in a RUAL web template?**

With a filter: {{price | money "EUR"}} or {{created | date "DD-MM-YYYY"}}. Numbers and money follow the page language, so the same template writes the amount the Dutch way on the Dutch page and the English way on the English one. A date writes the digits you ask for, in the entity's time zone.
