Skip to main content

Theme Manifest & Settings Configuration (theme.json)

The theme.json file is the mandatory manifest at the root of every SitePack theme. It registers your theme's identity with the platform and declares two kinds of merchant-facing customization:

  1. Customizer Settings — simple color/dimension/option inputs that appear in the SitePack Theme Editor and control CSS custom properties in your theme.
  2. Custom Templates — structured, headless-style content templates (like a landing page) that merchants can fill in without touching code.

:::info Format Changed If you've seen older sample themes with a separate config/settings_schema.json file and settings grouped into named categories ({ "name": "Colours", "settings": [{ "id": ..., "label": ... }] }) — that format is retired. The current schema (below) lives entirely inside theme.json and uses a flat settings array with no category wrapper and no id field. label is still read, though — supply it so the Theme Editor shows a readable name instead of a humanised key. Building a new theme from the Official Theme Skeleton already gives you the current format — this page describes what's actually in that skeleton. :::


The Root Manifest Structure​

Here is the complete, real theme.json from the Official Theme Skeleton:

{
"uuid": "sitepack-theme-skeleton",
"name": "Base Skeleton",
"author": "SitePack Team",
"version": "1",
"supports_site": true,
"supports_online_store": true,
"settings": [
{
"type": "colour",
"key": "header-background-color",
"label": "Header background",
"default": "#ffffff"
},
{
"type": "colour",
"key": "footer-background-color",
"default": "#3f434a"
}
],
"templates": []
}

Root Metadata Fields​

  • uuid (string, required): The theme's unique identifier. When you run sitepack theme:init, the CLI requests a fresh UUID from the SitePack API and writes it here automatically — you don't need to invent one by hand. For a theme directory you already have, sitepack theme:link does the same.
  • name (string, required): The display name shown in the Theme Editor and (if published) the Theme Marketplace.
  • author (string, required): The developer or agency name. sitepack theme:init fills this in from your logged-in profile automatically.
  • version (integer, required): A plain incrementing integer ("1", "2", "3"...) — not Semantic Versioning. You don't need to bump this yourself: every time you run sitepack theme:publish, SitePack assigns and returns the next version number automatically, and the CLI prints it to your terminal (✅ Theme published successfully! New version: 4). Treat this field as read-only/informational in your local theme.json.
  • supports_site (boolean, required): Set true if the theme can be used for standard (non-commerce) websites.
  • supports_online_store (boolean, required): Set true if the theme supports e-commerce storefront features (product, category, cart templates).
  • settings (array, optional): Your customizer settings — see below. Defaults to an empty array if omitted.
  • templates (array, optional): Your custom page templates — see below. Provide [] if you don't need any.
  • platform_colors (object, optional): Fills SitePack's core brand colours from your own colour settings. See Mapping Core Colours to Your Settings below.

Defining Customizer Settings ("settings")​

The "settings" array is a flat list — there is no grouping/category wrapper. Each entry becomes one input in the SitePack Theme Editor, and its resolved value becomes available in your Twig templates and as a --key-named CSS custom property.

Setting Object Fields​

  • key (string, required): The unique identifier for this setting. Used to look it up in Twig and to name its CSS variable.
  • type (string, required): The input type. Supported values:
    • colour — a color picker, returning a hex string (note the British spelling; color is accepted as an alias but colour is canonical).
    • pixels — a numeric input for pixel-based dimensions (e.g., border radius, container width).
    • font — a font picker. The value is a font key with underscores (Open_Sans).
    • options — a dropdown/select input for a small set of predefined choices. Add an options array alongside it.
    • text, textarea, image, list — content-shaped settings. These are not meant to drive CSS.
    • file: a downloadable document, such as a PDF brochure. The merchant uploads it in the Theme Editor; the value is the url of the uploaded file. See File fields.
    • boolean: an on/off toggle in the Theme Editor, for a site-wide switch such as showing a top bar. The value is "1" (on) or "0" (off), so a plain {% if %} works in Twig. Write the default as true or false.
    • media: an image or any other uploadable file. The value is the uploaded file's url. Prefer image for pictures and file for documents; media is for a slot that can hold either.
  • default (required): The fallback value used until a merchant changes it.
  • label (string, optional but recommended): The text shown next to the input in the Theme Editor. Without it the Admin falls back to a humanised version of the key (header-background-color → "Header background color").

