Skip to main content

Twig Global Objects & API Data Models

SitePack automatically injects fully-populated data models into your Twig templates depending on which page is being rendered. This guide is the definitive field-by-field reference for every object your theme can access — from the site object available everywhere, down to the page-specific product, category, cart, customer, blog/article, and sitemap objects.

Every field below has been verified directly against the platform's data models, so you can rely on it for both hand-written templates and AI-assisted development (Copilot, custom GPTs, etc.).

:::tip New to SitePack templating? If you haven't yet, start with the Directory Structure Guide to see which template file maps to which storefront page — this guide tells you what data is available inside each of those files. :::


Available Everywhere: Global Objects​

These objects are available in every template, because they're attached in the base rendering context used by layouts/base.twig and all its snippets.

The site Object​

The site global gives you the store's identity, branding, and status. Access its fields directly — there is no nested site.site.* path, despite what older sample code may show.

FieldTypeDescription
site.uuidstringThe store's unique identifier.
site.namestringThe store's display name.
site.localestringFull active locale code (e.g., nl_NL, en_US). Use this for the <html lang=""> attribute.
site.descriptionstringThe store's short tagline/description.
site.domainstringThe store's primary domain (e.g., mystore.com).
site.hoststringThe active HTTP hostname.
site.protocolstringActive protocol, typically https.
site.logostringAbsolute URL to the store's logo.
site.iconstringAbsolute URL to the store's favicon.
site.socialMediaarrayThe linked social profiles, each with a name and a url. name is one of Facebook, Twitter, Instagram, YouTube, Pinterest, TikTok or LinkedIn; networks the owner left empty are not in the list.
site.allowIndexbooleanWhether search engines are allowed to index the store.
site.cookieBarbooleanWhether the cookie consent banner is enabled.
site.createdDateTimeWhen the store was created.
<html lang="{{ site.locale }}">
<head>
<title>{{ sitepack_title() }} | {{ site.name }}</title>
</head>
<body>
<a href="/" title="{{ site.name }}">
{% if site.logo %}
<img src="{{ site.logo }}" alt="{{ site.name }}"/>
{% else %}
<span class="site-name">{{ site.name }}</span>
{% endif %}
</a>
</body>

