Skip to content

Build a theme in 15 minutes

From an empty folder to a live preview on your sandbox store with a correct two-file template.

Updated 2 Sept 20262 min read
On this page

This page is for anyone building a storefront theme for Dukkan stores. Themes are Liquid templates rendered on Dukkan's servers inside a hardened sandbox — zero theme JavaScript, an Arabic-first data model, RTL by default. Your tool is @dukkan.one/theme-cli, and by the end of this page you have a theme running in a live preview on your sandbox store.

1. Set up#

You need Node.js 18.17 or newer, a developer account, and an active sandbox store (create one here).

Shell
npx @dukkan.one/theme-cli theme init my-theme
cd my-theme
npx @dukkan.one/theme-cli login

login opens a device flow: it prints a /cli/authorize link and a code shaped DKN-XXXXXX; you confirm it in the portal and pick the sandbox store you work against. The resulting token reaches only that sandbox and can never touch a real store. Flow details and errors are in the CLI reference.

Warning

In the published 0.1.0 release, theme init writes a layout/theme.liquid in a legacy form the render engine does not support, so the page renders empty. Replace both files as shown in step 4 before your first theme dev.

2. The dev loop#

Shell
npx @dukkan.one/theme-cli theme dev

It pushes the theme as your sandbox store's draft, then watches your files, pushes on every change, and prints a live preview URL on the sandbox storefront shaped https://dukkan.one/stores/YOUR_SANDBOX_SLUG?__preview=TOKEN. A preview link lives 24 hours and is revoked automatically by any push that changes the draft, at which point the CLI prints a fresh one.

3. Theme anatomy#

Path Purpose
layout/theme.liquid The shell: <html>, <head> and the {% block content %} slot. Required.
templates/home.liquid Home page, product list and search results.
templates/category.liquid Category page.
templates/product.liquid Product page.
templates/cart.liquid Cart.
config/settings_schema.json Settings the merchant edits in the visual editor. Required.
locales/ar.json UI strings for the t filter; locales/en.json is optional.
snippets/*.liquid Partials, included by full path.
assets/*.css and images CSS, images and woff2 fonts only; never JavaScript.

Checkout and order pages are platform-owned and never themed. Prices arrive as { amount_minor, currency } and are displayed with the money filter. The full rule set is in Bundle anatomy.

4. The correct minimal pair#

The layout declares a content block; every template declares that it renders inside the layout and fills the block:

layout/theme.liquidLiquid
<!doctype html>
<html dir="{{ request.direction }}" lang="{{ request.locale }}">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{{ store.name }}</title>
  <style>{% include 'assets/theme.css' %}</style>
</head>
<body>
  <header><a href="{{ store.url }}">{{ store.name }}</a></header>
  {% block content %}{% endblock %}
  <footer>{{ 'footer.powered_by' | t }}</footer>
</body>
</html>
templates/home.liquidLiquid
{% layout 'layout/theme.liquid' %}
{% block content %}
<main>
  <h1>{{ store.name }}</h1>
  <ul>
    {% for product in products %}
    <li>
      <a href="{{ product.url }}">{{ product.name }}</a>
      <span>{{ product.price | money }}</span>
      {% unless product.available %}<em>{{ 'product.sold_out' | t }}</em>{% endunless %}
    </li>
    {% endfor %}
  </ul>
</main>
{% endblock %}

There is no content variable inside the layout; the contract is {% layout %} plus {% block %}, exactly as in every first-party Dukkan theme. Arabic strings never sit in templates — they live in locales/ar.json:

locales-ar.jsonJSON
{
  "nav": {
    "home": "الرئيسية",
    "cart": "السلة"
  },
  "product": {
    "add_to_cart": "أضف إلى السلة",
    "sold_out": "نفد المخزون"
  },
  "footer": {
    "powered_by": "مدعوم من دكان"
  }
}

5. Check#

Shell
npx @dukkan.one/theme-cli theme check
npx @dukkan.one/theme-cli theme check --remote

theme check runs locally the same code the server runs at push: the extension allowlist, size and count limits, path safety, a valid settings_schema.json, and no <script> anywhere. --remote adds a smoke render in the server sandbox. The exit code is 1 on any error, so the command is CI-ready. The rule list with verbatim error strings is in Theme validation.

Next steps#