Skip to main content

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​

OperatorNameReturns fallback when
??Null coalescingValue is null
?:ElvisValue is falsy (null, false, 0, '', [], a query without results)
|defaultDefault filterValue 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'
nullfallbackfallbackfallback
falsefalsefallbackfallback
00fallback0
''''fallbackfallback
[][]fallbackfallback

Operators​

Arithmetic​

OperatorDescriptionExample
+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​

OperatorDescriptionExample
==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​

OperatorDescriptionExample
andLogical AND{{ a and b }}
orLogical OR{{ a or b }}
xorLogical exclusive OR (true when exactly one side is true){{ a xor b }}
notLogical NOT{{ not a }}

String & Containment​

OperatorDescriptionExample
~Concatenation (cast to string){{ 'Hello ' ~ name }}
+Concatenation, when a side is text{{ post.title + ' - ' + site.name }}
starts withStarts with test{{ post.title starts with 'Hello' }}
ends withEnds with test{{ post.slug ends with '-draft' }}
inContains{{ 'a' in ['a', 'b'] }}
not inNot 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:

TestDescriptionExample
is definedValue is not null{{ user.id is defined }}
is emptyValue is empty (null, false, '', [], a query without results){{ post.meta('subtitle') is empty }}
is null / is noneValue is exactly null{{ post.thumbnail is null }}
is evenNumber is even{{ loop.index is even }}
is oddNumber is odd{{ loop.index is odd }}
is iterableValue can be looped over{{ post.tags is iterable }}
is sequenceValue is a list (sequential array){{ items is sequence }}
is mappingValue 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):

  1. ? — Ternary
  2. ??, ?: — Fallback
  3. or — Logical OR
  4. xor — Logical exclusive OR
  5. and — Logical AND
  6. ==, !=, ===, !==, <, >, <=, >=, <=>, in, not in, starts with, ends with — Comparison
  7. .. — Range
  8. ~ — Concatenation
  9. +, - — Addition / Subtraction
  10. *, /, %, // — Multiplication / Division
  11. not — Logical NOT
  12. is, is not — Tests
  13. ** — Power (right-associative)
  14. | — Filter
  15. -, + — Negation, except before **: as in Twig, -2 ** 2 is -(2 ** 2), so -4; write (-2) ** 2 for 4
  16. ., [] — 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 like post.link use esc_url, and HTML fields like post.content are already processed by WordPress.
  • Context-aware: URL attributes like href and src are escaped with esc_url, regardless of the field used. The attributes you set on a block are escaped with esc_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.

Common mistake

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​