Developer docs  /  Themes

Fluid filters reference

Before the filter list, three renderer facts you need to know:

Output is HTML-encoded by default. Templates render with HtmlEncoder.Default(ThemeV2RenderService.RenderAsync), so every {{ expression }} is escaped. Anything that emitsmarkup — icon SVGs from attr_icon, the layout's content_for_layout / theme_css / theme_js— must go through the built-in raw filter:

{{ Attribute.Name | attr_icon | raw }}
<style>{{ theme_css | raw }}</style>
<script>{{ theme_js | raw }}</script>
<main>{{ content_for_layout | raw }}</main>

Resource limits. Tenant-authored templates run with MaxRecursion = 100 andMaxSteps = 100_000 (see BuildTemplateOptions). A runaway {% for x in (1..100000) %} will becut off rather than pinning a CPU. Keep loops bounded.

Section settings are plain values. Section/block settings arrive as JSON blobs; a valueconverter unwraps Newtonsoft JTokens and System.Text.Json.JsonElements into plain CLR typesbefore Fluid sees them, so Section.Settings.itemsPerPage, arrays likeSection.Settings.categoryIds, and nested objects all iterate and compare normally.

Filters are registered per request in RegisterFilters(TemplateOptions options); the memberaccess strategy (which theme context types are visible to templates) is a static singleton.

Custom Fluid filters

All custom filters are tolerant of null / missing / non-numeric input — a broken value renders asafe fallback rather than throwing, so theme previews don't blow up on incomplete data.

dollar

Signature: {{ value | dollar }}Behaviour: Parses the input as a decimal (invariant culture) and formats it as"$" + v.ToString("#,##0.00"). Anything unparseable (null, empty, text) renders $0.00.

  • 165Output: $165.00
  • 1234.5Output: $1,234.50
  • null / "" / "abc"Output: $0.00

<span class="space-card__price-amount">{{ item.Price | dollar }}</span>

The origin theme deliberately relies on the $0.00 fallback — a priceless space must notcollapse the price block (see the comment in snippets/space-card.fluid). There is also asnippets/money.fluid wrapper: {{ amount | dollar }}.

plural

Signature: {{ noun | plural }}Behaviour: Light English pluraliser. s/x/ch/sh endings get es; consonant + ybecomes ies; everything else gets s. Case-insensitive ending checks.

  • SpaceOutput: Spaces
  • BoxOutput: Boxes
  • FacilityOutput: Facilities
  • BusOutput: Buses

<a href="/t/spaces">Browse {{ Organisation.SpaceName | plural }}</a>

Note the templates special-case Annual themselves because Annual | plural would give"Annuals": {% if Space.DurationType == "Annual" %}Years{% else %}{{ Space.DurationType | plural }}{% endif %}.

multiply

Signature: {{ a | multiply: b }}Behaviour: Emits a * b rounded to 1 decimal place. If either operand fails to parse,emits an empty string (unlike the built-in times, which does no rounding).

  • {{ 3.2 | multiply: 2.5 }}Output: 8
  • {{ 2.33 | multiply: 3 }}Output: 7 (6.99 → 7)
  • {{ "abc" | multiply: 2 }}Output: `` (empty)

<span>{{ Space.Width | multiply: Space.Length }}m<sup>2</sup></span>

The space card computes the same area with built-ins instead({{ item.Width | times: item.Length | round: 0 }}) — whole square metres on the card, onedecimal on the detail page.

cycle_abbrev

Signature: {{ durationType | cycle_abbrev }}Behaviour: Maps a billing cycle name to its short suffix. Mirrors the public site's AngularCycleAbbrevPipe; the default is always mo (never a dangling raw value).

  • week, weeklyOutput: w
  • fortnight, fortnightlyOutput: fn
  • day, dailyOutput: d
  • year, yearly, annual, annuallyOutput: yr
  • quarter, quarterlyOutput: qtr
  • halfyearly, half-yearlyOutput: 6mo
  • anything else (incl. month)Output: mo

