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.socialMediaarrayAssociative list of social links (keys are network names like facebook, instagram).
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 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). #}

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. Each item has url 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. Maat).
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 Objects (templates/account.twig)

FieldTypeDescription
customer.emailstringCustomer email address.
customer.firstNamestringFirst name.
customer.lastNamestringLast name.
customer.createdDateTimeAccount creation date.
<h2>{{ 'account.welcome_customer' | trans({'%name%': customer.firstName ~ ' ' ~ customer.lastName}) }}</h2>
<p>{{ 'account.email' | trans({'%email%': customer.email}) }}</p>

Order history (order.id, order.orderNumber, order.totalAmountCents, order.status, order.created, order.paid) and saved addresses are provided to the account template as separate context variables alongside customer — check the rendered variables available on your store's account page (e.g. via {{ dump() }} in a non-production environment) before building complex account UI, since address fields in particular can vary by store configuration.


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 Collections (templates/index.twig)

  • collections.frontpage.products — the merchant-curated array of featured products for the homepage grid.
<div class="product-grid">
{% for product in collections.frontpage.products %}
{% include 'snippets/product-card.twig' with { product: product } %}
{% endfor %}
</div>
Building your own home template?

collections.frontpage.products is only supplied to templates/index.twig. If you assign a custom template as your homepage (or want a product row on any other page), pull products in with the sitepack_products(limit, category) function instead — it returns the same product objects as an array.

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).

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().

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 %}

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!