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.
| Field | Type | Description |
|---|---|---|
site.uuid | string | The store's unique identifier. |
site.name | string | The store's display name. |
site.locale | string | Full active locale code (e.g., nl_NL, en_US). Use this for the <html lang=""> attribute. |
site.description | string | The store's short tagline/description. |
site.domain | string | The store's primary domain (e.g., mystore.com). |
site.host | string | The active HTTP hostname. |
site.protocol | string | Active protocol, typically https. |
site.logo | string | Absolute URL to the store's logo. |
site.icon | string | Absolute URL to the store's favicon. |
site.socialMedia | array | Associative list of social links (keys are network names like facebook, instagram). |
site.allowIndex | boolean | Whether search engines are allowed to index the store. |
site.cookieBar | boolean | Whether the cookie consent banner is enabled. |
site.created | DateTime | When 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.
navigation
A top-level array (not site.navigation) of the store's main menu, ready to loop over recursively:
| Field | Type | Description |
|---|---|---|
label | string | Menu item text. |
url | string | Link target. |
uuid | string | Item identifier. |
children | array | Nested submenu items, same shape. |
displayAsButton | boolean | Whether 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):
| Field | Type | Description |
|---|---|---|
seo.canonical | string | Canonical URL for the current page. |
seo.metaDescription | string | Meta description tag content. |
seo.openGraphDescription | string | Open Graph description for social sharing. |
seo.customTitleTag | string | Merchant-overridden <title> value. |
seo.mainImage | string | Social sharing image URL. |
seo.jsonLd | string | Pre-built JSON-LD structured data string. |
seo.sitemapUrl | string | Absolute URL of the store's XML sitemap. |
seo.robotsUrl | string | Absolute URL of the store's robots.txt. |
seo.allowIndex | boolean | Whether this page may be indexed. |
seo.noIndex | boolean | Inverse 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
| Field | Type | Description |
|---|---|---|
product.id | integer | Numeric product ID. |
product.uuid | string | Globally unique product identifier. |
product.name | string | Full product name. |
product.nameWithoutBrand | string | Product name with the brand prefix stripped. |
product.slug | string | URL-safe slug. |
product.url | string | Relative 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.canonical | string | Absolute canonical URL. |
product.brand | string | Brand / manufacturer name. |
product.model | string | Model designation. |
product.sku | string | Stock Keeping Unit. |
product.upc | string | Barcode value. |
product.condition | string | e.g. new, refurbished, used. |
product.image | string | Primary/featured image URL. |
product.images | array | All gallery images. Each item has url and altText. |
product.first_image | array | Convenience 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.video | string | Optional product video URL. |
product.description | string | Full rich-text description. |
product.shortDescription | string | Short plain-text teaser. |
product.pdf | string | Optional manual/datasheet URL. |
product.priceCents | integer | Active selling price, in cents. |
product.pricePromoCents | integer | Promotional price, in cents (0 if no active promo). |
product.priceAdvisedCents | integer | Manufacturer advised price (MSRP), in cents. |
product.disableProductSales | boolean | true if the product is catalog-only (not purchasable). |
product.ratingScore | float | Average review rating (0–5). |
product.reviewCount | integer | Number of reviews. |
product.facets | array | Spec/attribute list. Each item has name and values (array of strings). |
product.categoryMain / categorySub / categorySubSub | object | Category 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.variants | object | totalProducts, rootLevelAttributes, productCombinations. |
product.bundles | array | Related bundled products. |
product.seo | object | Same shape as the global seo object, above. |
product.created / product.modified | DateTime | Timestamps — 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)
| Field | Type | Description |
|---|---|---|
category.name | string | Category display name. |
category.slug | string | URL slug, in the store's main language. Pass the whole category to sitepack_category_slug(). |
category.filters | array | Available facet filters for the sidebar. |
category.elementsAbove / category.elementsBelow | array | Merchant-authored content blocks — render with sitepack_elements(). |
category.seo | object | Same shape as the global seo object. |
Alongside category, the category controller also injects these top-level (sibling, not nested) variables:
| Variable | Type | Description |
|---|---|---|
products | array of product | The paginated product listing for this category. |
sub_categories | array | Child categories, each with name, slug/url, label, backgroundImage. |
quick_filters | array | Shortcut filter links, each with url and name. Note the snake_case name — this is a real, intentional context key, not a typo. |
activated_filters | array | The 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_direction | string | The active sort: popularity (default), priceasc or pricedesc. |
total_products, total_pages, current_page | integer | Pagination info. |
per_page | integer | Products 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:
| Field | Type | Description |
|---|---|---|
filter.filterType | integer | 1 / 2 = an attribute facet (size, colour, brand); 3 = the price range; 5 = the category tree. |
filter.key | string | The filter's key, used in the filter/remove URLs and as the activated_filters map key. |
filter.label | string | Display label for the group (e.g. Maat). |
filter.options | array | The selectable options (empty for the price filter). |
filter.min / filter.max | integer | Only 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:
| Action | URL |
|---|---|
| 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)
| Field | Type | Description |
|---|---|---|
cart.uuid | string | Cart identifier. |
cart.totalProducts | integer | Number of items in the cart. |
cart.totalValue | integer | Grand total, in cents. |
cart.subTotal | integer | Subtotal before shipping/discount, in cents. |
cart.discount | integer | Discount amount, in cents. |
cart.shippingCosts | integer | Shipping cost, in cents. |
cart.items | array | Line items. |
cart.bundles | array | Bundled 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)
| Field | Type | Description |
|---|---|---|
customer.email | string | Customer email address. |
customer.firstName | string | First name. |
customer.lastName | string | Last name. |
customer.created | DateTime | Account 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.
| Field | Type | Description |
|---|---|---|
article.uuid | string | Article identifier. |
article.title | string | Article title (some contexts expose this as name). |
article.url | string | Relative article URL, in the store's main language. Pass the whole article to sitepack_blog_slug(). |
article.image | string | Featured image — already a full, ready-to-use URL (no filter needed). |
article.previewText | string | Short excerpt for listing cards. |
article.content | string | Full rich-text body. |
article.tags | array | Tags attached to the article. |
article.author | object | Author info. |
article.created | DateTime | Publish date — format it with Twig's built-in date filter (see example below). |
article.seo | object | Same 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 ofarticle-shaped items to loop over.blog.url— the blog's base URL (used for "back to blog" links fromtemplates/article.twig); pass it throughsitepack_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 ofarticle-shaped items tagged with this tag. Note the field isblogs, notarticles.
{% 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>
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)
| Field | Type | Description |
|---|---|---|
sitemap.pages | array | All static content pages, each with url and label. |
sitemap.categories | array | All product categories. |
sitemap.products | array | All products. |
sitemap.blogs | array | All blog articles. |
sitemap.content | array | Generic 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.
| Field | Type | Description |
|---|---|---|
newsletter.enabled | bool | Whether the merchant has newsletter signup turned on. |
newsletter.double_opt_in | bool | Whether a confirmation email is required before a subscriber is added. |
newsletter.texts.intro | string | Intro line above the form. |
newsletter.texts.button | string | Submit button label. |
newsletter.texts.confirmation | string | Message 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:
| Field | Type | Description |
|---|---|---|
usp.uuid | string | Stable identifier, handy as a loop key. |
usp.title | string | The promise (free-shipping threshold already interpolated). |
usp.text | string | Optional supporting line (may be empty). |
usp.icon | string | Optional 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:
| Field | Type | Description |
|---|---|---|
event.title | string | Event name. |
event.description | string | Optional description (may be null). |
event.date_formatted | string | Start date formatted in the page language, e.g. di 15 sep 2026. |
event.start_date / event.end_date | string | YYYY-MM-DD (end may be null). |
event.start_time / event.end_time | string | HH:MM (may be null). |
event.location | string | Resolved place — the free-text place, or the name/city of a linked location (may be empty). |
event.link | string | Optional link (may be null). |
event.image | string | Optional image URL (may be null). |
event.json_ld | object | A 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__uspscolumn (each USP as.topbar-usp→.topbar-usp__icon+.topbar-usp__label) and a.topbar-standard__contactcolumn (.topbar-contact__email/.topbar-contact__phoneasmailto:/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!