:::caution pixels defaults must carry their unit Write "default": "8px", not "default": 8. A setting's default is emitted into :root verbatim, so a bare number produces the invalid declaration --card-border-radius: 8. (The unit is only appended automatically when a merchant saves a value in the Admin — which means a bare-number default looks fine the moment anyone touches the setting, and broken for everyone who does not.) :::

Only colour, pixels and font settings are intended to resolve to CSS custom properties. Every declared key is emitted as a --key variable regardless of type, so a text or image setting will also appear in :root — just not usefully.

Complete Example​

{
"uuid": "your-theme-uuid",
"name": "Summit Minimalist",
"author": "CodeBrothers",
"version": "1",
"supports_site": true,
"supports_online_store": true,
"settings": [
{ "type": "colour", "key": "text-color", "label": "Body text colour", "default": "#333333" },
{ "type": "colour", "key": "background-color", "label": "Page background", "default": "#ffffff" },
{ "type": "colour", "key": "header-background-color", "label": "Header background", "default": "#ffffff" },
{ "type": "colour", "key": "header-text-color", "label": "Header text", "default": "#333333" },
{ "type": "pixels", "key": "card-border-radius", "label": "Card corner radius", "default": "8px" }
],
"templates": []
}

Reading Settings Values​

You do not need to read settings in Twig at all. SitePack already emits a :root block containing a --key custom property for every setting you declare — merchant value where one is set, your default otherwise. It arrives via the stylesheet that sitepack_head() links, so all your CSS has to do is consume it:

/* assets/css/theme.css */
header {
background: var(--header-background-color);
}

That is the whole mechanism. Change the setting in the Admin, the variable changes, the site follows.

:::danger Do not emit your own :root block A <style> block that writes --header-background-color: … in your layout is rendered after the platform stylesheet, so it overrides the merchant's real choice with whatever value your template computed. The site then ignores the Theme Editor entirely. :::

Settings whose type is not colour, pixels or font (i.e. text, textarea, image, list) are content rather than styling. Read those on a custom template through the page object — see Custom Templates Manifest Configuration below.

:::tip You Get Brand Colors for Free You do not need to declare settings for basic brand colors, links, fonts, border radius, or container width — SitePack always exposes the merchant's core Admin → Design settings as ready-to-use CSS variables (--main-color, --link-color, --link-active-color, --link-text-decoration, --border-radius, --container-width, --heading-font, --body-font, and more), automatically injected by sitepack_head(). Reserve your own theme.json settings for things specific to your design that aren't already covered — like per-section background colors. See the Twig Objects Guide for the full list. :::

Mapping Core Colours to Your Settings​

If your theme builds its palette on its own colour settings (say brand-color), the core --main-color would still carry the merchant's Admin → Design value or the SitePack default. Your own pages can bridge that in CSS, but pages SitePack renders without your stylesheet, like the checkout, only see the core variables. Map them in theme.json so those pages follow your palette too:

{
"uuid": "my-theme",
"settings": [
{ "type": "colour", "key": "brand-color", "label": "Brand colour", "default": "#5d6c5e" },
{ "type": "colour", "key": "brand-text-color", "label": "Text on the brand colour", "default": "#ffffff" }
],
"platform_colors": {
"main-color": "brand-color",
"main-color-text": "brand-text-color"
}
}
  • The keys you can map are main-color, main-color-text, secondary-color, secondary-color-text, link-color and link-active-color.
  • Each value must be the key of a colour setting your theme declares; anything else is ignored.
  • The mapped core variable takes the value of your setting: the merchant's value when they changed it, otherwise your default.
  • Map main-color without main-color-text (or secondary-color without secondary-color-text) and SitePack derives a readable black or white text colour for you.
  • A mapped colour no longer shows as a separate picker in the admin, so merchants edit it in one place: your setting.
  • When a new customer uploads a logo in the get-started onboarding, its brand colour goes into the setting you map main-color to (and its text colour into the one for main-color-text). The rest of your palette stays as you designed it. Without a mapping, the logo colour is stored under main-color itself, so map it if your buttons use your own setting.

