Expressions & Syntax
Full reference for Unblock's expression syntax.
Unblock uses Twig expression syntax — widely used and well-documented.
Accessing Data
Use dot notation to access properties:
{{ post.title }}
{{ post.author.name }}
{{ site.name }}
Bracket notation for array elements or dynamic properties:
{{ array[0] }}
{{ array['key'] }}
Slice a string or a list with [offset:length]; either side can be left out:
{{ post.title[0:10] }} <!-- First 10 characters -->
{{ items[1:2] }} <!-- 2 items from the second -->
{{ items[:3] }} <!-- First 3 items -->
A dot is already safe on a missing value: post.author.name gives nothing when there's no author. ?. is accepted too and works the same:
{{ post.author?.name }}
{{ post.thumbnail?.src }}
Method calls with arguments:
{{ post.date('F j, Y') }}
{{ user.avatar({size: 96}) }}
{{ post.meta('field_name') }}
After a filter, a dot or brackets read the filter's result, as in Twig:
{{ post.categories|first.name }} <!-- Name of the first category -->
{{ get_posts({posts_per_page: 3})|last.title }}
Conditional Logic
Ternary Operator
{{ condition ? 'yes' : 'no' }}
{{ post.thumbnail ? 'Has image' : 'No image' }}
{{ user.id ? user.name : 'Guest' }}
{{ user.id ? 'Welcome!' }} <!-- Empty if guest -->
Without :, as in Twig, the value is an empty string when the condition is false: a ? b is a ? b : '', anywhere in an expression, such as (user.id ? 'Welcome') ~ '!'.
Fallback Operators
| Operator | Name | Returns fallback when |
|---|---|---|
?? | Null coalescing | Value is null |
?: | Elvis | Value is falsy (null, false, 0, '', [], a query without results) |
|default | Default filter | Value is empty (like ?: but preserves 0) |
{{ post.meta('subtitle') ?: 'No subtitle' }}
{{ post.meta('hero_image', {transform_value: true}) ?: post.thumbnail ?: '/default.jpg' }}
{{ post.comment_count ?: 'No comments' }}
{{ post.meta('subtitle')|default('No subtitle') }}
{{ request.get('page') ?? 1 }}
A missing custom field is an empty string, not null, as in WordPress and Timber: use ?: or |default to replace it. ?? suits values that are null when missing, such as a request parameter.
Comparison:
| Value | ?? | ?: | |default |
|---|---|---|---|
'text' | 'text' | 'text' | 'text' |
null | fallback | fallback | fallback |
false | false | fallback | fallback |
0 | 0 | fallback | 0 |
'' | '' | fallback | fallback |
[] | [] | fallback | fallback |
Operators
Arithmetic
| Operator | Description | Example |
|---|---|---|
+ | Addition | {{ 5 + 3 }} → 8 |
- | Subtraction | {{ 10 - 3 }} → 7 |
* | Multiplication | {{ 4 * 5 }} → 20 |
/ | Division | {{ 20 / 4 }} → 5 |
% | Modulo | {{ 17 % 5 }} → 2 |
// | Floor division | {{ 17 // 5 }} → 3 |
** | Power | {{ 2 ** 3 }} → 8 |
.. | Range | {{ (1..5)|join(', ') }} → 1, 2, 3, 4, 5 |
As in Twig, - also negates a value ({{ -post.meta('offset') }}), and subtracts without spaces too: {{ loop.length-1 }}.
Comparison
| Operator | Description | Example |
|---|---|---|
== | Equal | {{ a == b }} |
!= | Not equal | {{ a != b }} |
=== | Identical | {{ a === b }} |
!== | Not identical | {{ a !== b }} |
< | Less than | {{ a < b }} |
> | Greater than | {{ a > b }} |
<= | Less or equal | {{ a <= b }} |
>= | Greater or equal | {{ a >= b }} |
<=> | Spaceship | {{ a <=> b }} |
Logical
| Operator | Description | Example |
|---|---|---|
and | Logical AND | {{ a and b }} |
or | Logical OR | {{ a or b }} |
xor | Logical exclusive OR (true when exactly one side is true) | {{ a xor b }} |
not | Logical NOT | {{ not a }} |
String & Containment
| Operator | Description | Example |
|---|---|---|
~ | Concatenation (cast to string) | {{ 'Hello ' ~ name }} |
+ | Concatenation, when a side is text | {{ post.title + ' - ' + site.name }} |
starts with | Starts with test | {{ post.title starts with 'Hello' }} |
ends with | Ends with test | {{ post.slug ends with '-draft' }} |
in | Contains | {{ 'a' in ['a', 'b'] }} |
not in | Not contains | {{ 'c' not in ['a', 'b'] }} |
+ adds numbers and numeric strings, as in Twig: a custom field holding 5 plus 1 gives 6. With text on either side, it concatenates. ~ always concatenates.
Tests
Tests check a property of a value with is, or is not to negate:
| Test | Description | Example |
|---|---|---|
is defined | Value is not null | {{ user.id is defined }} |
is empty | Value is empty (null, false, '', [], a query without results) | {{ post.meta('subtitle') is empty }} |
is null / is none | Value is exactly null | {{ post.thumbnail is null }} |
is even | Number is even | {{ loop.index is even }} |
is odd | Number is odd | {{ loop.index is odd }} |
is iterable | Value can be looped over | {{ post.tags is iterable }} |
is sequence | Value is a list (sequential array) | {{ items is sequence }} |
is mapping | Value is a map (keyed array or object) | {{ data is mapping }} |
is divisible by(n) | Number divides evenly by n | {{ loop.index is divisible by(3) }} |
is same as(x) | Strictly identical, type and value | {{ post.id is same as(1) }} |
Negate any test with is not:
{{ post.thumbnail is not empty }}
{{ loop.index is not divisible by(2) }}
Operator Precedence
From lowest to highest priority, in Twig 4's order (Twig 3 gives the same results, except in the combinations it deprecates):
?— Ternary??,?:— Fallbackor— Logical ORxor— Logical exclusive ORand— Logical AND==,!=,===,!==,<,>,<=,>=,<=>,in,not in,starts with,ends with— Comparison..— Range~— Concatenation+,-— Addition / Subtraction*,/,%,//— Multiplication / Divisionnot— Logical NOTis,is not— Tests**— Power (right-associative)|— Filter-,+— Negation, except before**: as in Twig,-2 ** 2is-(2 ** 2), so-4; write(-2) ** 2for4.,[]— Attribute access, also after a filter
{{ (a + b) * c }}
{{ (value ?? 'default')|upper }}
{{ (post.comment_count ?: 0) + 1 }}
{{ post.title|upper == 'HELLO' }} <!-- Filter binds before comparison -->
{{ 'Total: ' ~ price + 1 }} <!-- Adds before joining -->
{{ not post.meta('subtitle') is empty }} <!-- not (… is empty) -->
Literals
{{ 'single quotes' }} {{ "double quotes" }}
{{ 42 }} {{ 3.14 }} {{ -10 }}
{{ true }} {{ false }} {{ null }}
{{ [1, 2, 3] }} {{ ['a', 'b', 'c'] }}
{{ {key: 'value'} }} {{ {name: 'John', age: 30} }}
none is an alias of null; true, false, null and none also read in capitals (TRUE, FALSE, NULL, NONE), as in Twig. Printed, true gives 1 and false nothing.
Arrow Functions
Used with higher-order filters like map, filter, sort, reduce, and find:
{{ items|map(p => p.name) }}
{{ items|filter(p => p.active) }}
{{ items|find(p => p.id == 5) }}
{{ items|sort((a, b) => a.date <=> b.date) }}
{{ items|reduce((carry, item) => carry + item.price, 0) }}
{{ posts|filter(p => p.post_status == 'publish' and p.author.id == user.id) }}
As in Twig, map, filter and find pass the item's key as a second parameter, and reduce as a third:
{{ prices|map((price, name) => name ~ ': ' ~ price)|join(', ') }}
{{ prices|reduce((carry, price, name) => carry ~ name ~ ' ', '') }}
Named Arguments
As in Twig, an argument can be given by its name instead of its position. The arguments before it can then be left out, and keep their default:
{{ post.timestamp|date(timezone: 'Europe/Paris') }} <!-- Site date format, Paris time -->
{{ items|join(', ', and: ' and ') }}
{{ post.content|excerpt(num_words: 20) }}
{{ post.meta(field_name: 'subtitle') }}
Positional arguments come first, named ones after. The names are those of Twig and Timber, listed on each filter, function and field page; name = value works too, as in Twig. A name the filter does not have is ignored. An options object, such as post.excerpt({words: 25}), stays one argument: its keys are not argument names.
Escaping & Output
All output is automatically escaped — you never need to think about it. UnBlock combines two layers of protection:
- Field-aware: each field knows its type. Text fields use
esc_html, URL fields likepost.linkuseesc_url, and HTML fields likepost.contentare already processed by WordPress. - Context-aware: URL attributes like
hrefandsrcare escaped withesc_url, regardless of the field used. The attributes you set on a block are escaped withesc_attr.
{{ post.title }} <!-- esc_html -->
{{ post.link }} <!-- esc_url everywhere -->
{{ post.content }} <!-- Pre-processed HTML, safe to output -->
{{ '<b>Sale</b>' }} <!-- Prints the tags as text -->
HTML written inside an expression, even in a string, is escaped and prints as text. Put the markup in your blocks and the data in the expression.
A list prints nothing: turn it into text first, for example with join. A block attribute whose whole value is one expression is removed when the expression gives null.
Comments are not rendered in the output:
{# This is a comment #}
Error Handling
Missing properties return an empty string — no errors. Properties are safely navigated even through null:
{{ post.nonexistent }} <!-- Returns '' -->
{{ post.author.name }} <!-- Safe even if author is null -->
An expression holds on one line and up to 500 characters. A longer one, or one with a syntax error, prints nothing.
Watch operator precedence when combining filters with fallback operators. {{ value ?? 'default'|upper }} applies upper to 'default' only — not to the whole result. Use parentheses: {{ (value ?? 'default')|upper }}.
Reserved Keywords
Cannot be used as variable names: true, false, null, none and the same in capitals, and, or, xor, not, in, is.
A variable named like a provider, such as post or site, hides that provider. Inside a loop, the loop's names and loop hide a variable of the same name. See Variables.
Next steps
- Your First Expression — learn the basics step by step
- Providers — all available data fields
- Filters — transform and format data
- Functions — built-in utility and WordPress functions
- Common Patterns — ready-to-use template examples