Developer docs  /  Themes

The render context

Templates render twice: the page pass gets ThemeV2RenderContext, then the layout passgets ThemeV2LayoutContext, which carries the same properties plus IsEmbed,content_for_layout, theme_css and theme_js. Sections and snippets included from a pagetemplate see the page context; header/footer partials included from the layout see the layoutcontext. Property names are used as-is in templates ({{ Organisation.Name }},{{ Section.Settings.heading }} — PascalCase for model members, whatever casing your schemauses for settings keys).

Top-level properties

  • Organisation — Type: ThemeV2OrgContext — What it is: The tenant.
  • Page — Type: ThemeV2PageContext — What it is: Route metadata.
  • Settings — Type: ThemeV2GlobalSettings — What it is: Theme-wide settings, with per-page SEO already overlaid (§4.2).
  • Sections — Type: List<ThemeV2SectionInstance> — What it is: The ordered section list for this page key (§5).
  • Locations — Type: List<ThemeV2LocationData> — What it is: Active, non-hidden locations.
  • Categories — Type: List<ThemeV2CategoryData> — What it is: All categories across locations — narrowed to the filtered location's categories when a location filter is active.
  • Spaces — Type: List<ThemeV2SpaceData> — What it is: Listing data (see scope table above).
  • Space — Type: ThemeV2SpaceData — What it is: Single space on detail pages; null elsewhere.
  • Location — Type: ThemeV2LocationData — What it is: Single location on location detail; null elsewhere.
  • Request — Type: ThemeV2RequestContext — What it is: Path, base URL, validated filters, embed flag.
  • CmsPages — Type: List<ThemeV2CmsPage> — What it is: All active CMS pages (for nav/footer link lists).
  • CmsPage — Type: ThemeV2CmsPage — What it is: The current CMS page when rendering one; null elsewhere.
  • Menus — Type: List<ThemeV2Menu> — What it is: Navigation menus.
  • Footer — Type: ThemeV2FooterContent — What it is: Builder-edited footer content (may be null on old rows — guard).
  • Header — Type: ThemeV2HeaderContent — What it is: Builder-edited header content (may be null — guard).
  • Attributes — Type: List<ThemeV2AttributeData> — What it is: Org-wide attribute catalogue: distinct attributes across active spaces, with a SpaceCount each.

Layout-only additions:

  • IsEmbed — What it is: True for /embed/... or ?embed=1 renders.
  • content_for_layout — What it is: The pre-rendered page HTML — emit with {{ content_for_layout | raw }}.
  • theme_css — What it is: Contents of assets/theme.css — inline into a <style> with | raw.
  • theme_js — What it is: Contents of assets/theme.js — inline into a <script> with | raw.

(__attrIconMap / __attrKeywordMap / __attrDefaultIcon also ride both contexts — internalplumbing for the attr_icon filters, not for template use.)

The nested classes, field by field

Organisation (ThemeV2OrgContext) — Id, Name, Host, Logo, MainImage,SpaceName. SpaceName is the tenant's terminology for a space ("Unit", "Bay"…), read from thefirst location's settings, defaulting to "Space". Use it everywhere you would write the word"space": Browse {{ Organisation.SpaceName | plural }}.

Page (ThemeV2PageContext) — Slug, Title, Template. On space detail pages Titleis the resolved space's name (so <title>/og:title never degrade to the generic "Space").

Settings (ThemeV2GlobalSettings) — the big one:

  • Legacy branding (typed): PrimaryColor, SecondaryColor, FontFamily, LogoUrl, FaviconUrl. Kept as render fallbacks for orgs saved before the schema-driven design system; V1 theme branding is also merged into these when they are empty.
  • Design (dictionary): the schema-driven design tokens, keyed by the setting ids declared in schema/settings.json — e.g. Settings.Design.primaryColor, Settings.Design.colorMode, Settings.Design.bodyFont, Settings.Design.cornerRadius. Free-form: add a new control to settings.json and read Settings.Design.yourId in the layout — no C# change needed.
  • SEO: MetaTitle, MetaDescription, MetaKeywords, OgImage, OgTitle, OgDescription, CanonicalUrl, SiteName. By the time a template sees these, the renderer has already overlaid, in order: global settings → CMS page meta → the page's own SEO settings from the builder. CanonicalUrl is fully resolved (a global value is treated as a base-domain override with the request path appended; a page-level value is a full URL).
  • Indexing: AllowIndexing (default true), AutoSitemap, ExcludePrivatePages.
  • AI crawlers (AiCrawlers.GoogleExtended/Openai/Anthropic/Perplexity/TrainingBots, PublishLlmsTxt) — drive the generated robots.txt, not templates.
  • Structured data (Schema.Enabled/BusinessName/Phone/Address/OpeningHours/PriceRange/GbpUrl/IncludeReviews/IncludeFaq).
  • Analytics: GoogleTagManagerId, GoogleAnalyticsId. Forms: RecaptchaSiteKey.
  • Social: FacebookUrl, InstagramUrl, LinkedInUrl, TwitterUrl, YoutubeUrl, TiktokUrl.
  • Announcement bar: AnnouncementEnabled, AnnouncementText, AnnouncementLink, AnnouncementBackground (builder-edited Header values win over these when set).
  • Sales channel: ShowBookOnline, ShowGetQuote, HidePricing.
  • Custom code: CustomCssCode (emit in a <style>; </style> is stripped at save time), CustomHeadHtml (raw head HTML), plus CustomCss/CustomMeta dictionaries.

Sections[] (ThemeV2SectionInstance) — Id (GUID string), Type (section file name,e.g. "hero"), Order, IsVisible, HideOnMobile, Settings (dictionary — schema-hydrated),Blocks (list of ThemeV2BlockInstance: Id, Type, Settings).