Headless-Style Custom Layout Templates​

Beyond simple settings, theme.json lets you declare custom templates: a Twig file you design once, paired with a set of structured content fields a merchant fills in through the Admin — no drag-and-drop page builder, no broken layouts.

:::note Don't recreate core content as template fields Template fields are for content that is specific to one page (a hero title, a landing-page intro). Content that is reused across the site and managed centrally — the newsletter, USPs, the topbar and the agenda — comes from core through dedicated functions, not from theme.json fields. Don't add fields like usp-1-title, agenda-items, a phone number, a review score or newsletter texts to your manifest; use sitepack_usps(), sitepack_events(), sitepack_topbar(), sitepack_locations(), sitepack_reviews() and sitepack_newsletter_form() instead. See Content that comes from core. :::

Custom Templates Manifest Configuration​

Append a "templates" array to your theme.json:

{
"templates": [
{
"key": "landing-page",
"name": "Landing page",
"fields": [
{ "key": "hero-title", "type": "text", "label": "The main hero title" },
{ "key": "hero-sub-head", "type": "text", "label": "The main hero sub heading" },
{ "key": "hero-sub-paragraph", "type": "textarea", "label": "The main hero sub paragraph" },
{ "key": "hero-image", "type": "image", "label": "The main hero image" },
{ "key": "hero-cta-label", "type": "text", "label": "The main hero CTA label" },
{ "key": "hero-cta-url", "type": "text", "label": "The main hero CTA URL" },
{ "key": "usps", "type": "list", "label": "USPs" }
]
}
]
}

Template Registration Properties​

  • key (string, required): Unique identifier for the template. Must match the Twig filename: templates/{key}.twig (e.g., templates/landing-page.twig).
  • name (string, required): The human-friendly label shown in the template picker in the SitePack Admin.
  • fields (array, required): The content fields merchants fill in for pages using this template.

Field Entry Schema​

  • key (string, required): Lookup identifier for the field — used as a bracket-notation key in Twig.
  • type (string, required): One of text, textarea, image, file, media, list, boolean, or icon.
    • A file field is a download, such as a PDF with specifications or a price list. See File fields.
    • A boolean field renders as an on/off toggle in the Admin and is how you let a merchant show or hide a section (see Toggling sections on and off). The stored value is "1" (on) or "0" (off); over MCP and the public API send true or false.
    • An icon field renders an icon picker in the Admin. Its value is an icon name from SitePack's built-in icon library, which you render with sitepack_icon(). See the Icons Reference for the available names.
  • label (string, required): Descriptive text shown next to the field in the Admin editor.
  • default (optional): The value used until the merchant changes it. page carries it for every field the merchant never filled in; a saved value always wins, even an empty one or a toggle switched off. Most useful on a boolean field: set "default": true for a section you want visible out of the box. A boolean with no default starts off.
  • translatable (boolean, optional): Forces whether the field's value is offered for translation when the merchant buys an extra language. Leave it out and SitePack decides, see below. A boolean toggle and an icon name are never offered for translation (neither is language).

An icon field is read like any other field and handed straight to sitepack_icon():

{% if page['feature-icon'] %}
<span class="feature__icon">{{ sitepack_icon(page['feature-icon']) }}</span>
{% endif %}

File fields​

Use a file field for anything a visitor downloads: specifications, a brochure, a price list, terms. In the Admin the merchant sees the file that is there now (its name and full url) with buttons to upload, replace or remove it. The file is virus-scanned and stored on SitePack's own file storage, so the download keeps working without depending on another website.

  • Accepted files: PDF, Word (.doc, .docx), Excel (.xlsx) and PowerPoint (.ppt, .pptx), up to 10 MB.
  • The value in Twig is the file's absolute url, or empty when nothing is uploaded. Always guard it, so an empty slot does not render a dead button.
  • Over MCP and the public API a file field only accepts the url of a file in the site's own media library. An agent imports the PDF with the upload_media tool (as a data:application/pdf;base64,... URI or a public url ending in .pdf) and writes the url it returns into template_fields. A link to another website is refused.
{ "key": "specs-label", "type": "text", "label": "Download: button text" },
{ "key": "specs-pdf", "type": "file", "label": "Download: PDF file" }
{% if page['specs-pdf'] %}
<a href="{{ page['specs-pdf'] }}" class="btn-secondary" target="_blank" rel="noopener">
{{ page['specs-label']|default('Download the specifications') }}
</a>
{% endif %}