Store settings lookup: use site.getSetting('key') to read a general store setting configured in the SitePack Admin (independent of your theme's own theme.json settings), for example {{ site.getSetting('contact_email') }}.

The settings Object​

A separate, page-scoped settings object exposes feature flags relevant to the current request — most commonly used to conditionally show storefront-only UI:

{% if settings.getSetting('store_enabled') %}
<li class="nav-icon nav-shopping-cart" id="nav-cart">
{{ sitepack_shopping_cart_icon() }}
</li>
{% endif %}

Don't confuse this with your own theme customizer values. Those reach your theme as CSS custom properties — one --key per setting declared in theme.json, consumed with var(--key) in your stylesheet. See the theme.json Reference.

Automatic Brand Design Variables​

Independently of anything you declare in theme.json, SitePack always makes the merchant's core brand settings (configured once in Admin → Design) available as CSS custom properties on :root, injected by sitepack_head(). You can use these immediately in your stylesheet with zero configuration:

a { color: var(--link-color); }
a:hover { color: var(--link-active-color); text-decoration: var(--link-text-decoration); }
header nav { max-width: var(--container-width); }

Common variables include --main-color, --main-color-text, --secondary-color, --secondary-color-text, --link-color, --link-active-color, --link-text-decoration, --border-radius, --container-width, --heading-font, and --body-font. This is why the skeleton's own theme.css can reference variables like --link-color and --container-width that aren't listed anywhere in its theme.json — they're core, not theme-defined.

A theme can fill the colour variables from its own settings with platform_colors in theme.json; see Mapping Core Colours to Your Settings.

A top-level array (not site.navigation) of the store's main menu, ready to loop over recursively:

FieldTypeDescription
labelstringMenu item text.
urlstringLink target.
uuidstringItem identifier.
childrenarrayNested submenu items, same shape.
displayAsButtonbooleanWhether the merchant flagged this item to render as a button.
<ul class="nav-menu">
{% for item in navigation %}
<li class="nav-item {% if item.displayAsButton %}btn{% endif %}">
<a href="{{ item.url }}">{{ item.label }}</a>
{% if item.children is not empty %}
<ul class="nav-dropdown">
{% for child in item.children %}
<li><a href="{{ child.url }}">{{ child.label }}</a></li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>

seo​

A top-level seo object with per-page search metadata (also available nested as product.seo / category.seo for entity-specific overrides):

FieldTypeDescription
seo.canonicalstringCanonical URL for the current page.
seo.metaDescriptionstringMeta description tag content.
seo.openGraphDescriptionstringOpen Graph description for social sharing.
seo.customTitleTagstringMerchant-overridden <title> value.
seo.mainImagestringSocial sharing image URL.
seo.jsonLdstringPre-built JSON-LD structured data string.
seo.sitemapUrlstringAbsolute URL of the store's XML sitemap.
seo.robotsUrlstringAbsolute URL of the store's robots.txt.
seo.allowIndexbooleanWhether this page may be indexed.
seo.noIndexbooleanInverse convenience flag.
<link rel="canonical" href="{{ seo.canonical }}">
<meta name="description" content="{{ seo.metaDescription }}">
{# The page's SEO JSON-LD is emitted for you by sitepack_head(); you do not print
seo.jsonLd yourself (and the `raw` filter is not available in the sandbox). #}

The site's Organization node in that JSON-LD is built from the merchant's company profile (Settings → My company in the admin). Its @type follows the organisation type the merchant picked during onboarding, so it can be a more specific schema.org type such as Restaurant, OnlineStore, NGO or Dentist, and it carries whatever the merchant filled in: legalName, foundingDate, founder, vatID, contactPoint (its availableLanguage is the site language, English and every purchased translation), priceRange, paymentAccepted (the payment methods active in the site's checkout), servesCuisine, hasMerchantReturnPolicy, a DonateAction for charities, and a separate SoftwareApplication node for software companies. For a local business type, the site's locations use that same type instead of a generic LocalBusiness. Do not emit your own Organization JSON-LD from a theme: it would duplicate this node with less information.


Product Objects (templates/product.twig, templates/category.twig, snippets/product-card.twig)​

The product Object​

FieldTypeDescription
product.idintegerNumeric product ID.
product.uuidstringGlobally unique product identifier.
product.namestringFull product name.
product.nameWithoutBrandstringProduct name with the brand prefix stripped.
product.slugstringURL-safe slug.
product.urlstringRelative storefront path, in the store's main language. Pass the whole product to sitepack_product_slug() so a translated visitor stays in their language.
product.canonicalstringAbsolute canonical URL.
product.brandstringBrand / manufacturer name.
product.modelstringModel designation.
product.skustringStock Keeping Unit.
product.upcstringBarcode value.
product.conditionstringe.g. new, refurbished, used.
product.imagestringPrimary/featured image URL.
product.imagesarrayAll gallery images: the product's own images first, then each variant image once (see sitepack_product_options()). Each item has url, thumbnail and altText.
product.first_imagearrayConvenience shortcut for card/listing contexts: {thumbnail, alt_text}, derived from images[0]. Note the snake_case keys — this is the one place in the API that doesn't use camelCase.
product.videostringOptional product video URL.
product.descriptionstringFull rich-text description.
product.shortDescriptionstringShort plain-text teaser.
product.pdfstringOptional manual/datasheet URL.
product.priceCentsintegerActive selling price, in cents.
product.pricePromoCentsintegerPromotional price, in cents (0 if no active promo).
product.priceAdvisedCentsintegerManufacturer advised price (MSRP), in cents.
product.disableProductSalesbooleantrue if the product is catalog-only (not purchasable).
product.ratingScorefloatAverage review rating (0–5).
product.reviewCountintegerNumber of reviews.
product.facetsarraySpec/attribute list. Each item has name and values (array of strings).
product.categoryMain / categorySub / categorySubSubobjectCategory ancestry, each with name, slug, url. Pass the whole object to sitepack_category_slug() so the breadcrumb keeps a translated shopper in their language.
product.variantsobjecttotalProducts, rootLevelAttributes, productCombinations.
product.bundlesarrayRelated bundled products.
product.seoobjectSame shape as the global seo object, above.
product.created / product.modifiedDateTimeTimestamps — pipe through Twig's date filter.

Always price in cents for math, and format with sitepack_price() for display:

<div class="product-pricing">
{% if product.pricePromoCents > 0 and product.pricePromoCents < product.priceCents %}
<span class="price-old">{{ sitepack_price(product.priceCents) }}</span>
<span class="price-current sale-price">{{ sitepack_price(product.pricePromoCents) }}</span>
{% else %}
<span class="price-current">{{ sitepack_price(product.priceCents) }}</span>
{% endif %}
</div>
{# Star rating, as used in snippets/product-card.twig #}
{% if product.reviewCount > 0 %}
{% for i in 1..5 %}
{% if product.ratingScore >= i %}
{{ sitepack_icon('star-solid') }}
{% elseif product.ratingScore >= i - 0.5 %}
{{ sitepack_icon('star-half') }}
{% else %}
{{ sitepack_icon('star') }}
{% endif %}
{% endfor %}
<span class="review-count">({{ product.reviewCount }})</span>
{% endif %}

The category Object (templates/category.twig)​

FieldTypeDescription
category.namestringCategory display name.
category.slugstringURL slug, in the store's main language. Pass the whole category to sitepack_category_slug().
category.filtersarrayAvailable facet filters for the sidebar.
category.elementsAbove / category.elementsBelowarrayMerchant-authored content blocks — render with sitepack_elements().
category.seoobjectSame shape as the global seo object.

Alongside category, the category controller also injects these top-level (sibling, not nested) variables:

VariableTypeDescription
productsarray of productThe paginated product listing for this category.
sub_categoriesarrayChild categories, each with name, slug/url, label, backgroundImage.
quick_filtersarrayShortcut filter links, each with url and name. Note the snake_case name — this is a real, intentional context key, not a typo.
activated_filtersarrayThe filters the visitor has switched on this session, as a map of filter key to a list of active option slugs (e.g. { 'maat': ['38'] }). Use it to mark options active and to build the "active filter" chips.
sort_directionstringThe active sort: popularity (default), priceasc or pricedesc.
total_products, total_pages, current_pageintegerPagination info.
per_pageintegerProducts shown per page — combine with current_page for a "showing X of Y" indicator.
{% for product in products %}
{% include 'snippets/product-card.twig' %}
{% endfor %}

{% if quick_filters is defined and quick_filters %}
<div class="category-quick-filters">
{% for quick in quick_filters %}
<a href="{{ quick.url | ensure_link }}" class="btn btn-default">{{ quick.name }}</a>
{% endfor %}
</div>
{% endif %}

Filters, sorting and pagination​

Each entry in category.filters is a filter group with these getters:

FieldTypeDescription
filter.filterTypeinteger1 / 2 = an attribute facet (size, colour, brand); 3 = the price range; 5 = the category tree.
filter.keystringThe filter's key, used in the filter/remove URLs and as the activated_filters map key.
filter.labelstringDisplay label for the group (e.g. Size).
filter.optionsarrayThe selectable options (empty for the price filter).
filter.min / filter.maxintegerOnly for the price filter (filterType == 3): the lowest and highest product price in the category, in whole euros, used to bound the slider.

Each option carries option.label / option.name, option.slug, option.total (product count), and, for the category tree (filterType == 5), option.url and option.nestedLevel.

The price filter is a single range rather than a list of options. Its active value lives in activated_filters['price'] as one "min-max" string (whole euros, e.g. "15-65"); set it with /categories/{slug}/filter/price/{min}-{max}. In the sandbox the split filter is not available, so read the two bounds out of the "min-max" string in JavaScript (e.g. from a data-current attribute) rather than in Twig.

Filters, sorting and clearing are plain links the frontend handles — no JavaScript required for the links themselves:

ActionURL
Activate an option/categories/{slug}/filter/{key}/{option-slug}
Set the price range/categories/{slug}/filter/price/{min}-{max}
Remove an active option/categories/{slug}/remove/{key}/{option-slug}
Clear every active filter/categories/{slug}/clear
Change the sort/store/api/category-sort?direction={popularity|priceasc|pricedesc}&url=/categories/{slug}
{% for filter in category.filters %}
{% if filter.filterType in [1, 2] and filter.options|length > 1 %}
<div class="filter-group">
<div class="filter-group__title">{{ filter.label }}</div>
{% for option in filter.options %}
{% set active = activated_filters[filter.key] is defined and option.slug in activated_filters[filter.key] %}
<a href="/categories/{{ category.slug }}/{{ active ? 'remove' : 'filter' }}/{{ filter.key }}/{{ option.slug }}"
class="filter-option{% if active %} is-active{% endif %}">
{{ option.label }} <span>{{ option.total }}</span>
</a>
{% endfor %}
</div>
{% endif %}
{% endfor %}

The sort control posts the chosen direction to the store endpoint, which stores it in the session and redirects back to the category:

<select onchange="window.location.href='/store/api/category-sort?direction='
+ this.value + '&url=' + encodeURIComponent('/categories/{{ category.slug }}')">
<option value="popularity"{{ sort_direction == 'popularity' ? ' selected' }}>{{ 'category.sort_popularity'|trans }}</option>
<option value="priceasc"{{ sort_direction == 'priceasc' ? ' selected' }}>{{ 'category.sort_price_asc'|trans }}</option>
<option value="pricedesc"{{ sort_direction == 'pricedesc' ? ' selected' }}>{{ 'category.sort_price_desc'|trans }}</option>
</select>

Shopping Cart Object (templates/cart.twig)​

FieldTypeDescription
cart.uuidstringCart identifier.
cart.totalProductsintegerNumber of items in the cart.
cart.totalValueintegerGrand total, in cents.
cart.subTotalintegerSubtotal before shipping/discount, in cents.
cart.discountintegerDiscount amount, in cents.
cart.shippingCostsintegerShipping cost, in cents.
cart.itemsarrayLine items.
cart.bundlesarrayBundled items in the cart.
{% if cart.totalProducts > 0 %}
<div class="cart">
{{ sitepack_cart() }}
</div>
{% else %}
<p>{{ 'cart.empty' | trans }}</p>
{% endif %}

The cart's actual line-item markup (quantity controls, remove buttons, checkout button) is rendered by sitepack_cart() — you generally don't hand-loop cart.items yourself in templates/cart.twig; use the cart_item_* template hooks to inject content into that rendered markup instead.


Customer Account (templates/account.twig)​

The whole account section — the login and registration forms, the dashboard, the orders overview, a single order and the settings form — is rendered by sitepack_account(). Which panel appears is decided from the session and the URL, and every string is translated for you, so the entire template is:

{% extends 'layouts/base.twig' %}

{% block content %}
{{ sitepack_account() }}
{% endblock %}

Style it by overriding its canonical classes (.account, .account-nav, .order-item, .account-auth, .account-field, …) in your assets/css/theme.css — you don't reproduce the markup.

Linking to the account area

Link to the login / register / dashboard pages with sitepack_account_slug() rather than hardcoding /account, so the link stays locale-correct in a multilingual shop (/account → /de/account). For example the "Account" link in your header: <a href="{{ sitepack_account_slug() }}">, and a logout link: <a href="{{ sitepack_account_slug('logout') }}">.


Blog & Article Objects​

Content is modeled per-article, and the same shape is used both for a single article view and for entries in a listing loop.

FieldTypeDescription
article.uuidstringArticle identifier.
article.titlestringArticle title (some contexts expose this as name).
article.urlstringRelative article URL, in the store's main language. Pass the whole article to sitepack_blog_slug().
article.imagestringFeatured image — already a full, ready-to-use URL (no filter needed).
article.previewTextstringShort excerpt for listing cards.
article.contentstringFull rich-text body.
article.tagsarrayTags attached to the article.
article.authorobjectAuthor info.
article.createdDateTimePublish date — format it with Twig's built-in date filter (see example below).
article.seoobjectSame shape as the global seo object.
{# snippets/blog-card.twig #}
<article class="blog-post-summary">
<h2><a href="{{ sitepack_blog_slug(article) }}">{{ article.title }}</a></h2>
<p class="article-meta">{{ 'general.published_on' | trans({'%date%': article.created | date('M d, Y')}) }}</p>
{% if article.image %}
<img src="{{ article.image }}" alt="{{ article.title }}">
{% endif %}
<div class="article-excerpt">{{ article.previewText }}</div>
<a href="{{ sitepack_blog_slug(article) }}">{{ 'general.read_more' | trans }}</a>
</article>

blog (listing context, templates/blog.twig)​

  • blog.title — listing page title.
  • blog.articles — array of article-shaped items to loop over.
  • blog.url — the blog's base URL (used for "back to blog" links from templates/article.twig); pass it through sitepack_blog_slug() so the link stays in the language being read.

tag (tag archive, templates/tag.twig)​

  • tag.title — the tag name.
  • tag.description — optional tag description text.
  • tag.blogs — array of article-shaped items tagged with this tag. Note the field is blogs, not articles.
{% for article in tag.blogs %}
{% include 'snippets/blog-card.twig' with { article: article } %}
{% endfor %}

Homepage (templates/homepage.twig)​

The homepage renders from templates/homepage.twig, a registered custom template that SitePack auto-assigns as the site homepage. It receives two kinds of data:

  • Editable copy from the page object, keyed by the field keys you declare in theme.json (hero title, CTA label, intro text, section headings, and so on). Read them with bracket notation: page['hero-title']. See the Homepage template section in the theme.json Reference.
  • Live content through the sitepack_* data functions, which return the same object shapes used elsewhere in the theme.

A custom template does not receive collections.frontpage, so the featured product grid is pulled in with sitepack_products() rather than a curated collection:

{% set products = sitepack_products(8, 'popular') %}
{% if products | length >= 1 %}
<div class="product-grid">
{% for product in products %}
{% include 'snippets/product-card.twig' with { product: product } %}
{% endfor %}
</div>
{% endif %}
One template, every site type

Build homepage.twig so each section renders only when it has content: sitepack_products() and sitepack_categories() return empty arrays on a site with no shop, sitepack_events() is empty without an agenda, and sitepack_blogs() is empty without posts. The same template then fits a webshop, an events site and a content website. See the full example in Page Examples.

Sitemap Object (templates/sitemap.twig)​

FieldTypeDescription
sitemap.pagesarrayAll static content pages, each with url and label.
sitemap.categoriesarrayAll product categories.
sitemap.productsarrayAll products.
sitemap.blogsarrayAll blog articles.
sitemap.contentarrayGeneric content entries.

Site content managed in the admin​

Four content types are managed centrally by the merchant in the admin and exposed to your theme. Render them with the matching functions in the Functions Reference rather than reading these shapes by hand — the shapes are documented here so you know what each function returns.

newsletter​

Available everywhere as the newsletter global. Usually you just call {{ sitepack_newsletter_form() }}; read the object only when you build your own form markup.

FieldTypeDescription
newsletter.enabledboolWhether the merchant has newsletter signup turned on.
newsletter.double_opt_inboolWhether a confirmation email is required before a subscriber is added.
newsletter.texts.introstringIntro line above the form.
newsletter.texts.buttonstringSubmit button label.
newsletter.texts.confirmationstringMessage shown after a successful signup.

Texts are already resolved for the page language.

USPs — sitepack_usps()​

Returns an array of the site's global USPs, in the merchant's sort order (the optional placement argument is legacy and ignored — see the function reference). Each item:

FieldTypeDescription
usp.uuidstringStable identifier, handy as a loop key.
usp.titlestringThe promise (free-shipping threshold already interpolated).
usp.textstringOptional supporting line (may be empty).
usp.iconstringOptional icon name for sitepack_icon() (may be empty).

Locations — sitepack_locations()​

Returns an array of the site's visible locations, in the admin's order (by name) — see the function reference. Each item:

FieldTypeDescription
location.uuidstringStable identifier, handy as a loop key.
location.namestringThe location or company name.
location.addressstringStreet and house number.
location.postalCodestringPostal code.
location.citystringCity.
location.countryCodestringISO country code, e.g. NL.
location.emailstringE-mail address.
location.phonestringPhone number exactly as the merchant typed it, for display.
location.phoneHrefstringA tel: link with only digits and + (e.g. tel:0246413379), for href. Empty when there is no phone number.
location.vatNumberstringVAT number.
location.cocNumberstringChamber of Commerce (KvK) number.
location.openingHoursarraySeven entries, mon to sun, each { day, open, close, closed }: day is the day key, open / close are times like 09:00, and closed is true when the location is shut that day (the times are then ''). A day with a missing opening or closing time counts as closed. Empty when the location is open on no day at all (for example a webshop that never entered opening hours), so guard the block with {% if location.openingHours %}.

Text fields are never null: an empty field is ''.

Reviews — sitepack_reviews()​

Returns the site's reviews: the aggregated company score and the individual customer reviews. See the function reference. An empty array when the site has neither a score nor visible reviews; otherwise:

FieldTypeDescription
reviews.averageScorefloat|nullThe average rating, rounded to one decimal, e.g. 9.4. null when the site has reviews but none with a score.
reviews.bestScoreint|nullThe top of the rating scale, usually 10 (or 5 for star ratings). null without a score.
reviews.worstScoreint|nullThe bottom of the rating scale, usually 1. null without a score.
reviews.totalintThe number of reviews the average is based on; 0 when there is no score. Guard the score with {% if reviews.total %}.
reviews.percentageint|nullThe average as a percentage of the scale, e.g. 94; null when unknown.
reviews.sourcestringWhere the score comes from: the review integration (e.g. FiveStarSpecialist), or sitepack when it is calculated from the reviews the merchant manages in SitePack. '' without a score.
reviews.urlstringThe public review page of the company; '' when unknown.
reviews.updatedstringThe date the score was last refreshed, YYYY-MM-DD; '' when unknown.
reviews.itemsarrayThe newest visible customer reviews (at most 12), in the order the merchant set. Empty when there are none. Each item is described below.

Each item in reviews.items:

FieldTypeDescription
item.authorstringThe reviewer's name as shown on the site; '' for an anonymous review.
item.textstringThe review text (plain text, never empty).
item.scorefloat|nullThe score this reviewer gave, e.g. 9 or 4.5; null when they gave none.
item.bestScoreintThe top of this review's scale: 5 (stars) or 10.
item.datestringThe review date, YYYY-MM-DD.
item.date_formattedstringThe review date in the page language, e.g. 2 oktober 2026.
item.labelstringAn optional topic the merchant gave the review, e.g. Repair; '' when none.
item.sourcestringmanual for a review the merchant entered, or the integration it was imported from (e.g. FiveStarSpecialist).

Events — sitepack_events(limit, scope)​

Returns an array of events (by default only upcoming ones; pass scope as 'past' or 'all' to change that); each item:

FieldTypeDescription
event.titlestringEvent name.
event.descriptionstringOptional description (may be null).
event.date_formattedstringStart date formatted in the page language, e.g. di 15 sep 2026.
event.start_date / event.end_datestringYYYY-MM-DD (end may be null).
event.start_time / event.end_timestringHH:MM (may be null).
event.locationstringResolved place — the free-text place, or the name/city of a linked location (may be empty).
event.linkstringOptional link (may be null).
event.imagestringOptional image URL (may be null).
event.json_ldobjectA schema.org Event object. Emit it with {{ sitepack_json_ld(event.json_ld) }} — never json_encode|raw — see sitepack_json_ld().

Blogs — sitepack_blogs(limit)​

Returns an array of the most recent published blog posts (newest first) — the way to show a "latest news" block on a custom template, which does not otherwise receive the blog listing. limit defaults to 3 (max 24). Each item:

FieldTypeDescription
blog.titlestringPost title.
blog.urlstringLink to the post (already language-correct — use it directly).
blog.excerptstringPreview text.
blog.imagestringMain image URL (may be empty).
blog.createdstringYYYY-MM-DD.
blog.date_formattedstringDisplay date, e.g. 20-08-2026.
blog.tagsarrayTag strings (may be empty).
blog.authorobjectauthor.first_name, author.last_name, author.url.
blog.translationsarrayPer-language addresses; link a post with sitepack_blog_slug(blog) rather than blog.url.

Topbar​

The topbar is rendered as a whole with {{ sitepack_topbar() }}; it has no per-item object to iterate. The merchant turns it on and picks one of two fixed layouts in the admin, and the function prints the matching HTML:

  • Standard — <div class="topbar-standard"> with a .topbar-standard__usps column (each USP as .topbar-usp → .topbar-usp__icon + .topbar-usp__label) and a .topbar-standard__contact column (.topbar-contact__email / .topbar-contact__phone as mailto: / tel: links).
  • Text — <div class="topbar-text"> wrapping one sanitized rich-text line.

Style those classes in your stylesheet for both layouts. See sitepack_topbar() for the full markup and colour handling.


Landing Page / Custom Template Fields​

When a page uses a custom template declared in theme.json (like the skeleton's landing-page), its configured field values are exposed on the page object, keyed by the field's key. Because keys often contain hyphens, use bracket notation:

{% if page['hero-title'] %}
<h1 class="landing-hero__title">{{ page['hero-title'] }}</h1>
{% endif %}

A list field is an array of strings. An image, file or media field holds the absolute url of the uploaded file (empty when there is none), so guard it before rendering a download button:

{% if page['specs-pdf'] %}
<a href="{{ page['specs-pdf'] }}" target="_blank" rel="noopener">{{ page['specs-label'] }}</a>
{% endif %}

See the theme.json Reference for how to declare these custom template fields.


Exploring Further:​

Now that you have mastered the Twig data models, discover all the dynamic layout rendering utilities and math helpers inside the Custom Theme Functions Guide!