Bundle anatomy
Required files, the layout/block contract, reserved paths, and the CSS and asset rules.
On this page
This page is for anyone organizing a theme's files. By the end you know what is required, how a template binds to the layout, and what the server rejects at push and why.
Files#
| Path | Required | Purpose |
|---|---|---|
layout/theme.liquid |
yes | the shell every page renders into |
config/settings_schema.json |
yes | the settings and sections schema; see Settings schema |
templates/home.liquid |
in practice, yes | the home entry point; the smoke render at push starts there |
templates/product.liquid, templates/category.liquid, templates/cart.liquid |
per page you support | see Templates |
snippets/section-body.liquid |
if the schema declares sections | the section body dispatcher; see Sections |
snippets/*.liquid |
no | partials included by full path |
locales/ar.json and locales/en.json |
recommended | tables for the t filter; see Arabic first |
assets/* |
no | CSS, images, fonts |
config/showcase.json |
no | the showcase and first-install document for sectioned themes |
Allowed extensions only: .liquid, .json, .css, .svg, .png, .jpg, .jpeg, .webp, .woff2. Any .js, .ts or .html is rejected; a theme ships no JavaScript at all, and interactivity comes from the platform runtime through data-dukkan-* attributes. Numeric limits are in Limits.
The layout/block contract#
The layout declares a content block and every template fills it:
<!doctype html>
<html dir="{{ request.direction }}" lang="{{ request.locale }}">
<head>
<meta charset="utf-8">
<title>{{ store.name }}</title>
<style>{% include 'assets/theme.css' %}</style>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>{% layout 'layout/theme.liquid' %}
{% block content %}
<h1>{{ product.name }}</h1>
<p>{{ product.price | money }}</p>
{% endblock %}- There is no content variable inside the layout; anything outside
{% block %}in a template is not rendered. - A layout may declare several blocks (
head,content,scripts_data) and a template fills whichever it wants. {% include %}takes the full path inside the bundle:{% include 'snippets/product-grid.liquid' %}. There is no directory lookup or implicit extension, and no path outside the bundle ever resolves.
Reserved paths and attributes#
snippets/sections.liquidis platform-reserved: the platform injects it at push, and its presence in your bundle is rejected. You writesnippets/section-body.liquidonly.- Never emit
data-dukkan-sectionordata-section-id=from your own Liquid; the wrapper is platform-owned, and it is rejected by the static scan and in the output of the smoke render. - Capability attributes such as
data-dukkan-add-to-cartanddata-dukkan-cart-countare allowed; they are your contract with the runtime.
Liquid rules#
A template is rejected at push if it contains any of: <script> (except <script type="application/ld+json"> for JSON-LD data), an inline event handler (onclick=), javascript:, data:text/html, <iframe>, or the | raw filter. Verbatim error strings are in Theme validation.
Every {{ }} output is escaped automatically; the only path to raw HTML is a platform filter that marks its output safe (money, money_major, json, richtext). See Filters.
CSS and assets#
- Put styles in
assets/theme.cssand inline them in<style>via{% include 'assets/theme.css' %}; CSS files are scanned like templates, and any<inside them is rejected (it forecloses a</style>breakout). - Use logical properties (
padding-inline,margin-inline-start) rather thanleft/right; see Arabic first. - Prefix class names with your theme (
.bz-card) so two installed themes never collide inside saved settings. - Merchant images and files arrive through
asset_urlandimg_url, never as absolute URLs. Theme images bundled underassets/are stored and served from storage.
No money math in Liquid#
Prices are { amount_minor, currency } objects. Display them with money, and use the platform-computed product.savings and section.products_total instead of subtracting or adding in the template. See Objects.