Which Fields Get Translated​

A page built on your template renders from these field values, not from the page builder, so they are the copy a visitor actually reads. When the merchant buys an extra language, SitePack offers them for translation automatically:

  • text, textarea and list fields are offered — each list item separately, labelled with the label you gave the field.
  • image, file and media fields are not: an asset is not language. All languages share the same file, so give each language its own file field when a document exists per language (specs-pdf-nl, specs-pdf-en).
  • Within the offered types, a value that is plainly not a sentence is skipped as well — a URL or path, a price or a bare number, an icon class, a true/false flag. That keeps hero-cta-url out of the translation editor even though it is typed text.

Set translatable explicitly when one of your fields is the exception:

{
"fields": [
{ "key": "hero-cta-url", "type": "text", "label": "CTA URL", "translatable": true },
{ "key": "tracking-label", "type": "text", "label": "Tracking label", "translatable": false }
]
}

A field the merchant has not translated falls back to the master value, so a half-translated language still renders a complete page.

Name your fields for what they hold

The automatic rule reads the last segment of the key too: …-url, -link, -slug, -icon, -class, -param, -id, -email and -phone are treated as machine values. Naming a link field hero-cta-url rather than hero-cta-2 gets you the right behaviour for free.

See Theme Translations for what the merchant sees.

Accessing Custom Template Fields in Twig​

Field values are exposed on the page object, keyed by the field's key. Because keys commonly contain hyphens, always use bracket notation:

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

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

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

{% if page['usps'] %}
<ul class="landing-usps">
{% for usp in page['usps'] %}
<li>{{ usp.label | default(usp) }}</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

The Homepage Template (templates/homepage.twig)​

The homepage is a custom template like any other, with one special property: declare a template whose key is homepage (or index or home, which many existing themes use) and SitePack treats it as the theme's home template. A key built around "home", such as restaurant-home or home-webshop, is recognised too; when a theme has several, index, homepage and home win, in that order. When SitePack builds a new site, it puts the homepage on this template and fills its fields (see below). On an existing site the merchant switches the homepage to it under Pages > Home > Template; the matching fields then appear in the SitePack Admin as the homepage's content editor.

:::info Why a dedicated homepage template The homepage is the most visited page of a site, so it deserves a structured template rather than a hand-assembled page. When a theme ships no homepage.twig, SitePack falls back to a homepage built in the page-builder editor. That works, but in practice it gives weaker results for exactly the page that matters most, so ship a homepage.twig. :::

The AI Website builder fills these fields​

When a new customer creates a site through SitePack's get-started onboarding, the SitePack AI Website builder reads your home template's field schema and fills it from what the customer told us (business, industry, goals, chosen look, their USPs). The same happens when the customer switches to your theme on the last onboarding screen: the homepage is rewritten for your fields. It can only do that well if every field says what it is for, so:

  • Give each field a clear, descriptive label (what the field holds and where it shows up), not a terse key echo. The label is the instruction the builder writes from.
  • Name keys for their content (hero-cta-url, intro-text), so the automatic translation rules and the builder both infer the right treatment.
  • Always include a call-to-action: a label field and a URL field for a primary button in the hero. A homepage without an obvious next step converts poorly, and the builder will populate a CTA when the fields are there.

What the builder does per field type:

FieldWhat it gets
text, textarea, list, iconWritten for this business, in the site's language. icon gets a Font Awesome 6 class such as fa-solid fa-truck-fast.
optionsOne of the options you list, or nothing.
imageThe customer's own uploaded photos first, then stock photos that match the business. Never an unrelated picture: a field without a good photo stays empty, so your template's fallback shows.
A link (key ending in -url or -link, or a label that mentions a URL)Only the path of a page that exists on the new site, such as /contact.
A category slug (the label mentions "slug")Only a category the new site has.
boolean, media, file, colour and a list keyed sectionsLeft to your default.

