Custom Twig Functions Reference API
SitePack registers a suite of custom Twig functions on top of the standard Twig environment. They compile down to native, sandboxed PHP calls, giving you high-performance building blocks for rendering layout chrome (headers, footers, navigation), commerce UI (cart, prices, stock), and platform integrations (icons, cookies, app blocks) — without writing any backend code yourself.
Using these built-in helpers ensures consistent e-commerce behavior, security sandboxing, and forward compatibility with future platform updates.
Complete Functions Reference Directory
theme_asset(path)
Generates the fully qualified, CDN-served URL of a static asset from inside your theme's own /assets directory.
- Parameters:
path(string): The relative path to the asset from inside your root/assetsdirectory.
- Example Usage:
<link rel="stylesheet" href="{{ theme_asset('css/theme.css') }}"><script src="{{ theme_asset('js/theme.js') }}" type="module"></script>
- Tip: The
asset_urlfilter (see the Filters Guide) is an identical alias — use whichever reads more naturally in context:theme_asset('css/theme.css')vs'css/theme.css' | asset_url.
Reading your theme's settings
There is no function for this. Every setting you declare in theme.json is emitted as a CSS custom property, so your stylesheet consumes it directly:
header { background: var(--header-background-color); }
See the theme.json Reference. Content-shaped fields on a custom template are read from the page object instead: page['hero-title'].
sitepack_title()
Outputs the correct <title> tag content for the current route (product name, category name, blog post title, static page title, etc., falling back to the store name).
<title>{{ sitepack_title() }}</title>
On legal pages (terms, privacy, cookie policy…), which have no SEO title of their own, the site's name follows the policy name: Privacy policy | Example Store. Use content.name for the page heading.
sitepack_head()
Renders the required SitePack <head> payload (asset preload/link tags for active editor elements and the compiled theme, plus other platform requirements). Always call it inside <head>, right after your <title> tag. It also prints the merchant's own head code from Settings → Custom HTML (verification meta tags, tracking snippets). Their body code lands in the body_start hook, so keep {% app_block 'body_start' %} right after your opening <body> tag.
<head>
<title>{{ sitepack_title() }}</title>
<meta charset="UTF-8">
{{ sitepack_head() }}
<link rel="stylesheet" href="{{ 'css/theme.css' | asset_url }}">
</head>
sitepack_navigation()
Renders the merchant's configured main navigation menu (including nested dropdown items) as semantic markup.
<nav>
<ul>
{{ sitepack_navigation() }}
</ul>
</nav>
sitepack_breadcrumbs()
Renders the hierarchical breadcrumb trail for the active route (e.g., Home > Men's > Jackets). Typically placed once in layouts/base.twig, right after the header include.
{{ sitepack_breadcrumbs() }}
sitepack_translations()
Renders a ready-made language switcher: a <nav><ul><li> list of direct links to the current page in every available store language, each <a> carrying the correct hreflang/lang attributes and the locale prefix (NL, DE, EN, …) as its label. The list item for the active language gets class="active". If the store has fewer than two languages it renders nothing, so it is safe to call unconditionally.
- Parameters:
attributes(hash, optional): Pass{ class: '...' }to override the default<nav>class (sitepack-translations).
- Example Usage:
{# Default markup #}{{ sitepack_translations() }}{# With a custom wrapper class #}{{ sitepack_translations({ class: 'lang-switcher' }) }}
- Example Output:
<nav class="sitepack-translations" aria-label="Languages"><ul><li class="active"><a href="https://store.example/nl/test" hreflang="nl_NL" lang="nl_NL">NL</a></li><li><a href="https://store.example/de/test" hreflang="de_DE" lang="de_DE">DE</a></li></ul></nav>
sitepack_translations_list()
Returns the same language-alternate data as a plain associative array instead of markup, so you can build a fully custom switcher (a <select>, a flag dropdown, etc.). Each key is the URL locale prefix (nl, de, en, …) and each value is the absolute URL of the current page in that language. Returns an empty array when the store has fewer than two languages.
- Returns:
array<string, string>— e.g.{ nl: 'https://store.example/nl/test', de: 'https://store.example/de/test' }. - Example Usage:
{% set languages = sitepack_translations_list() %}{% if languages|length > 1 %}<ul class="lang-switcher">{% for prefix, url in languages %}<li class="{{ prefix == site.locale[:2] ? 'active' : '' }}"><a href="{{ url }}" hreflang="{{ prefix }}">{{ prefix|upper }}</a></li>{% endfor %}</ul>{% endif %}
- Tip: Both switcher helpers link to the equivalent page in each language (the hreflang alternates SitePack resolves per route), not just the language homepage — so a visitor stays on the same product, category or article when they switch. See the Translations Guide for the full internationalization workflow.
sitepack_slug(path, absolute)
Resolves a link to another page of the store, addressed by that page's default slug — the one the merchant sees in the admin — into the address the language currently being read is actually served under. Use it for every hard-coded link in your theme (/pricing, /contact, a CTA in your footer): a plain href="/pricing" throws a visitor reading /de/ straight out of their language, and a theme cannot know translated slugs.
-
Parameters:
path(string): The page's default path, with or without slashes. Nested pages take their full path ('services/hosting'); every segment is localized.absolute(bool, optional, defaultfalse): Return a full URL instead of a root-relative path. The host is the one the request is being served from, so a link rendered on staging keeps you on staging.
-
Returns:
string— a root-relative path such as/de/preise, or an absolute URL whenabsoluteistrue. -
Example Usage:
<a href="{{ sitepack_slug('pricing') }}">{{ 'nav.pricing'|trans }}</a><a href="{{ sitepack_slug('services/hosting') }}">{{ 'nav.hosting'|trans }}</a>{# Back to the homepage of the language being read #}<a href="{{ sitepack_slug('/') }}">{{ site.name }}</a> -
What it returns, per language being rendered:
Rendered under sitepack_slug('pricing')the store's main language /pricing/de/, page translated aspreise/de/preise/fr/, page not translated/fr/pricinga store that sells no translations /pricing -
Tip: It never invents an address. A path that is not a page, a page without a translated slug, or a store without translations all fall back to the language prefix plus the path you passed — which is exactly what the router serves — so it is always safe to use, even on a single-language store. See the Translations Guide.
-
Sub pages are served one level deep. A page lives at its own slug, or at its parent's slug plus its own (
/services/hosting), never deeper. When you pass the whole trail the merchant sees in the admin ('products/services/hosting'),sitepack_slug()resolves it to the address that is served,/services/hosting(or/de/dienste/webhosting), as long as those last two segments are a page the store translates. Prefer passing the served path itself.
sitepack_account_slug(path, absolute)
Links to the customer account section — the standard SitePack login / register / dashboard pages — for the language currently being read. Use it for the "Account" link in your header, and for any link into the account area, instead of hardcoding /account: a plain href="/account" drops a visitor reading /de/ back onto the main language.
-
Parameters:
path(string, optional): A sub-path inside the account section —'login','register','orders','settings','logout'. Omit it to link to the account entry (login when logged out, dashboard when logged in).absolute(bool, optional, defaultfalse): Return a full URL instead of a root-relative path, on the host the request is being served from.
-
Returns:
string— a root-relative path such as/de/account/logout, or an absolute URL whenabsoluteistrue. -
Example Usage:
<a href="{{ sitepack_account_slug() }}">{{ 'header.account'|trans }}</a><a href="{{ sitepack_account_slug('logout') }}">{{ 'account.logout'|trans }}</a> -
What it returns, per language being rendered:
Rendered under sitepack_account_slug()sitepack_account_slug('logout')the store's main language /account/account/logout/de//de/account/de/account/logouta store that sells no translations /account/account/logout
The account section is served from a fixed route, so only the language prefix is added — the account segment and the sub-path stay the same in every language. The account pages render inside your theme's chrome, through your account.twig — see sitepack_account() for how to render them.
sitepack_legal_slug(path, absolute)
Links to a legal policy page (Terms & Conditions, Privacy Policy, Impressum, Return Policy, Review Policy, Cookie Policy) for the language currently being read. Use it for any hand-written link to a legal page instead of hardcoding /legal/…: a plain href="/legal/privacy-policy" drops a visitor reading /de/ back onto the main language. (The built-in sitepack_legal_links() and sitepack_content_legal() already do this for you.)
-
Parameters:
path(string, optional): The policy slug ('privacy-policy','terms-conditions','impressum','return-policy','review-policy','cookie-policy'). Omit it to link to the/legalbase.absolute(bool, optional, defaultfalse): Return a full URL instead of a root-relative path, on the host the request is being served from.
-
Returns:
string, a root-relative path such as/de/legal/privacy-policy, or an absolute URL whenabsoluteistrue. -
Example Usage:
<a href="{{ sitepack_legal_slug('privacy-policy') }}">{{ 'footer.privacy'|trans }}</a> -
What it returns, per language being rendered:
Rendered under sitepack_legal_slug('privacy-policy')the store's main language /legal/privacy-policy/de//de/legal/privacy-policya store that sells no translations /legal/privacy-policy
A legal page is served from a fixed route whose per-policy slug is the same in every language: only the body is translated. So, like the account section, only the language prefix is added; the legal segment and the policy slug stay the same. Merchants translate the body from the admin's Content → Translations screen (content type "Legal pages").
sitepack_product_slug(item, absolute) / sitepack_category_slug(item, absolute) / sitepack_blog_slug(item, absolute)
The same idea as sitepack_slug(), for the three kinds of content served from a fixed route: a product (/products/…), a category (/categories/…) and a blog post (/blog/…). Use them for every link to one of those, listings included.
Pass the item itself — product, category, article — not one of its fields. A listing is assembled in the store's main language, so the url on each item is that language's address; the item also carries the address it has in every other language, and these helpers read the right one off it.
-
Parameters:
item(object|array|string): The product, category or article. A slug ('red-shoe') or path ('/products/red-shoe') is also accepted for links you write by hand; an absolute URL is reduced to its path. Anything after the slug — a category page number, a filter segment, a query string, a fragment — is kept exactly as it is.absolute(bool, optional, defaultfalse): Return a full URL instead of a root-relative path, on the host the request is being served from.
-
Returns:
string— a root-relative path such as/de/products/roter-schuh, or an absolute URL whenabsoluteistrue. -
Example Usage:
<a href="{{ sitepack_product_slug(product) }}">{{ product.name }}</a><a href="{{ sitepack_category_slug(category) }}">{{ category.name }}</a><a href="{{ sitepack_blog_slug(article) }}">{{ article.title }}</a>{# The overview of the language being read: /products, /de/products #}<a href="{{ sitepack_product_slug('') }}">{{ 'nav.shop'|trans }}</a> -
What they return, per language being rendered:
Rendered under sitepack_product_slug(product)the store's main language /products/red-shoe/de/, product translated asroter-schuh/de/products/roter-schuh/fr/, product not translated/fr/products/red-shoea store that sells no translations /products/red-shoe
/de/produkte/… is served by nothing. Only the item's own slug changes per language; products, categories and blog stay as they are in every language, which is why you should never assemble one of these addresses by hand.
If you pass a hash into a snippet instead of the item itself, carry translations across or the card loses the translated address:
{% include 'snippets/category-card.twig' with { category: {
url: sub_category.slug,
translations: sub_category.translations|default([]),
categoryName: sub_category.name
} } %}
- Tip: Like
sitepack_slug(), they never invent an address. An item with no translations (a single-language store) keeps its own path; a bare slug keeps the slug behind the right prefix — both are addresses the router serves. Building the address yourself is what breaks:"/de/products/" ~ product.slug404s the moment the merchant translates that slug, andproduct.urlon its own silently drops the visitor's language.
sitepack_content()
Renders the main routed page content. This is used internally by the platform to inject the resolved {% block content %} — as a theme developer you generally won't call this yourself inside page templates (you use {% block content %}...{% endblock %} instead), but it's used by sitepack_content_legal()-style helpers and internal layout composition.
sitepack_elements(elements)
Renders an array of merchant-authored content elements (rich text blocks, image blocks, banners, etc. added via the page/category editor) in order.
- Parameters:
elements(array): The elements array, e.g.category.elementsAboveorcategory.elementsBelow.
- Example Usage:
{% if category.elementsAbove %}<section class="category-elements-above">{{ sitepack_elements(category.elementsAbove) }}</section>{% endif %}
sitepack_content_legal()
Renders the active legal document's rich-text body (Terms & Conditions, Privacy Policy, Return Policy, etc.) on templates/legal.twig. The body is served in the language the page is being read in, so a visitor on /nl/legal/privacy-policy gets the Dutch text once the merchant has translated it, and the links it renders to the other policies stay on that language.
{% block content %}
{{ sitepack_content_legal() }}
{% endblock %}
sitepack_footer()
Renders the merchant's configured footer link columns.
<footer>
{{ sitepack_footer() }}
</footer>
sitepack_legal_links()
Renders a simple inline list of links to the store's published legal pages (Privacy Policy, Terms, etc.), handy for a footer legal bar. The links are locale-aware: rendered under /de/ they point at /de/legal/…, so a translated visitor stays on their language.
<div class="footer-legal-links">
{{ sitepack_legal_links() }}
</div>
sitepack_copyright(showPolicies, showSiteMapLink, showShortDescription)
Renders the copyright line (e.g., © 2026 Store Name. All rights reserved.), with optional extras.
- Parameters:
showPolicies(boolean, optional, defaultfalse): Also render legal policy links.showSiteMapLink(boolean, optional, defaultfalse): Also render a link to the sitemap page.showShortDescription(boolean, optional, defaultfalse): Also render the store's short description text.
- Example Usage:
<footer>{{ sitepack_footer() }}{{ sitepack_copyright(true, true, false) }}</footer>
sitepack_icon(name)
Renders an inline SVG icon from SitePack's built-in icon library. See the Icons Reference for the full catalog of available names.
{{ sitepack_icon('angle-right-solid') }}
sitepack_live_search()
Renders the full interactive live-search UI (search input, predictive results dropdown, and modal wrapper).
{{ sitepack_live_search() }}
sitepack_live_search_icon()
Renders just the search trigger icon (used to open the live search modal via JavaScript), for placing inside a compact header nav item.
<li class="nav-icon nav-search-icon" id="nav-search">
{{ sitepack_live_search_icon() }}
</li>
sitepack_shopping_cart_icon(icon, showCount)
Renders the shopping cart icon with a live item-count badge.
- Parameters:
icon(string, optional): Icon name to render (see Icons Reference). Default:'shopping-bag-solid'.showCount(boolean, optional): Whether to show the live quantity badge. Default:true.
- Example Usage:
<li class="nav-icon nav-shopping-cart" id="nav-cart">{{ sitepack_shopping_cart_icon() }}</li>
sitepack_category_filters()
Renders the interactive faceted-filter sidebar UI (price ranges, brand checkboxes, attribute filters) for the current category listing.
<div id="sidebar-category-filters">
{{ sitepack_category_filters() }}
</div>
sitepack_cart()
Renders the full interactive shopping cart contents — line items, quantity controls, totals, and checkout button — on templates/cart.twig.
{% if cart.totalProducts > 0 %}
<div class="cart">
{{ sitepack_cart() }}
</div>
{% endif %}
sitepack_account()
Renders the entire customer account section on templates/account.twig: the sign-in form and the registration form when logged out, and the dashboard, orders overview, single order and settings panels when logged in. Which one appears is decided for you from the customer's session and the URL — you do not branch on it yourself. All strings are translated by SitePack, and every account_* template hook is emitted, so a theme's whole account.twig is one call:
{% extends 'layouts/base.twig' %}
{% block content %}
{{ sitepack_account() }}
{% endblock %}
- Styling: it ships base styling for its canonical classes (
.account,.account__layout,.account-nav,.account__heading,.order-item,.account-auth,.account-field, …), built only from the always-present theme variables, so it looks right in any theme with no extra CSS. Override those classes in yourassets/css/theme.cssto match your design — you do not need to reproduce the markup to restyle it. - Note: this replaces the per-theme account markup that themes used to carry. You no longer add
account.*translation keys for it, and you keep linking into the section withsitepack_account_slug()from your header.
sitepack_cookies()
Renders the merchant's configured GDPR/ePrivacy cookie consent banner. Place this near the end of layouts/base.twig, before your closing scripts.
{{ sitepack_cookies() }}
The banner (#sitepack-cookiebar) is shown right after the page's first paint, not before it, so a styled consent dialog never becomes the page's Largest Contentful Paint at load. Its texts follow the language of the page being read. Do not preload an image you use in the banner: it would compete with your hero for bandwidth. And keep any image in it smaller than your hero on phones. An image that paints later still counts for LCP when it is the largest thing on screen, and Chrome measures a background image by the area it paints (with background-size: cover that is the full width of the dialog).
sitepack_scripts()
Reserves the position, right before </body>, where platform-level scripts belong. Always include this call in layouts/base.twig for forward compatibility, even though it may not always render visible output today.
{{ sitepack_scripts() }}
<script src="{{ 'js/theme.js' | asset_url }}" type="module"></script>
</body>
app_block 'name'
This is a Twig tag, not a function — always use the {% app_block 'name' %} block syntax. It renders any content that installed third-party apps have registered for the given hook name (see the Template Hooks Guide for the full list of standard hook names). If no app targets that hook, it silently renders nothing.
<div class="custom-promotional-strip">
{% app_block 'header_top' %}
</div>
sitepack_price(priceCents, currency)
Converts an integer cents value into a formatted, human-readable currency string.
- Parameters:
priceCents(integer): Price value represented in cents (e.g.,2450).currency(string, optional): Currency symbol. Default:'€'.
- Example Output:
€ 24,50 - Example Usage:
<span class="price-tag">{{ sitepack_price(product.priceCents) }}</span>
sitepack_product_options()
Renders the product's variant/options picker as HTML on a product.twig page, so you no longer copy the whole selector block into every theme. It picks the right UI for the product automatically:
- the option-set picker for shops on the single-product options & variants model — the markup is rendered by the function itself (colour options as swatches, others as text buttons), and its styling and behaviour ship with SitePack too, so a theme needs no variant template, CSS or JavaScript of its own;
- the legacy attribute chips (
pdp-option) for older linked-variant products; - a flat sibling list for shops from before the variant system.
The product_variants_before and product_variants_after app blocks are emitted around it, so installed apps keep hooking the variant area without the theme wiring them up. It renders nothing when the product has no variants, so it is safe to call unconditionally.
- Parameters: none.
- Returns: an HTML string (empty when the product has no options or variants).
- Example Usage:
{# In templates/product.twig, where the variant selector belongs #}{{ sitepack_product_options() }}
- Note: because the app blocks are emitted by the function, do not also add
{% app_block 'product_variants_before' %}/{% app_block 'product_variants_after' %}around the call — that would render them twice.
Variant image. When a shopper picks a variant that has its own image, the picker shows that image as the main product photo. It finds the main photo without any theme markup: the largest visible image on the page that shows one of the product's own images, skipping clickable thumbnails. In a grid gallery that is the first photo. Variant images are part of product.images (after the product's own images, each once), so your gallery loop shows them as thumbnails without extra code. If your theme marks the current thumbnail with is-active, that class moves along with the picked variant. It also works the other way round: a click on a variant's photo in the gallery picks that variant. When several variants share one photo (one colour in every size), only the options they have in common are picked. A variant without an image brings the original product photo back. The page itself opens on the product's main photo; the image only follows the variant once the shopper picks one.
You only need to do something when the automatic choice is wrong for your layout:
- put
data-product-main-imageon the main<img>(or on its wrapper) to point the picker at it explicitly; - or take the swap over yourself: every pick raises a cancelable
sitepack:variant:changeevent onwindow, and callingpreventDefault()stops the built-in image swap.
window.addEventListener('sitepack:variant:change', (event) => {
const { productUuid, variant, image, initial } = event.detail;
if (initial || !image) {
return;
}
event.preventDefault(); // the theme's own gallery shows the variant image
myGallery.show(image.url, image.altText);
});
detail carries productUuid, variant (with uuid, optionValues, pricePretty, available and image), image ({url, thumbnail, altText} or null), initial (true for the selection the page opens with), trigger ('option' for a click on a value, 'image' for a click on a gallery photo, null on page load) and source: 'variant-picker'. The built-in swap only runs for trigger: 'option'; after a photo click your gallery already shows that photo.
sitepack_products(limit, type, category)
Returns a list of products as data (an array of product objects), so you can render a product row anywhere, most usefully on your templates/homepage.twig. A custom template like the homepage does not receive a curated frontpage collection, so this function is the supported way to pull products into it. Loop over the result and hand each item to your product card snippet.
- Parameters:
limit(integer, optional): How many products to return (1-48). Default:4.type(string, optional): Which selection to return —'newest','popular'or'sale'. Default:'newest'.category(string, optional): A category slug. When given, the selection is scoped to that category; when omitted (or empty), it spans the whole shop.
- Returns:
arrayof product objects — the same shape used everywhere else in the theme, soproduct.name,product.priceCents,product.pricePromoCents,product.url,product.first_image.thumbnail,product.brand, etc. are all available. Returns an empty array when the store has no store module or no matching products. - Example Usage:
{# In a custom "home" template: a "New in" product row #}{% set products = sitepack_products(8) %}{% if products|length > 0 %}<div class="product-grid">{% for product in products %}{% include 'snippets/product-card.twig' with { product: product } %}{% endfor %}</div>{% endif %}{# The most popular products in one category #}{% for product in sitepack_products(4, 'popular', 'jurken') %}{% include 'snippets/product-card.twig' with { product: product } %}{% endfor %}{# A sale / discount row across the whole shop #}{% for product in sitepack_products(8, 'sale') %}{% include 'snippets/product-card.twig' with { product: product } %}{% endfor %}
- Tip: Use
sitepack_products()on yourtemplates/homepage.twigfor the featured product grid (see Homepage), and anywhere else you want a product row (a category teaser on a landing page, related picks in the footer, and so on).
sitepack_categories(limit)
Returns the shop's main (top-level) categories as data (an array of category objects), so you can render a category row anywhere, most usefully on your templates/homepage.twig, which does not receive the catalog automatically. Loop over the result and hand each item to your category card snippet, the same way templates/products.twig renders its category listing.
- Parameters:
limit(integer, optional): Cap how many categories to return. Default:0, which returns all main categories.
- Returns:
arrayof category objects, each withcategory.name,category.slug,category.urlandcategory.backgroundImage.backgroundImageis the category's own image, or — when it has none — the image of the most popular product in that category, so a card always has a visual (it is an empty string when the category has neither). Returns an empty array when the store has no store module or no categories. - Example Usage:
{# In a custom "home" template: a row of the shop's main categories #}{% set categories = sitepack_categories() %}{% if categories|length > 0 %}<div class="category-grid">{% for category in categories %}<a href="{{ sitepack_category_slug(category.slug) }}" class="category-card">{% if category.backgroundImage %}<img src="{{ category.backgroundImage }}" alt="{{ category.name }}" loading="lazy"/>{% endif %}<span>{{ category.name }}</span></a>{% endfor %}</div>{% endif %}{# Just the first four, for a compact teaser #}{% for category in sitepack_categories(4) %}{% include 'snippets/category-card.twig' with { category: category } %}{% endfor %}
- Tip: Always link a category with
sitepack_category_slug(category.slug)rather than hardcoding/categories/…, so the link stays locale-correct on a multilingual shop.
Site content managed in the admin
The functions below render (or return) content a merchant manages centrally in the admin — the newsletter, the USPs, the topbar and the agenda. Prefer them over hardcoding this content in your templates or translation files: the merchant can then edit it, it is the same everywhere it appears, and it stays in sync (a free-shipping USP tracks the real shipping setting, an event drops off the agenda once it is in the past). See the overview for which content this replaces.
sitepack_newsletter_form()
Renders the complete newsletter subscription form as HTML: the intro text, the email field, the submit button (all from the merchant's newsletter settings), plus a CSRF token and a honeypot. The form works without JavaScript (it posts to /newsletter/subscribe) and is progressively enhanced by the SDK. It renders nothing when the merchant has the newsletter turned off, so it is safe to call unconditionally.
- Parameters: none.
- Returns: an HTML string (empty when the newsletter is disabled).
- Example Usage:
<section class="newsletter">{{ sitepack_newsletter_form() }}</section>
- Tip: if you want to build your own markup instead, the raw texts are on the
newsletterobject —newsletter.enabled,newsletter.texts.intro,newsletter.texts.button,newsletter.texts.confirmation(see Twig Objects).
sitepack_usps(placement)
Returns the site's reusable USPs as data (an array), so you can render them however your design needs. USPs are global: the merchant maintains one ordered list and the same set is available everywhere you call this. A USP entered once is reused across the site without being re-typed. Free-text values are already resolved for the page language and the free-shipping threshold is interpolated.
- Parameters:
placement(string, optional): kept for backwards compatibility only. USPs no longer carry per-placement flags, so this argument is ignored — every visible USP is returned regardless of what you pass. Prefer callingsitepack_usps()with no argument and slicing the result yourself (| slice(0, 3)) if a spot should show fewer.
- Returns:
arrayof USP objects:usp.uuid(string),usp.title(string),usp.text(string, may be empty),usp.icon(an icon name forsitepack_icon(), may be empty). Order follows the merchant's sort order in the admin. Returns an empty array when the site has no visible USPs — render nothing in that case so you keep no empty block. - Example Usage:
{% set usps = sitepack_usps() %}{% if usps|length >= 1 %}<div class="usp-strip">{% for usp in usps %}<div class="usp">{% if usp.icon %}<span class="usp__icon">{{ sitepack_icon(usp.icon) }}</span>{% endif %}<span class="usp__title">{{ usp.title }}</span>{% if usp.text %}<span class="usp__text">{{ usp.text }}</span>{% endif %}</div>{% endfor %}</div>{% endif %}
sitepack_locations()
Returns the site's visible locations (the business addresses the merchant keeps under Instellingen → Locaties) as data, so a theme can show a phone number in the header, an address in the footer or opening hours on a contact page without hardcoding any of it. Editing a location in the admin updates every place your theme prints it. Hidden locations are left out.
- Parameters: none.
- Returns:
arrayof location objects, in the admin's order (by name). Every text field is a string and is''when the merchant left it empty, so you can test it directly with{% if location.phone %}. Returns an empty array when the site has no visible locations — render nothing in that case. See the field list for every key; the ones you will use most arelocation.name,location.address,location.postalCode,location.city,location.email,location.phone,location.phoneHref(a ready-madetel:link) andlocation.openingHours(one entry per day, or empty when the location has no opening hours). - Example Usage:
{# Header: the phone number of the first location, next to the call to action #}{% set location = sitepack_locations()|first %}{% if location and location.phone %}<a class="header-phone" href="{{ location.phoneHref }}">{{ location.phone }}</a>{% endif %}{# Contact page: every location with its opening hours #}{% for location in sitepack_locations() %}<address><strong>{{ location.name }}</strong><br>{{ location.address }}<br>{{ location.postalCode }} {{ location.city }}</address>{% if location.openingHours %}<dl class="opening-hours">{% for day in location.openingHours %}<dt>{{ ('days.' ~ day.day)|trans }}</dt><dd>{{ day.closed ? 'opening_hours.closed'|trans : day.open ~ ' – ' ~ day.close }}</dd>{% endfor %}</dl>{% endif %}{% endfor %}
sitepack_locations()|firstisfalseon a site without locations, so the{% if location %}guard is required. Day names are not translated for you:day.dayis a fixed key (mon…sun), so map it to your own translation keys as above.
sitepack_reviews()
Returns the site's reviews as data: the company review score (average rating, its scale and the number of reviews) and items, the customer reviews themselves. Use it for a "9,4 out of 10, 632 reviews" badge and for a block of real customer quotes, instead of typing a score or testimonials into template fields that go stale.
Merchants manage their reviews in the admin (Instellingen → Reviews): they add them by hand, or import them with the 5-sterrenspecialist app, and they decide which ones are visible and in what order. The score comes from 5-sterrenspecialist when the app is connected; otherwise SitePack calculates it from the visible reviews that have a score (reviews.source is then sitepack). It is the same score SitePack puts in the site's AggregateRating structured data, so what the visitor sees and what search engines read stay identical.
- Parameters: none.
- Returns: an
array, or an empty array when the site has neither a score nor visible reviews. Render nothing, or your own fallback, in that case. See the field list. For the score you will mostly usereviews.averageScore(a number such as9.4),reviews.bestScore(the top of the scale),reviews.total(the number of reviews) andreviews.url(the public review page). A site can have reviews without a score: thenreviews.totalis0and the score fields arenull, so guard the score with{% if reviews.total %}. For quotes, loop overreviews.items|default([]): each item hasauthor,text,score,bestScore,date,date_formatted,labelandsource. - Example Usage:
The scores are numbers, not text: format them yourself with{% set reviews = sitepack_reviews() %}{# Hero stat: the live review score #}{% if reviews.total|default(0) %}<div class="hero-stat"><strong>{{ reviews.averageScore|number_format(1, ',', '.') }}</strong><span>{{ 'reviews.score_label'|trans({'%total%': reviews.total|number_format(0, ',', '.')}) }}</span></div>{% endif %}{# The three newest customer quotes; the block disappears when there are none #}{% set quotes = reviews.items|default([])|slice(0, 3) %}{% if quotes %}<section class="reviews">{% for review in quotes %}<figure class="review">{% if review.label %}<span class="review-label">{{ review.label }}</span>{% endif %}<blockquote>{{ review.text }}</blockquote><figcaption>{{ review.author|default('reviews.anonymous'|trans) }}, {{ review.date_formatted }}{% if review.score is not null %}({{ review.score }}/{{ review.bestScore }}){% endif %}</figcaption></figure>{% endfor %}</section>{% endif %}{# Footer: a link to the public review page #}{% if reviews.url|default('') %}<a href="{{ reviews.url }}" rel="noopener" target="_blank">{{ reviews.averageScore|number_format(1, ',', '.') }} / {{ reviews.bestScore }}</a>{% endif %}
number_formatso they follow your locale (9,4in Dutch,9.4in English). Keep labels such as "out of 632 reviews" or "Anonymous" in your translation files. Review texts are plain text written by customers: print them with{{ }}(escaped), never with|raw.
sitepack_topbar()
Renders the merchant-managed topbar as HTML. There is no free-form editor: the merchant turns the bar on and picks one of two fixed layouts in the admin (Thema → Topbalk), and this function prints the matching markup. Renders nothing when the topbar is turned off — or when the chosen layout has no content yet — so it leaves no empty strip and is safe to call unconditionally.
The two layouts are:
-
Standard — the first 1–3 of the site's USPs (title + icon, from the USP screen) on the left, and a chosen location's email and phone as
mailto:/tel:links on the right. Both are re-resolved live, so editing the USP screen or the location updates the bar. -
Text — a single centered rich-text line, sanitized to the safe-HTML allowlist on save (emojis and links allowed).
-
Parameters: none.
-
Returns: an HTML string (empty when the topbar is off or empty).
-
Markup you style: you do not control the inner markup, so target these classes in your stylesheet and provide styles for both layouts (the merchant can switch at any time):
<!-- Standard --><div class="topbar-standard"><div class="topbar-standard__usps"><span class="topbar-usp"><span class="topbar-usp__icon">…</span><span class="topbar-usp__label">…</span></span></div><div class="topbar-standard__contact"><a class="topbar-contact__item topbar-contact__email" href="mailto:…">…</a><a class="topbar-contact__item topbar-contact__phone" href="tel:…">…</a></div></div><!-- Text --><div class="topbar-text">…</div> -
Example Usage:
{# In snippets/header.twig — render the core topbar, print nothing when it is off #}{% set core_topbar = sitepack_topbar() %}{% if core_topbar %}<div class="topbar"><div class="topbar__inner">{{ core_topbar }}</div></div>{% endif %} -
Colours: the bar uses the theme's topbar colour (and its text colour). The merchant edits those straight from the topbar screen; they map onto your theme's topbar colour setting, so keep reading that setting for the strip's background rather than hardcoding a colour.
-
Mobile note: the standard layout is a two-column strip (USPs / contact) — collapse it as you see fit under 768px (a common choice is to keep the contact links and hide the USP strip, or wrap to two short rows). The text layout is one line; let it wrap or truncate.
sitepack_events(limit, scope)
Returns the site's events (the agenda) as data (an array). By default only upcoming events are returned; pass a scope to fetch past or all events. Each event's date is already formatted in the page language (the sandbox only allows a handful of filters, so formatting happens server-side) and each carries ready-to-print schema.org Event JSON-LD.
- Parameters:
limit(integer, optional): how many events to return. Default:5.scope(string, optional): which events to return —'upcoming'(default, soonest first),'past'(most recent first) or'all'(newest first). Any other value falls back to'upcoming'.
- Returns:
arrayof event objects:event.title,event.description,event.date_formatted(localised, e.g. "di 15 sep 2026"),event.start_date/event.end_date(YYYY-MM-DD),event.start_time/event.end_time(HH:MMor null),event.location(resolved place, may be empty),event.link,event.image, andevent.json_ld(a schema.org Event object). Returns an empty array when there are no matching events — render nothing in that case. - Example Usage:
{# upcoming events (default) #}{% set events = sitepack_events(6) %}{# past events, most recent first #}{% set past_events = sitepack_events(6, 'past') %}{% if events|length >= 1 %}<div class="agenda">{% for event in events %}<div class="agenda__row"><span class="agenda__date">{{ event.date_formatted }}</span><span class="agenda__name">{{ event.title }}</span>{% if event.location %}<span class="agenda__location">{{ event.location }}</span>{% endif %}</div>{{ sitepack_json_ld(event.json_ld) }}{% endfor %}</div>{% endif %}
- Tip: always emit
event.json_ldthroughsitepack_json_ld()(see below), never withjson_encode|raw— it renders a safe<script type="application/ld+json">block and gives you valid schema.orgEventstructured data for free, good for search visibility of markets, fairs and in-store days.
sitepack_blogs(limit)
Returns the site's most recent published blog posts as data (an array, newest first), so you can build a news / "latest from the blog" block on any template. This is the only way to pull blog posts into a custom template (such as your templates/homepage.twig), which does not receive the blog.articles listing. Dates are formatted server-side.
- Parameters:
limit(integer, optional): how many posts to return (clamped to1–24). Default:3.
- Returns:
arrayof blog objects:blog.title,blog.url(language-correct link to the post),blog.excerpt(preview text),blog.image(main image URL, may be empty),blog.created(YYYY-MM-DD),blog.date_formatted(e.g. "20-08-2026"),blog.tags(array of strings),blog.author(first_name,last_name,url) andblog.translations(per-language addresses, used bysitepack_blog_slug()). Returns an empty array when there are no posts — render nothing, or a fallback, in that case. - Example Usage:
{% set posts = sitepack_blogs(3) %}{% if posts|length >= 1 %}<div class="news-grid">{% for post in posts %}<a class="news-card" href="{{ sitepack_blog_slug(post) }}">{% if post.image %}<img src="{{ post.image }}" alt="{{ post.title }}">{% endif %}<div class="news-card__meta">{% if post.tags|first %}{{ post.tags|first }} · {% endif %}{{ post.date_formatted }}</div><h3>{{ post.title }}</h3><p>{{ post.excerpt }}</p></a>{% endfor %}</div>{% endif %}
- Tip: link each post with
sitepack_blog_slug(post), notpost.url— the helper readsblog.translationsand returns the address for the language being viewed (on a single-language store it falls back topost.url). Never build/blog/...yourself. On a store with no posts yet, keep a small hardcoded fallback so the section still looks complete.
Post types from apps
Apps can register their own content types, such as the cinemas of a cinema network, which the merchant manages under Content in the admin. See Post Types for how an app declares them. These functions read them in any template, and in app block templates too.
Results are cached by the storefront and refreshed automatically whenever the merchant changes an entry or its translation, so call them wherever you need them; see Caching.
sitepack_posts(type, limit)
Returns the visible entries of a post type as data (an array), in the order the post type sorts them. Works for public and private post types.
- Parameters:
type(string): the post type key, e.g.'cinemas'.limit(integer, optional): how many entries to return.0(default) returns all of them.
- Returns:
arrayof entries:entry.uuid,entry.type,entry.title,entry.slug,entry.url(path without language prefix,nullfor a private post type),entry.position,entry.fields(the app's fields by key) andentry.created/entry.modified. Returns an empty array when the post type does not exist on the site (the app is not installed) or has no visible entries, so always render a fallback or nothing. - Example Usage:
{% set cinemas = sitepack_posts('cinemas', 6) %}{% if cinemas|length >= 1 %}<div class="cinema-grid">{% for cinema in cinemas %}<a class="cinema-card" href="{{ sitepack_post_slug(cinema) }}">{% if cinema.fields.image %}<img src="{{ cinema.fields.image }}" alt="{{ cinema.title }}">{% endif %}<h3>{{ cinema.title }}</h3><p>{{ cinema.fields.city }}</p></a>{% endfor %}</div>{% endif %}
sitepack_post(type, slug)
Returns one visible entry of a post type by its slug, or null.
- Parameters:
type(string): the post type key.slug(string): the entry's slug.
- Returns: the entry object described above, or
null. - Example Usage:
{% set flagship = sitepack_post('cinemas', 'grand-central') %}{% if flagship %}<p>Our flagship: {{ flagship.title }} ({{ flagship.fields.screens }} screens)</p>{% endif %}
sitepack_post_slug(entry, absolute)
The address of an entry of a public post type in the language the page is being read in: /cinemas/grand-central, or /de/cinemas/grand-central under /en/. Pass a path instead of an entry for the overview: sitepack_post_slug('cinemas').
- Parameters:
entry(object|string): an entry fromsitepack_posts()/sitepack_post(), or a path.absolute(boolean, optional):truefor a full URL on the host the page is served from.
- Returns:
string. An empty string for an entry of a private post type, which has no page: test with{% if sitepack_post_slug(entry) %}before linking.
On the pages of a public post type itself, the entry arrives as post (post-<key>.twig) and the list as posts (posts-<key>.twig), each with post_type. See the templates table.
sitepack_json_ld(data)
Renders a schema.org JSON-LD object as a safe <script type="application/ld+json"> block. Use it for any structured-data object the platform hands you — most commonly event.json_ld from sitepack_events().
- Parameters:
data(object): a JSON-LD object (e.g.event.json_ld). An empty or missing value renders nothing.
- Returns: an HTML string (a complete
<script>tag, or empty). - Why this and not
json_encode|raw: the JSON is encoded server-side with HTML-hex escaping, so a value such as an event title of</script>…can never break out of the script element. The genericrawfilter is not available in the theme sandbox for exactly this reason. - Example Usage:
{% for event in sitepack_events(6) %}<div class="agenda__row">{{ event.title }}</div>{{ sitepack_json_ld(event.json_ld) }}{% endfor %}
- Note: you do not emit the page-level SEO JSON-LD yourself —
sitepack_head()already outputs it in the<head>.
calculate_text_color(backgroundColor)
Analyzes the luminosity of a hex background color and returns whichever of #000000 (black) or #ffffff (white) gives better text contrast.
- Parameters:
backgroundColor(string): A hex color code (e.g.,#ffffff).
- Example Usage:
<header style="background-color: {{ settings.header_color }}; color: {{ calculate_text_color(settings.header_color) }}">
calculate_brightness(hex, steps)
Lightens or darkens a hex color by a number of steps — useful for generating hover/active states from a single merchant-picked color.
- Parameters:
hex(string): Hex color code to adjust.steps(integer): Positive values lighten, negative values darken.
- Example Usage:
<style>.btn-primary { background-color: {{ settings.button_color }}; }.btn-primary:hover { background-color: {{ calculate_brightness(settings.button_color, -20) }}; }</style>
The Master Layout Integration Blueprint
Here is the production layouts/base.twig from the Official Theme Skeleton, showing how these functions compose together in practice:
<!DOCTYPE html>
<html lang="{{ site.locale }}">
<head>
<title>{{ sitepack_title() }}</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{ sitepack_head() }}
<link rel="stylesheet" href="{{ 'css/theme.css' | asset_url }}">
{% app_block 'head_after' %}
</head>
<body class="{{ body_class }}">
{% app_block 'body_start' %}
{% include 'snippets/header.twig' %}
<main>
{% block content %}{% endblock %}
</main>
{{ sitepack_breadcrumbs() }}
{% include 'snippets/footer.twig' %}
{{ sitepack_cookies() }}
{{ sitepack_scripts() }}
<script src="{{ 'js/theme.js' | asset_url }}" type="module"></script>
{% app_block 'body_end' %}
</body>
</html>
Exploring Further:
Now that you have mastered custom rendering functions, discover the custom data string filters available for theme templates inside the Custom Theme Filters Guide!