Locations[] (ThemeV2LocationData) — Id, Name, Address (single joined string),PhoneNumber, EmailAddress, Image, Latitude, Longitude, Categories.

Categories[] (ThemeV2CategoryData) — Id, Name, Icon, Price, DurationType,Width, Height, Length.

Spaces[] / Space (ThemeV2SpaceData) — every field:

  • Id, UId — Notes: UId is the GUID to link with: /t/spaces/{{ item.UId }}.
  • Name, Icon
  • CategoryId, CategoryUId, CategoryName
  • Price — Notes: The space's price (not the category's — category price drifts when a space price is overridden).
  • DurationType — Notes: Billing cycle as a string ("Month", "Week"…) — pipe through cycle_abbrev.
  • Status — Notes: Space status string ("Vacant"…).
  • Width, Height, Length, DoorHeight — Notes: Dimensions (nullable doubles).
  • AllowOnlineBookings — Notes: Space and location toggles combined — gates the Book Online CTA.
  • LocationId, LocationName
  • Medias — Notes: Image URIs, ordered.
  • Attributes — Notes: Attribute names.
  • AttributeIds, AttributeUIds — Notes: Parallel id lists (used for filter links / promo matching).
  • PromotionLabel — Notes: The winning promotion's description (or name) — always set when a promo applies.
  • SpacePromotionPrice — Notes: Admin-set promotional price on the space row; wins over the computed amount.
  • PromotionPrice — Notes: Effective discounted price per cycle (nullable).
  • PromotionRuleType, PromotionRequiredAmount, PromotionDurationType — Notes: Rule metadata for "first N months" style badges.
  • HasPromotion — Notes: Convenience: PromotionLabel is non-empty.

Request (ThemeV2RequestContext) — Path (normalised, e.g. /spaces/{uid}), BaseUrl(https://{org host}), AssetsBaseUrl, FilterLocationId, FilterLocationName,FilterCategoryId, FilterCategoryName, FilterAttributeNames, IsEmbed. Filter values arevalidated — a ?location= id that isn't one of the org's locations comes through as null.

CmsPages[] / CmsPage (ThemeV2CmsPage) — Slug, Title, Content (HTML),IsActive, MetaTitle, MetaDescription.

Menus[] (ThemeV2Menu) — Name, Title, IsActive, Items[](ThemeV2MenuItem: Label, Url, IsActive, Children[] — one level of nesting).Menu URLs are stored site-relative; pipe them through storefront_url.

Footer (ThemeV2FooterContent) — LogoUrl, LinkColumns, BusinessName, Address,Phone, Email, FacebookUrl, InstagramUrl, LinkedInUrl, TwitterUrl, YoutubeUrl.

Header (ThemeV2HeaderContent) — HideLoginButton (inverted so a null header keeps thelogin button: {% unless Header.HideLoginButton %}), LoginLabel, TopBarEnabled (nullable),AnnouncementEnabled/AnnouncementText/AnnouncementLink/AnnouncementBackground (nullable"never set" semantics — the renderer resolves the announcement precedence before templates run).

Attributes[] (ThemeV2AttributeData) — Id, Name, SpaceCount (active spaces carryingthe attribute — lets tiles show "12 spaces" and skip empty attributes).

Custom Fluid filters

Registered in ThemeV2RenderService.RegisterFilters — available in every template, on top ofthe standard Liquid set:

  • dollar — Example: {{ item.Price | dollar }} — Behaviour: $1,234.50; null/garbage → $0.00.
  • storefront_url — Example: {{ menuItem.Url | storefront_url }} — Behaviour: Prefixes site-relative paths with /t (leaves /account and absolute URLs alone); turns bare phone numbers into tel: and bare emails into mailto:.
  • external_url — Example: {{ Settings.FacebookUrl | external_url }} — Behaviour: Adds https:// to bare domains; passes http(s)/mailto/tel through.
  • dateformat — Example: {{ someDate | dateformat }} — Behaviour: dd/MM/yyyy.
  • asset_url — Example: {{ "hero.jpg" | asset_url }} — Behaviour: Emits /api/public/theme-v2/assets/hero.jpg — serves org-uploaded builder assets (blob organisations/{orgId}/theme-v2/assets/…), not theme files.
  • json — Example: {{ Spaces | json }} — Behaviour: Serialises any value to JSON — for inlining server data into <script>.
  • plural — Example: {{ Organisation.SpaceName | plural }} — Behaviour: Light English pluraliser (space→spaces, bay→bays, box→boxes).
  • cycle_abbrev — Example: {{ item.DurationType | cycle_abbrev }} — Behaviour: week→w, fortnight→fn, day→d, year→yr, quarter→qtr, halfyearly→6mo, default mo.
  • multiply — Example: {{ item.Width | multiply: item.Length }} — Behaviour: Numeric multiply, rounded to 1 decimal.
  • attr_icon — Example: {{ attrName | attr_icon }} — Behaviour: Keyword-matches the attribute name against schema/attribute-icons.json and emits the matching inline SVG; falls back to a check icon.
  • attr_icon_key — Example: {{ "lock" | attr_icon_key }} — Behaviour: Direct lookup by icon key (for admin-chosen icons, e.g. the attributes section's per-feature icon blocks).
  • safe_video_url — Example: <iframe src="{{ Section.Settings.videoUrl | safe_video_url }}"> — Behaviour: HTTPS + whitelist (YouTube, Vimeo, Dailymotion, Loom); anything else → about:blank.

Section settings dictionaries are converted to plain CLR types before rendering (JTokens fromthe DB, JsonElements from draft previews), so {% for block in Section.Blocks %} and{{ Section.Settings.anything }} just work.