Skip to main content

Page Examples

The fastest way to understand a SitePack theme is to see the smallest template that still renders a working page. Every example below is a complete {% block content %} you can paste into the matching templates/*.twig file — the chrome (head, header, footer, cookies, scripts) comes from layouts/base.twig, so a page template only fills in its own content.

Each section lists the related functions you can reach for to go beyond the basics. Full signatures live in the Twig Functions reference; the data each template receives is documented in Twig Objects.

Start from the skeleton

Run sitepack theme:init to scaffold a theme that already contains all of these templates. The snippets here are stripped down on purpose — they show the minimum, not the finished design.


The shared layout

Every template extends 'layouts/base.twig' and fills the content block. The layout renders the page chrome once, so no page template repeats it:

layouts/base.twig
<!DOCTYPE html>
<html lang="{{ pageHreflang }}">
<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 }}">
</head>
<body>
{% include 'snippets/header.twig' %}
<main>{% block content %}{% endblock %}</main>
{% include 'snippets/footer.twig' %}
{{ sitepack_cookies() }}
{{ sitepack_scripts() }}
</body>
</html>

Related functions: sitepack_head(), sitepack_navigation(), sitepack_breadcrumbs(), sitepack_footer(), sitepack_cookies(), sitepack_scripts().


Home page (templates/homepage.twig)

The homepage is the most visited page of the site, so it gets its own dedicated template. templates/homepage.twig is a registered custom template: you declare its fields in theme.json under the reserved homepage key, and SitePack automatically assigns it as the site homepage (no manual page-builder setup). The SitePack AI Website builder fills those fields in when it generates a site, which is why the field labels in your manifest should plainly describe what each field holds. See the Homepage template section in the theme.json Reference for the matching field schema.

Because it is a custom template it does not receive collections.frontpage. You read the editable copy from the page object and pull in live content (products, categories, events, blog posts) with the sitepack_* data functions. Each section is wrapped in a show-* toggle (a boolean field) combined with a content check, so it renders only when the merchant switched it on and there is something to show. That one template then adapts to whatever the site is: a webshop turns the product and category rows on, a content website leans on the hero, intro and blog, an events site shows the agenda. See Toggling sections on and off for the pattern and the gotcha around the default filter.

Always ship a clear call to action

A homepage without an obvious next step converts badly. The example below renders a primary CTA button in the hero from the hero-cta-label / hero-cta-url fields, and repeats a closing CTA at the bottom. Keep at least the hero CTA in your own design.

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

{% block content %}
{% app_block 'homepage_before' %}

{# Hero: headline, supporting text and the primary call to action #}
<section class="home-hero"{% if page['hero-image'] %} style="background-image:url('{{ page['hero-image'] }}')"{% endif %}>
{% if page['hero-eyebrow'] %}<p class="home-hero__eyebrow">{{ page['hero-eyebrow'] }}</p>{% endif %}
<h1 class="home-hero__title">{{ page['hero-title'] | default(site.name) }}</h1>
{% if page['hero-subtitle'] %}<p class="home-hero__subtitle">{{ page['hero-subtitle'] }}</p>{% endif %}

{% if page['hero-cta-label'] and page['hero-cta-url'] %}
<a href="{{ page['hero-cta-url'] | ensure_link }}" class="btn btn-primary btn-lg home-hero__cta">
{{ page['hero-cta-label'] }}
</a>
{% endif %}
</section>

{# USP strip, newsletter and other central content come from core #}
{% set usps = sitepack_usps() %}
{% if usps | length >= 1 %}
<div class="usp-strip">
{% for usp in usps | slice(0, 4) %}
<div class="usp">
{% if usp.icon %}<span class="usp__icon">{{ sitepack_icon(usp.icon) }}</span>{% endif %}
<span class="usp__title">{{ usp.title }}</span>
</div>
{% endfor %}
</div>
{% endif %}

{# Intro: a short, content-managed paragraph (good for website themes) #}
{% if page['show-intro'] and (page['intro-title'] or page['intro-text']) %}
<section class="home-intro">
{% if page['intro-title'] %}<h2>{{ page['intro-title'] }}</h2>{% endif %}
{% if page['intro-text'] %}<p>{{ page['intro-text'] }}</p>{% endif %}
</section>
{% endif %}

{# Featured products: toggled on and only when the shop has products #}
{% set products = sitepack_products(8, 'popular') %}
{% if page['show-featured-products'] and products | length >= 1 %}
<section class="home-products">
<h2>{{ page['featured-title'] | default('Featured products') }}</h2>
<div class="product-grid">
{% for product in products %}
{% include 'snippets/product-card.twig' with { product: product } %}
{% endfor %}
</div>
</section>
{% endif %}

{# Shop categories: toggled on and only when the shop has categories #}
{% set categories = sitepack_categories(6) %}
{% if page['show-categories'] and categories | length >= 1 %}
<section class="home-categories">
<h2>{{ page['categories-title'] | default('Shop by category') }}</h2>
<div class="category-grid">
{% for category in categories %}
{% include 'snippets/category-card.twig' with { category: category } %}
{% endfor %}
</div>
</section>
{% endif %}

{# Agenda: toggled on and only when there are upcoming events #}
{% set events = sitepack_events(4) %}
{% if page['show-events'] and events | length >= 1 %}
<section class="home-agenda">
<h2>{{ page['events-title'] | default('Upcoming events') }}</h2>
{% 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 %}
</section>
{% endif %}

{# Latest from the blog: toggled on and only when there are posts #}
{% set posts = sitepack_blogs(3) %}
{% if page['show-blog'] and posts | length >= 1 %}
<section class="home-blog">
<h2>{{ page['blog-title'] | default('Latest news') }}</h2>
<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 %}
<h3>{{ post.title }}</h3>
<p>{{ post.excerpt }}</p>
</a>
{% endfor %}
</div>
</section>
{% endif %}

{# Closing call to action #}
{% if page['show-closing-cta'] and page['closing-cta-label'] and page['closing-cta-url'] %}
<section class="home-closing-cta">
{% if page['closing-cta-title'] %}<h2>{{ page['closing-cta-title'] }}</h2>{% endif %}
{% if page['closing-cta-text'] %}<p>{{ page['closing-cta-text'] }}</p>{% endif %}
<a href="{{ page['closing-cta-url'] | ensure_link }}" class="btn btn-primary btn-lg">
{{ page['closing-cta-label'] }}
</a>
</section>
{% endif %}

{% if page['show-newsletter'] %}
<section class="home-newsletter">
{{ sitepack_newsletter_form() }}
</section>
{% endif %}

{% app_block 'homepage_after' %}
{% endblock %}
No homepage.twig? The editor takes over

When a theme ships no homepage.twig, SitePack falls back to a homepage built in the page-builder editor. That works, but for the single most viewed page of a site a dedicated, well-structured homepage.twig gives noticeably better results, so ship one.

Related functions: sitepack_products(limit, type, category) for product rows ('newest', 'popular', 'sale', optionally scoped to a category), sitepack_categories(limit) for a category row, sitepack_events(limit, scope) for the agenda, sitepack_blogs(limit) for a news block, sitepack_usps() and sitepack_newsletter_form() for the central content the merchant manages in the admin, and the ensure_link filter to normalise CTA URLs.


Product page — templates/product.twig

A working product page needs four things: a title, a price, the variant/options picker, and an add-to-cart form. sitepack_product_options() renders the entire variant block (option-set picker, legacy variant chips, or the flat sibling list — whichever the product has) and emits the product_variants_before/after app blocks itself:

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

{% block content %}
<article class="product-detail">
<div class="product-gallery">
{% for image in product.images %}
<img src="{{ image.thumbnail }}" alt="{{ product.name }}">
{% endfor %}
</div>

<div class="product-info">
<h1>{{ product.name }}</h1>

{% if product.pricePromoCents > 0 %}
<span class="price">{{ sitepack_price(product.pricePromoCents) }}</span>
{% else %}
<span class="price">{{ sitepack_price(product.priceCents) }}</span>
{% endif %}

<form action="/cart/add" method="post">
{{ sitepack_product_options() }}

<input type="hidden" name="id" value="{{ product.uuid }}">
<input type="number" name="quantity" value="1" min="1">
<button type="submit">{{ 'product.add_to_cart' | trans }}</button>
</form>
</div>
</article>
{% endblock %}
Do not wrap the app blocks yourself

sitepack_product_options() already emits {% app_block 'product_variants_before' %} and {% app_block 'product_variants_after' %}. Wrapping the call in those blocks renders them twice.

Related functions: sitepack_price(priceCents) for every amount (never format cents yourself), sitepack_product_options() for the picker, sitepack_product_slug(item) for links, sitepack_icon(name) for glyphs, and sitepack_json_ld(data) for structured data. See product in Twig Objects for the full field list (brand, sku, facets, reviewCount, …).


Category page — templates/category.twig

A category page renders its title, an optional filter sidebar, and the product listing it receives in products:

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

{% block content %}
<h1>{{ category.name }}</h1>

{{ sitepack_category_filters() }}

<div class="product-grid">
{% for product in products %}
{% include 'snippets/product-card.twig' with { product: product } %}
{% endfor %}
</div>
{% endblock %}

Related functions: sitepack_category_filters() for the ready-made filter UI, sitepack_category_slug(item) for sub-category links, and sitepack_elements(elements) to render CMS blocks the merchant placed above or below the listing (category.elementsAbove / category.elementsBelow). Pagination fields (total_pages, current_page) are described in Twig Objects.


Content page — templates/page.twig

A CMS page is entirely merchant-managed, so the template is a single call:

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

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

Related functions: sitepack_content() renders the page body built in the editor; sitepack_elements(elements) renders a specific element list; sitepack_content_legal() renders legal/policy pages.


Cart page — templates/cart.twig

The cart is fully rendered (and kept in sync by the SDK) by one function:

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

{% block content %}
<h1>{{ 'cart.title' | trans }}</h1>

{% if cart.totalProducts > 0 %}
{{ sitepack_cart() }}
{% else %}
<p>{{ 'cart.empty' | trans }}</p>
{% endif %}
{% endblock %}

Related functions: sitepack_cart() for the line items and totals, sitepack_shopping_cart_icon() for the header mini-cart, sitepack_price(priceCents) for any amount you print yourself.


Blog & article — templates/blog.twig, templates/article.twig

The blog index loops over blog.articles; the article page prints one article:

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

{% block content %}
<h1>{{ blog.title }}</h1>
{% for article in blog.articles %}
{% include 'snippets/blog-card.twig' with { article: article } %}
{% endfor %}
{% endblock %}
templates/article.twig
{% extends 'layouts/base.twig' %}

{% block content %}
<article>
<h1>{{ article.title }}</h1>
{% if article.image %}<img src="{{ article.image }}" alt="{{ article.title }}">{% endif %}
<div>{{ article.content | sitepack_html }}</div>
</article>
{% endblock %}

Related functions & filters: print rich text through the sitepack_html filter (never raw), sitepack_blog_slug(item) for links, and sitepack_json_ld(data) for article structured data.


Search results — templates/search.twig

Search hands you keywords and a flat results list where each row already carries its type, url, title, image and price:

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

{% block content %}
<h1>{{ 'search.results_for' | trans({'%query%': keywords}) }}</h1>

{% for result in results %}
<a href="{{ result.url | ensure_link }}">
{% if result.image %}<img src="{{ result.image }}" alt="{{ result.title }}">{% endif %}
<span>{{ result.title }}</span>
{% if result.price %}<span>{{ result.price }}</span>{% endif %}
</a>
{% else %}
<p>{{ 'search.no_results' | trans }}</p>
{% endfor %}
{% endblock %}
result.price is already formatted

Unlike a product's priceCents, a search result's price is a ready-made string ("€ 24,50"). Print it directly with {{ result.price }} — do not pass it to sitepack_price(), which expects integer cents.

Related functions & filters: sitepack_live_search() for the instant-search box in the header, and the ensure_link filter to normalise any URL you output.


Keep the app blocks

The snippets above omit {% app_block '…' %} hooks for brevity, but a real template must keep them — installed apps inject markup there. See the Template Hooks guide for the full list, and always add symmetric …_before / …_after hooks around any new section.