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 aSpaceCounteach.
Layout-only additions:
IsEmbed— What it is: True for/embed/...or?embed=1renders.content_for_layout— What it is: The pre-rendered page HTML — emit with{{ content_for_layout | raw }}.theme_css— What it is: Contents ofassets/theme.css— inline into a<style>with| raw.theme_js— What it is: Contents ofassets/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 inschema/settings.json— e.g.Settings.Design.primaryColor,Settings.Design.colorMode,Settings.Design.bodyFont,Settings.Design.cornerRadius. Free-form: add a new control tosettings.jsonand readSettings.Design.yourIdin 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.CanonicalUrlis 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-editedHeadervalues 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), plusCustomCss/CustomMetadictionaries.
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:UIdis the GUID to link with:/t/spaces/{{ item.UId }}.Name,IconCategoryId,CategoryUId,CategoryNamePrice— 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 throughcycle_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,LocationNameMedias— 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:PromotionLabelis 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/accountand absolute URLs alone); turns bare phone numbers intotel:and bare emails intomailto:.external_url— Example:{{ Settings.FacebookUrl | external_url }}— Behaviour: Addshttps://to bare domains; passeshttp(s)/mailto/telthrough.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 (bloborganisations/{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, defaultmo.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 againstschema/attribute-icons.jsonand 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.