Variables & Filters
Render dynamic content with #variable# syntax. Apply filters to transform output.
Basic Syntax
Variables are wrapped in # hash marks. They're replaced with their value at render time.
<h1>Hello, #name#!</h1>
<p>Your email is #user.email#</p>
Use dot notation to access nested values:
#user.email# <!-- object property -->
#order.items.0.name# <!-- array index -->
#settings.theme.color# <!-- deeply nested -->
Naming Rules
Variable names can contain letters, numbers, underscores, and dots (for nesting). They must start with a letter or underscore.
#my_variable# <!-- valid -->
#user.first_name# <!-- valid (dot = nesting) -->
#item_2# <!-- valid -->
#my-variable# <!-- NOT valid (hyphens not allowed) -->
snake_case for variable names — first_name, cart_count, is_admin.
Unresolved variables are preserved as-is in the output. They don't cause errors.
Special Variables
These variables are available automatically in every page:
| Prefix | Description | Example |
|---|---|---|
user.* | Authenticated user claims from JWT | #user.email#, #user.full_name#, #user.authenticated# |
session.* | Server-side session state (reactive) | #session.cart_count# |
env.* | Environment variables | #env.API_URL# |
query.* | URL query parameters | #query.page#, #query.search# |
flash.* | One-time flash messages (consumed on read) | #flash.success#, #flash.error# |
errors.* | Form validation errors | #errors.email# |
now | Current date and time (ISO format) | #now#, #now|date:"full"# |
_csrf | CSRF token for forms | #_csrf# |
#session.x# updates automatically when the value changes via w-set.
Default Values
Use the default filter to provide a fallback when a variable is empty or undefined:
#user.name|default:"Guest"#
#query.sort|default:"newest"#
#session.theme|default:"light"#
Without a default, unresolved variables stay as literal #variable# text in the output.
Arithmetic
Use arithmetic operators directly inside #...# expressions:
#price * 0.21# <!-- multiply -->
#session.age + 1# <!-- add -->
#total - discount# <!-- subtract -->
#count / 2# <!-- divide -->
#2 + 3 * 4# <!-- = 14 (precedence: * before +) -->
Arithmetic works in templates, session mutations, and <if> conditions:
<what>
session.tax = #session.price# * 0.21
session.total = #session.price# + #session.tax#
</what>
<p>Tax: #session.tax|round:2#</p>
Math Filters
| Filter | Description | Example |
|---|---|---|
round | Round to nearest integer | #tax|round# → 4 |
round:N | Round to N decimal places | #price * 0.21|round:2# → 2.10 |
ceil | Round up | #rating|ceil# → 4 |
floor | Round down | #rating|floor# → 3 |
Filters
Filters transform a variable's output. Add them after the variable name with a pipe |:
#name|uppercase#
#price|currency:"EUR"#
#bio|truncate:100#
Available Filters
| Filter | Description | Example |
|---|---|---|
uppercase | Convert to uppercase | #name|uppercase# → ALICE |
lowercase | Convert to lowercase | #name|lowercase# → alice |
capitalize | Capitalize first letter | #word|capitalize# → Hello |
title | Title Case every word | #text|title# → Hello World |
truncate:N | Truncate to N characters | #bio|truncate:50# |
count | String length | #name|count# → 5 |
number | Format with thousands separator | #total|number# → 1,234 |
currency | Format as currency (default USD) | #price|currency# → $9.99 |
currency:"EUR" | Currency with code | #price|currency:"EUR"# |
date | Format a date (default: medium) | #created|date# → Mar 15, 2025 |
date:"mask" | Format with custom mask | #created|date:"mmm d, yyyy"# |
round:N | Round to N decimal places | #tax|round:2# → 2.10 |
ceil | Round up to integer | #rating|ceil# → 4 |
floor | Round down to integer | #rating|floor# → 3 |
json | JSON-encode the value | #data|json# |
markdown | Render Markdown to HTML | #content|markdown# |
raw | Output without HTML escaping | #html_content|raw# |
pluralize | Pluralize based on count | #count|pluralize:"item","items"# |
replace | Replace substring | #text|replace:"old","new"# |
slice:start:end | Substring extraction | #text|slice:0:10# |
default:"val" | Fallback for empty values | #name|default:"Anonymous"# |
Date & Time Formatting
The date filter formats date strings using intuitive mask patterns. Use #now# to get the current date and time.
#now|date# <!-- Mar 7, 2026 -->
#now|date:"full"# <!-- Friday, March 7, 2026 -->
#now|date:"h:nn tt"# <!-- 2:30 PM -->
#created|date:"mmm d, yyyy"# <!-- Mar 15, 2025 -->
#event_date|date:"dddd, mmmm d"# <!-- Saturday, March 15 -->
Date Masks
| Mask | Output | Example |
|---|---|---|
d | Day, no leading zero | 5 |
dd | Day, leading zero | 05 |
ddd | Abbreviated day name | Mon |
dddd | Full day name | Monday |
m | Month, no leading zero | 3 |
mm | Month, leading zero | 03 |
mmm | Abbreviated month name | Mar |
mmmm | Full month name | March |
yy | 2-digit year | 26 |
yyyy | 4-digit year | 2026 |
Time Masks
| Mask | Output | Example |
|---|---|---|
h | 12-hour, no leading zero | 2 |
hh | 12-hour, leading zero | 02 |
H | 24-hour, no leading zero | 14 |
HH | 24-hour, leading zero | 14 |
n | Minutes, no leading zero | 5 |
nn | Minutes, leading zero | 05 |
s | Seconds, no leading zero | 8 |
ss | Seconds, leading zero | 08 |
t | A or P | P |
tt | AM or PM | PM |
Presets
Use a preset name instead of a mask for common formats:
| Preset | Equivalent | Example Output |
|---|---|---|
short | m/d/yy | 3/15/25 |
medium | mmm d, yyyy | Mar 15, 2025 |
long | mmmm d, yyyy | March 15, 2025 |
full | dddd, mmmm d, yyyy | Saturday, March 15, 2025 |
time | h:nn tt | 2:30 PM |
iso | yyyy-mm-dd | 2025-03-15 |
#created|date:"short"# <!-- 3/15/25 -->
#now|date:"time"# <!-- 2:30 PM -->
#event|date:"iso"# <!-- 2025-03-15 -->
m/mm always means month. Use n/nn for minutes. This avoids ambiguity in combined date+time masks like "m/d/yyyy h:nn tt".
Accepted Input Formats
The date filter accepts these input formats:
2025-03-15— ISO date2025-03-15T14:30:00— ISO datetime2025-03-15 14:30:00— datetime with space2025-03-15T14:30:00Z— RFC 3339
Filter Chaining
Chain multiple filters with additional pipes. They apply left to right:
#bio|truncate:100|uppercase#
#user.name|default:"Guest"|capitalize#
#price|currency:"USD"|replace:"$","USD "#
The first filter receives the raw variable value. Each subsequent filter receives the output of the previous one.
HTML Escaping
All variable output is HTML-escaped by default. Characters like <, >, &, and " are converted to their HTML entities.
<!-- If name = "<script>alert('xss')</script>" -->
<p>#name#</p>
<!-- Renders: <script>alert('xss')</script> -->
To output raw, unescaped HTML (for trusted content only), use the raw filter:
<div>#article.body|raw#</div>
|raw with content you trust. Never use it with user-submitted data — it bypasses XSS protection.
Computed Variables
Define computed variables in the <what> block to create derived values using string interpolation:
<what>
compute.greeting = "Hello, #user.name#!"
compute.profile_url = "/users/#user.id#/profile"
compute.full_title = "#title# | My Site"
</what>
<h1>#greeting#</h1>
<a href="#profile_url#">View profile</a>
Computed variables:
- Are available as
#name#(without thecompute.prefix) - Can reference any variable available in the page context
- Resolve in order, so later computed variables can reference earlier ones