<span class="space-card__price-cycle">/{{ item.DurationType | cycle_abbrev }}</span>

theme.js carries the same map twice (the quote modal's cycleAbbrev object and theCYCLE_MONTHS normalisation table) — if you add a new duration type, update all three.

attr_icon

Signature: {{ attributeName | attr_icon | raw }}Behaviour: Keyword-matches the (lowercased) attribute name against the keywords map inschema/attribute-icons.json and returns the matching SVG string from its icons map. Firstkeyword hit wins. Falls back to the schema's default icon key, and if the schema is missing ormalformed, to a hard-coded generic check-mark SVG. Because it returns markup, it must be pipedthrough raw.

{{ "Drive-up access" | attr_icon | raw }}
{# → the "car" SVG, because the keywords map routes names containing "drive" to it #}

The maps are loaded once per render by LoadAttributeIconMaps and stitched onto both the page andlayout contexts (the filter runs in both render passes).

attr_icon_key

Signature: {{ iconKey | attr_icon_key | raw }}Behaviour: Direct lookup of an icon SVG by its key in schema/attribute-icons.json'sicons object (e.g. lock, thermometer, car). No keyword matching, no fallback — an unknownkey renders empty. Use this when the admin explicitly chose an icon; use attr_icon forautomatic matching by attribute name.

The precedence pattern from snippets/attribute-tile.fluid:

{% if iconBlock and iconBlock.Settings.image and iconBlock.Settings.image != "" %}
<img class="attribute-card__img" src="{{ iconBlock.Settings.image }}" alt="" loading="lazy">
{% elsif iconBlock and iconBlock.Settings.icon and iconBlock.Settings.icon != "" %}
{{ iconBlock.Settings.icon | attr_icon_key | raw }}
{% else %}
{{ Attribute.Name | attr_icon | raw }}
{% endif %}

safe_video_url

Signature: {{ url | safe_video_url }}Behaviour: Whitelist for <iframe src>. The URL must be absolute, https, and its host mustbe one of: youtube.com, www.youtube.com, youtube-nocookie.com, www.youtube-nocookie.com,youtu.be, vimeo.com, player.vimeo.com, dailymotion.com, www.dailymotion.com, loom.com,www.loom.com. Anything else — including plain http:// — renders about:blank.

<iframe src="{{ Section.Settings.videoUrl | safe_video_url }}" width="100%" height="100%"
frameborder="0" allowfullscreen loading="lazy"
sandbox="allow-scripts allow-same-origin" referrerpolicy="no-referrer"></iframe>

Never put a tenant-supplied URL into an iframe without this filter.

external_url

Signature: {{ url | external_url }}Behaviour: For social/outbound links. http(s)://, mailto: and tel: URLs pass throughuntouched; a bare domain gets https:// prepended (leading slashes stripped). Empty input rendersempty.

  • facebook.com/mybrandOutput: https://facebook.com/mybrand
  • https://x.com/mybrandOutput: https://x.com/mybrand
  • mailto:hi@example.comOutput: mailto:hi@example.com

{% assign facebookUrl = Footer.FacebookUrl | default: Settings.FacebookUrl %}
{% if facebookUrl %}<a href="{{ facebookUrl | external_url }}" target="_blank" rel="noopener">…</a>{% endif %}

storefront_url

Signature: {{ url | storefront_url }}Behaviour: For internal menu URLs, which are stored site-relative (/spaces). Thestorefront serves pages under /t/, so:

  • /spaces/t/spaces (any leading-/ path not already under /t/ or /account)
  • /account/... and absolute URLs pass through untouched
  • a bare phone number (0412 345 678) becomes tel:0412345678
  • a bare email (hi@example.com) becomes mailto:hi@example.com
  • empty input → /t/

<a href="{{ item1.Url | storefront_url }}">{{ item1.Label }}</a>

Use this on every menu-item URL (header, footer, top bar) — never hand-prefix /t/ onto storedmenu URLs.

dateformat

Signature: {{ date | dateformat }}Behaviour: Parses the input as a DateTimeOffset and formats it dd/MM/yyyy; unparseableinput renders empty. Registered but currently unused by the origin theme — available forcustom themes.

{{ Space.AvailableFrom | dateformat }} {# → 07/07/2026 #}

asset_url

Signature: {{ "file.ext" | asset_url }}Behaviour: Emits the public theme-asset path: /api/public/theme-v2/assets/{path}. Note theorigin theme doesn't use it for theme.css/theme.js — those are inlined into the layout astheme_css / theme_js (see §4). Use asset_url for any additional asset files your theme ships.

<img src="{{ 'hero-fallback.jpg' | asset_url }}" alt="">
{# → <img src="/api/public/theme-v2/assets/hero-fallback.jpg" alt=""> #}

json

Signature: {{ obj | json }}Behaviour: Serialises any Fluid value back to JSON via System.Text.Json. Intended forinlining server data into <script> blocks. Registered but currently unused by the origin theme.

<script>window.__SPACE__ = {{ Space | json | raw }};</script>

(You need raw here too — without it the quotes get HTML-encoded.)

Built-in Fluid/Liquid filters the theme relies on

Fluid ships the standard Liquid filter set. These are the ones the origin theme actually uses —real usages shown so you can copy the idiom:

  • timesWhat it does: Multiply (no rounding) — Real usage: {{ item.Width | times: item.Length | round: 0 }}m² — card area (snippets/space-card.fluid)
  • roundWhat it does: Round to N decimals — Real usage: {{ Space.Width | round: 1 }}m × {{ Space.Length | round: 1 }}m
  • splitWhat it does: String → array on a separator — Real usage: {% assign placeParts = listingUrl | split: "query_place_id=" %} then {{ placeParts[1] | split: "&" | first }} — extracting a Google place id (sections/google-reviews.fluid)
  • url_encodeWhat it does: Percent-encode for URLs — Real usage: <a href="/t/locations/{{ Space.LocationName | url_encode }}/spaces">
  • sliceWhat it does: Substring / sub-array — Real usage: {{ item.Settings.name | slice: 0, 2 | upcase }} — two-letter avatar initials (sections/testimonials.fluid)
  • upcase / downcaseWhat it does: Case conversion — Real usage: per {{ item.DurationType | downcase }}
  • defaultWhat it does: Fallback when nil/empty — Real usage: {{ Settings.Design.primaryColor | default: Settings.PrimaryColor | default: "#2563eb" }} — the layout's CSS-variable cascade
  • plusWhat it does: Numeric add — also the standard trick to coerce a string setting to a numberReal usage: {% assign avg = Section.Settings.averageRating | plus: 0 %} then {% if avg >= 4.5 %} (sections/google-reviews.fluid)
  • replaceWhat it does: String replace — Real usage: {{ fontParts[1] | replace: ' ', '+' }} — building a Google Fonts URL (layouts/main.fluid / embed.fluid)
  • firstWhat it does: First element — Real usage: {{ placeId | split: "&" | first }}
  • appendWhat it does: String concat — Real usage: used in layouts/main.fluid for meta/canonical URL assembly
  • stripWhat it does: Trim whitespace — Real usage: partials/announcement-bar.fluid
  • rawWhat it does: Suppress HTML encoding — Real usage: required for every filter/value that returns markup (see §1)

Also useful, not filters: the .size property ({% if Spaces.size > 0 %}an empty list istruthy in Fluid, so always compare size, per the comment in snippets/space-listing.fluid),the contains operator ({% if listingUrl contains "query_place_id=" %}), forloop.first /forloop.index, and {% capture %} for reusable HTML blocks.