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

  • OrganisationType: ThemeV2OrgContextWhat it is: The tenant.
  • PageType: ThemeV2PageContextWhat it is: Route metadata.
  • SettingsType: ThemeV2GlobalSettingsWhat it is: Theme-wide settings, with per-page SEO already overlaid (§4.2).
  • SectionsType: List<ThemeV2SectionInstance>What it is: The ordered section list for this page key (§5).
  • LocationsType: List<ThemeV2LocationData>What it is: Active, non-hidden locations.
  • CategoriesType: List<ThemeV2CategoryData>What it is: All categories across locations — narrowed to the filtered location's categories when a location filter is active.
  • SpacesType: List<ThemeV2SpaceData>What it is: Listing data (see scope table above).
  • SpaceType: ThemeV2SpaceDataWhat it is: Single space on detail pages; null elsewhere.
  • LocationType: ThemeV2LocationDataWhat it is: Single location on location detail; null elsewhere.
  • RequestType: ThemeV2RequestContextWhat it is: Path, base URL, validated filters, embed flag.
  • CmsPagesType: List<ThemeV2CmsPage>What it is: All active CMS pages (for nav/footer link lists).
  • CmsPageType: ThemeV2CmsPageWhat it is: The current CMS page when rendering one; null elsewhere.
  • MenusType: List<ThemeV2Menu>What it is: Navigation menus.
  • FooterType: ThemeV2FooterContentWhat it is: Builder-edited footer content (may be null on old rows — guard).
  • HeaderType: ThemeV2HeaderContentWhat it is: Builder-edited header content (may be null — guard).
  • AttributesType: List<ThemeV2AttributeData>What it is: Org-wide attribute catalogue: distinct attributes across active spaces, with a SpaceCount each.

Layout-only additions:

  • IsEmbedWhat it is: True for /embed/... or ?embed=1 renders.
  • content_for_layoutWhat it is: The pre-rendered page HTML — emit with {{ content_for_layout | raw }}.
  • theme_cssWhat it is: Contents of assets/theme.css — inline into a <style> with | raw.
  • theme_jsWhat 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, UIdNotes: UId is the GUID to link with: /t/spaces/{{ item.UId }}.
  • Name, Icon
  • CategoryId, CategoryUId, CategoryName
  • PriceNotes: The space's price (not the category's — category price drifts when a space price is overridden).
  • DurationTypeNotes: Billing cycle as a string ("Month", "Week"…) — pipe through cycle_abbrev.
  • StatusNotes: Space status string ("Vacant"…).
  • Width, Height, Length, DoorHeightNotes: Dimensions (nullable doubles).
  • AllowOnlineBookingsNotes: Space and location toggles combined — gates the Book Online CTA.
  • LocationId, LocationName
  • MediasNotes: Image URIs, ordered.
  • AttributesNotes: Attribute names.
  • AttributeIds, AttributeUIdsNotes: Parallel id lists (used for filter links / promo matching).
  • PromotionLabelNotes: The winning promotion's description (or name) — always set when a promo applies.
  • SpacePromotionPriceNotes: Admin-set promotional price on the space row; wins over the computed amount.
  • PromotionPriceNotes: Effective discounted price per cycle (nullable).
  • PromotionRuleType, PromotionRequiredAmount, PromotionDurationTypeNotes: Rule metadata for "first N months" style badges.
  • HasPromotionNotes: 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:

  • dollarExample: {{ item.Price | dollar }}Behaviour: $1,234.50; null/garbage → $0.00.
  • storefront_urlExample: {{ 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_urlExample: {{ Settings.FacebookUrl | external_url }}Behaviour: Adds https:// to bare domains; passes http(s)/mailto/tel through.
  • dateformatExample: {{ someDate | dateformat }}Behaviour: dd/MM/yyyy.
  • asset_urlExample: {{ "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.
  • jsonExample: {{ Spaces | json }}Behaviour: Serialises any value to JSON — for inlining server data into <script>.
  • pluralExample: {{ Organisation.SpaceName | plural }}Behaviour: Light English pluraliser (spacespaces, baybays, boxboxes).
  • cycle_abbrevExample: {{ item.DurationType | cycle_abbrev }}Behaviour: weekw, fortnightfn, dayd, yearyr, quarterqtr, halfyearly6mo, default mo.
  • multiplyExample: {{ item.Width | multiply: item.Length }}Behaviour: Numeric multiply, rounded to 1 decimal.
  • attr_iconExample: {{ 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_keyExample: {{ "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_urlExample: <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.