The builder never invents facts: figures, ratings, review counts, prices, years or awards stay empty unless the customer gave them, and a statistic field (a key containing stat, rating, review or score) that would get a number is left out. Design those sections so they hide when the field is empty.

Example homepage template registration​

Add this alongside any other entries in your theme.json templates array. Every field uses one of the basic content types (text, textarea, image, list); the template decides what else to show (product rows, categories, events, blog) from live data, so those do not need fields.

{
"templates": [
{
"key": "homepage",
"name": "Homepage",
"fields": [
{ "key": "hero-eyebrow", "type": "text", "label": "Hero: small label above the title (optional)" },
{ "key": "hero-title", "type": "text", "label": "Hero: main headline" },
{ "key": "hero-subtitle", "type": "textarea", "label": "Hero: supporting text below the headline" },
{ "key": "hero-image", "type": "image", "label": "Hero: background or feature image" },
{ "key": "hero-cta-label", "type": "text", "label": "Hero: call-to-action button text" },
{ "key": "hero-cta-url", "type": "text", "label": "Hero: call-to-action button link (page, product or category)" },

{ "key": "show-intro", "type": "boolean", "label": "Show the intro section", "default": true },
{ "key": "intro-title", "type": "text", "label": "Intro section: title" },
{ "key": "intro-text", "type": "textarea", "label": "Intro section: short introduction paragraph" },

{ "key": "show-featured-products", "type": "boolean", "label": "Show the featured products row", "default": true },
{ "key": "featured-title", "type": "text", "label": "Featured products: section heading" },

{ "key": "show-categories", "type": "boolean", "label": "Show the shop categories row", "default": true },
{ "key": "categories-title", "type": "text", "label": "Category row: section heading" },

{ "key": "show-events", "type": "boolean", "label": "Show the events agenda", "default": false },
{ "key": "events-title", "type": "text", "label": "Agenda: section heading" },

{ "key": "show-blog", "type": "boolean", "label": "Show the latest news from the blog", "default": false },
{ "key": "blog-title", "type": "text", "label": "Latest news: section heading" },

{ "key": "show-newsletter", "type": "boolean", "label": "Show the newsletter signup", "default": true },

{ "key": "show-closing-cta", "type": "boolean", "label": "Show the closing call-to-action", "default": true },
{ "key": "closing-cta-title", "type": "text", "label": "Closing call-to-action: title" },
{ "key": "closing-cta-text", "type": "textarea", "label": "Closing call-to-action: supporting text" },
{ "key": "closing-cta-label", "type": "text", "label": "Closing call-to-action: button text" },
{ "key": "closing-cta-url", "type": "text", "label": "Closing call-to-action: button link" }
]
}
]
}

The two -cta-url fields are skipped by the translation editor automatically (a URL is not language), while the title, text and label fields are offered for translation, so a multilingual site still renders a complete homepage. See Which Fields Get Translated above.

Toggling sections on and off​

Each show-* field above is a boolean, so the merchant (and the AI Website builder) gets a plain on/off toggle per section in the Admin. Read it in Twig with a simple truth check and combine it with a content check, so a section appears only when it is both switched on and has something to show:

{% set products = sitepack_products(8, 'popular') %}
{% if page['show-featured-products'] and products | length >= 1 %}
<section class="home-products">...</section>
{% endif %}
  • Check the value directly, as {% if page['show-featured-products'] %}. Do not wrap it in the default filter (page['show-x'] | default(true)): Twig's default treats false as empty, so it would flip every "off" back to "on". Give the field a "default" in theme.json instead, which is where the starting value belongs.
  • Keep the content check. The toggle is the merchant's intent; the length check guards against an empty result (a shop with no products, a site with no events). Together they mean turning a section on never produces an empty block.
  • Set sensible defaults per site type. The example ships the shop rows on and the agenda/blog off, which suits a webshop. The AI builder flips these to match what it is generating: events on for an events site, blog on for a content website.

One template, every object type​

Because the homepage reads its copy from these fields and pulls live content with the sitepack_* functions, the same template adapts to whatever the site sells:

Each section renders only when it has content, so one homepage.twig fits all of them. The complete template markup is in Page Examples.


Exploring Further:​

Now that you have configured your manifest and inputs, understand how static assets are served via the CDN in the Theme Assets Guide!