Developers/Theme docs

STOREFRONT DEVELOPMENT

Build storefront
themes with code.

Create storefronts with HTML, CSS and Nunjucks. Connect your design to real shop data, preview it in the code editor, and publish when it is ready.

HTML + CSS + NunjucksHome + product pagesNo build step for theme authors

01 / YOUR FIRST THEME

From starter to storefront

You need a shop account with permission to manage storefront settings, plus a basic understanding of HTML and CSS. Nunjucks adds variables, loops and reusable templates. You do not need PHP, a local server or an npm build to edit an imported theme.

  1. Download the starter. The JSON includes a home page, product page, reusable sections, product cards, styles and editable theme settings.
  2. Import it into your shop. Open Storefront → Builder → Import theme, enter a name, select the JSON and choose Import storefront. Importing creates an inactive draft.
  3. Open Code. Select your imported storefront and choose Code. Open sections/hero.njk or assets/theme.css to make your first change.
  4. Save and check the preview. Use Save draft. Check both Home and Product info. The product preview uses sample data.
  5. Publish. Choose Validate & publish, then test the real shop and an actual product page.
Drafts and live pages are separate. Saving changes the draft. Publishing creates a revision and makes this storefront live for the shop.

02 / PROJECT FORMAT

Files that travel together

The import file is one JSON object with three keys: manifest, files and settings. The files object maps bundle-relative paths to text contents. Upload the JSON itself, not a ZIP archive or a standalone template.

Minimal import format
{
    "manifest": {
        "engine": "nunjucks",
        "engine_version": "1.0",
        "context_version": "1",
        "pages": {
            "home": "templates/home.njk"
        }
    },
    "files": {
        "templates/home.njk": "<main class=\"sx-theme\"><h1>{{ shop.name }}</h1></main>",
        "assets/theme.css": ".sx-theme { padding: 32px; }"
    },
    "settings": {}
}

This minimal bundle renders a home page. Use the full starter for a product page and purchase controls.

DirectoryPurpose
layouts/Shared wrappers and Nunjucks blocks.
templates/Page entry points named by the manifest.
sections/Large page areas such as a hero, product grid or footer.
blocks/Reusable section content, with its own properties.
snippets/Smaller pieces such as a single product card.
assets/Text-based CSS or JSON files.
locales/Translation JSON and, for visual designs, Builder data.

Only .njk, .css and .json files are accepted. Use forward slashes and canonical relative paths. The logical theme.json shown below becomes the import's manifest; it is not an extra entry in files.

Adding a file or changing the manifest? The Code editor saves existing files. Export the saved theme, edit its JSON bundle locally, and import it as a new storefront when you need new file paths, a different page map or a new settings schema.

03 / CONFIGURATION

Describe pages and editable settings

Set engine to nunjucks, engine_version to 1.0, and context_version to 1. The pages.home entry is required and must point to an existing template. Add pages.product to provide a product-page template.

Manifest keyMeaning
id, nameYour theme's identifier and human-readable name.
pagesMaps home and optionally product to template paths.
page_settingsPage-scoped values, including the section order used by the starter.
sections, blocks, snippetsNamed definitions with a path and optional settings.
settings_schemaThe merchant-editable theme settings and their defaults.

Supported setting types

TypeValue and behavior
textTrimmed text, up to 500 characters. Read it with themeProperties.your_id.
colorA six-digit hex color such as #6E56CF. Invalid input falls back to the default.
checkboxA boolean. Use a Nunjucks if condition to show or hide content.
selectAn allowed value from options, where each option has value and label.

Every field needs an id, type, label and sensible default. The import's top-level settings object supplies the initial values. Unknown keys are ignored and supported values are normalized. In the Code editor, use the theme settings form to change these values.

The starter uses the settings accent, surface and text through the platform's CSS variables --sx-accent, --sx-surface and --sx-text.

04 / COMPOSITION

Compose a page from smaller files

Write HTML fragments rather than a complete document. The platform provides the outer HTML document, metadata, stylesheet loading and storefront behavior. The starter wraps its output in .sx-theme and scopes its own styles under .starter-theme.

templates/home.njk
{% extends "layouts/base.njk" %}
{% block content %}
  {% for section_name in settings.section_order %}
    {% render_section section_name %}
  {% endfor %}
{% endblock %}

render_section, render_block, render_snippet and render_template render a definition in the matching manifest collection. For example, Hero resolves to the path in manifest.sections.Hero.path. Pass named arguments when a snippet needs an item from a loop.

Keep the settings scopes distinct

VariableScope
themePropertiesNormalized merchant settings from settings_schema, available throughout the render.
settingsAt the page entry point: its page_settings. Inside a rendered section or template: that definition's settings.
propertiesA rendered block's settings. Snippets inherit the surrounding context and receive named arguments.
Section, block and snippet scopes
{# Inside sections/hero.njk #}
<h1>{{ themeProperties.hero_title }}</h1>
<p>{{ settings.eyebrow }}</p>

{# Inside blocks/product-list.njk #}
{% for product in products %}
  {% render_snippet "Product card", product=product, show_stock=properties.show_stock %}
{% else %}
  <p>No products are available yet.</p>
{% endfor %}

Standard Nunjucks if, for, set, block and literal extends / include paths work. Standard imports must use a quoted path, such as {% include "snippets/footer.njk" %}; dynamic include paths are rejected. Templates can only read files in their own bundle.

05 / DATA REFERENCE

Design around the shop's real data

The theme receives public JSON data. It has no database access or server secrets. Use the supplied URLs and display prices so the theme can be imported into another shop.

RootAvailable data
shopname, title, slug, description, subtitle, currency, logo_url, favicon_url, brand_color, url, contact_url, contact_links, support_email, show_rating, terms. Contact fields respect the shop's visibility settings.
productsThe home page's product collection. Each item includes name, URL, image, description, display price and stock information. Use a for loop and include an empty state.
categoriesHome-page categories with name, slug, url, description, image_url, icon and products_count.
routeshome and terms. Product pages also supply product and checkout. Use the tracking button hook below to open order tracking.
global.localeslocale, language and defaultLocale.
global.common.shopInfoCompatibility aliases for shop, items, categories and statistics. Home statistics include products, sold, reviews and rating.

Product fields

Both product cards and the product page provide name, description, url, image_url, price_cents, price_display, display_price, display_price_cents, currency, stock, stock_label, in_stock, type_key, type_label, badges and status_label.

Prefer display_price on cards because products can have a starting price. price_cents is an integer in the currency's minor units. An unknown or unlimited stock count may be null; display stock_label instead of inventing a number.

On the product page, the current item is product. The products and categories collections are not a related-product feed there. The page additionally provides:

FieldUse
product.description_html, product.terms_htmlSanitized rich text. Output these directly inside a block element. For plain-text card summaries, use description.
product.imagesImage entries with url and alt.
product.variantsEach variant has index, key, name, description, price_cents, price_display, stock, in_stock, min_quantity, max_quantity, available and unavailable_reason.
product.checkout_urlThe current shop's checkout entry point for this product.
product.description_tabsAdditional sections with title, content and sanitized content_html.
product.reviewsReview entries with rating, comment and date. Also available: review_count and review_average.
addons, upsellsProduct collections supplied for the current product page.
Escape ordinary values. Autoescaping is on. Do not apply safe to names, descriptions from arbitrary input or JSON attributes. The documented rich-text fields go through the platform's sanitizer and do not need a safe filter.

06 / PLATFORM BEHAVIOR

Connect product controls without custom scripts

The live product-page wrapper binds the following hooks. The starter's sections/product-detail.njk contains a complete example. These controls are interactive on the real product page; script execution and form submission are restricted in the editor preview.

HookBehavior
data-sx-product-formMarks the product purchase form. The platform sends the visitor to this product's own checkout.
data-sx-variantsPut this on the form and bind its value to product.variants | dump. Keep normal autoescaping enabled.
data-sx-variantA select whose option values are variant indexes, not variant keys or product IDs.
data-sx-quantityA number input. The platform applies the selected variant's bounds, with a maximum of 25.
data-sx-total, data-sx-availabilityOptional targets updated with the selected total and availability.
data-sx-checkout-buttonThe submit button. The platform disables it when the selection is unavailable.
input[name="addons[]"]Checked add-on checkboxes inside the form, with the add-on's integer ID as their value.
data-sx-product-image, data-sx-gallery-imageA main image and gallery buttons. A gallery button's attribute contains its target image URL.
data-sx-track-orderA type="button" button opens the platform's order-tracking UI.
data-sx-theme-toggleA button uses the storefront's existing color-mode switch behavior.

A regular link to product.checkout_url is a simpler checkout entry point. It does not carry a selection from your custom form. Use the bound purchase form when you offer variant, quantity or add-on controls.

Theme forms do not keep action or method attributes. The platform controls the destination and recalculates prices, stock and payment rules on the server. Theme data is for display, not an order authority.

07 / DESIGN

Write scoped CSS and use public media

assets/theme.css is loaded on themed pages. If present, assets/product.css is also loaded on product pages. Put your styling there; extra CSS files are not automatically loaded just because they exist in the bundle.

Scope selectors to your own wrapper so they do not affect platform UI. Use responsive layouts, visible focus states, image alternative text and an empty-product state. Keep product names and contact links data-driven.

Images belong in HTML. Theme CSS rejects url(...), @import and executable CSS. Use an img with an HTTPS or shop-relative URL, then style it with object-fit. Binary images, JavaScript and font files are not accepted bundle entries.

Use shop.logo_url, product.image_url or an image uploaded to the shop gallery. External images must remain publicly reachable. Exports retain image URLs; they do not embed the image files. HTML inline styles and event handlers are removed, so put visual rules in your CSS file.

Working alongside the visual Builder

Code-based themes do not automatically become fully editable visual elements. Custom source is preserved as custom-code nodes. Editing generated page files can replace visual editing for those areas. Export a saved copy before mixing source edits and visual editing, and avoid manually editing locales/builder.json unless you are intentionally working with a visual Builder export.

08 / TEMPLATE UTILITIES

Filters and helpers

ExpressionResult
1299 | money('USD')USD 12.99. The input is cents. This filter uses a fixed two-decimal format.
value | int, value | floatNumeric conversion; an unparseable value becomes zero.
product.variants | dumpA JSON string. Autoescape it when inserting it into an HTML attribute.
'catalog.title' | tLooks up a dotted translation key in the current locale's JSON file.
'theme.css' | asset_urlA URL for an existing bundle asset. Returns an empty string if the file is absent. This does not load the asset or bypass HTML/CSS restrictions.
uid()A render-local ID such as theme-1. Useful for accessible labels; not a persistent database ID.
value | default('Fallback'), items | lengthExamples of standard Nunjucks filters. Test optional values and empty collections explicitly.
Translations and money
{# locales/en.json contains {"catalog": {"count": "{count} products"}} #}
<p>{{ 'catalog.count' | t({count: products | length}) }}</p>

{# Money takes cents. Prefer product.display_price for catalog display. #}
<p>{{ 1299 | money('USD') }}</p>

Translation filenames match global.locales.locale, for example locales/en.json. The locale is lowercase and may use underscores. A missing key returns the key itself; an English translation file is not an automatic fallback for every other locale.

09 / RELEASE WORKFLOW

Preview, publish and share

Save draft validates the edited file and updates the saved draft. The safe preview uses that draft. It disables scripts and form submission, so a working checkout is verified in the live storefront.

The product preview uses Preview product with sample variants and prices. It does not modify or represent an actual product. Check a real product after publishing, including variant selection, quantity, stock handling and the transition to checkout.

Validate & publish validates and renders the theme, creates an immutable revision and activates it. Published revisions → Make live switches to an older published revision without replacing the current draft.

Export an installable design

Save before using the Builder's export button. The export contains the saved manifest, text files and settings. Another merchant can import it into their own shop and publish it after checking links, media and real products. It does not transfer a shop's products, orders or API credentials.

  • Test a populated and empty catalog, long names, missing images and unavailable variants.
  • Check Home and Product info on narrow and wide screens.
  • Keep image URLs reachable and replace shop-specific text before sharing.
  • Use only templates and media you have permission to distribute.

10 / COMPATIBILITY

Rules your theme must follow

LimitCurrent value
Files per bundle120
One source file256 KiB
Total source contents2 MiB
Rendered output2 MiB
Renderer timeout900 ms
Import upload4 MiB for the JSON upload, in addition to the source limits above.

Scripts, inline event handlers, JavaScript URLs, function/prototype access and dynamic standard template imports are rejected. The renderer cannot fetch external data or read server files. Custom scripts, iframes and external stylesheet tags do not become executable theme features.

HTML is sanitized after rendering. Use allowed semantic HTML and platform data-sx-* hooks. Inline SVG is not a supported theme markup format; use a public image URL or a supported icon already provided by the storefront.

11 / DEBUGGING

Common mistakes and fixes

SymptomWhat to check
Import failsUse valid JSON with object-shaped manifest/files/settings values, a supported engine version, canonical paths and an existing home template.
File not foundMatch paths and capitalization exactly. Include the file in the files map; a manifest declaration does not create it.
A hero setting is blankMerchant settings live in themeProperties. Section-local settings live in settings. Check the field ID and schema default.
Products disappear on the product pageUse product for the current item. The page is not supplied with the home-page catalog collections.
CSS is rejectedRemove @import, url(...) and executable CSS. Move images into HTML elements.
A script or style vanishesThe theme is sanitized. Put styles in the CSS assets and use the documented platform behavior hooks.
Variant selection does not workTest on the real product page. Use variant indexes, escaped variants JSON and the complete purchase-form hooks.
Saving does not update the shopPublish the draft and confirm the intended storefront is live. Saving and exporting do not activate it.
Adding a new file is not possible in CodeExport, add the path and contents to the JSON files object, update the manifest if needed, then import the new bundle.
Custom code is not visually editableUse Code for that area. Arbitrary source does not automatically map to individual Builder elements.

12 / COMPLETE EXAMPLE

The starter, file by file

These are the exact sources in the downloadable starter. Expand a file to inspect or copy it. The manifest is shown separately; the downloaded JSON already packages everything in the correct import format.

theme.jsonJSON
theme.json
{
    "id": "developer-starter",
    "name": "Developer Starter",
    "engine": "nunjucks",
    "engine_version": "1.0",
    "context_version": "1",
    "pages": {
        "home": "templates/home.njk",
        "product": "templates/product.njk"
    },
    "page_settings": {
        "home": {
            "section_order": [
                "Header",
                "Hero",
                "Products",
                "Footer"
            ]
        },
        "product": {
            "section_order": [
                "Header",
                "Product detail",
                "Footer"
            ]
        }
    },
    "sections": {
        "Header": {
            "path": "sections/header.njk"
        },
        "Hero": {
            "path": "sections/hero.njk",
            "settings": {
                "eyebrow": "THE COLLECTION"
            }
        },
        "Products": {
            "path": "sections/products.njk",
            "settings": {
                "block_order": [
                    "Product list"
                ]
            }
        },
        "Product detail": {
            "path": "sections/product-detail.njk"
        },
        "Footer": {
            "path": "sections/footer.njk"
        }
    },
    "blocks": {
        "Product list": {
            "path": "blocks/product-list.njk",
            "settings": {
                "show_stock": true
            }
        }
    },
    "snippets": {
        "Product card": {
            "path": "snippets/product-card.njk"
        }
    },
    "settings_schema": [
        {
            "id": "accent",
            "type": "color",
            "label": "Accent color",
            "default": "#6E56CF"
        },
        {
            "id": "surface",
            "type": "color",
            "label": "Background",
            "default": "#11121A"
        },
        {
            "id": "text",
            "type": "color",
            "label": "Text color",
            "default": "#F4F4FA"
        },
        {
            "id": "hero_title",
            "type": "text",
            "label": "Hero title",
            "default": "Something worth discovering."
        },
        {
            "id": "hero_copy",
            "type": "text",
            "label": "Hero description",
            "default": "Explore our collection and find your next essential."
        },
        {
            "id": "show_description",
            "type": "checkbox",
            "label": "Show product summaries",
            "default": true
        },
        {
            "id": "card_style",
            "type": "select",
            "label": "Card style",
            "default": "bordered",
            "options": [
                {
                    "value": "bordered",
                    "label": "Bordered"
                },
                {
                    "value": "minimal",
                    "label": "Minimal"
                }
            ]
        }
    ]
}
assets/product.cssCSS
assets/product.css
.starter-product { padding: 40px 0 64px; }
.starter-product-grid { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); gap: 40px; margin: 28px 0; }
.starter-product-image { width: 100%; max-height: 440px; object-fit: contain; border-radius: 12px; }
.starter-purchase { align-self: start; border: 1px solid #ffffff25; border-radius: 12px; padding: 24px; }
.starter-purchase form { display: grid; gap: 18px; margin-bottom: 20px; }
.starter-purchase label { display: grid; gap: 6px; }
.starter-purchase select, .starter-purchase input { max-width: 100%; width: 100%; padding: 10px; color: var(--sx-text); background: var(--sx-surface); border: 1px solid #ffffff45; border-radius: 6px; }
.starter-purchase button:disabled { opacity: .5; cursor: not-allowed; }
.starter-description img { max-width: 100%; height: auto; }
@media (max-width: 700px) { .starter-product-grid { grid-template-columns: 1fr; gap: 24px; } }
assets/theme.cssCSS
assets/theme.css
.starter-theme { min-height: 100vh; background: var(--sx-surface); color: var(--sx-text); font-family: system-ui, sans-serif; line-height: 1.65; }
.starter-theme * { box-sizing: border-box; }
.starter-theme a { color: inherit; text-decoration: none; }
.starter-theme button, .starter-theme input, .starter-theme select { font: inherit; }
.starter-shell { max-width: 1120px; margin: auto; padding: 0 24px; }
.starter-header, .starter-brand, .starter-header nav, .starter-footer { display: flex; align-items: center; flex-wrap: wrap; gap: 24px; }
.starter-header { justify-content: space-between; padding: 28px 0; border-bottom: 1px solid #ffffff20; }
.starter-brand { font-size: 20px; font-weight: 750; gap: 10px; }
.starter-header nav button { color: inherit; background: none; border: 0; cursor: pointer; padding: 0; }
.starter-hero { padding: 88px 0; max-width: 760px; }
.starter-eyebrow { color: var(--sx-accent); font-size: 12px; font-weight: 700; letter-spacing: .15em; }
.starter-hero h1 { font-size: clamp(36px, 6vw, 68px); line-height: 1.08; letter-spacing: -.045em; margin: 16px 0 24px; }
.starter-hero > p:not(.starter-eyebrow) { opacity: .8; max-width: 560px; font-size: 18px; }
.starter-button { display: inline-block; background: var(--sx-accent); color: #fff !important; padding: 12px 22px; border: 0; border-radius: 8px; font-weight: 700; cursor: pointer; }
.starter-products { padding: 16px 0 64px; scroll-margin-top: 24px; }
.starter-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px; }
.starter-card { padding: 20px; min-width: 0; border-radius: 12px; }
.starter-card--bordered { border: 1px solid #ffffff25; }
.starter-card img { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: 8px; }
.starter-card h3 { font-size: 18px; margin-bottom: 6px; overflow-wrap: anywhere; }
.starter-card p, .starter-card small { opacity: .75; }
.starter-footer { border-top: 1px solid #ffffff20; padding: 28px 0; font-size: 14px; }
.starter-footer span { margin-right: auto; }
.starter-theme a:focus-visible, .starter-theme button:focus-visible { outline: 3px solid var(--sx-accent); outline-offset: 4px; }
@media (max-width: 760px) { .starter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 480px) { .starter-grid { grid-template-columns: 1fr; } .starter-shell { padding: 0 18px; } .starter-hero { padding: 52px 0; } .starter-header nav { gap: 16px; font-size: 14px; } }
blocks/product-list.njkNJK
blocks/product-list.njk
<div class="starter-grid">
  {% for product in products %}
    {% render_snippet "Product card", product=product, show_stock=properties.show_stock %}
  {% else %}
    <p>No products are available yet.</p>
  {% endfor %}
</div>
layouts/base.njkNJK
layouts/base.njk
<div class="sx-theme starter-theme">
  <div class="starter-shell">
    {% block content %}{% endblock %}
  </div>
</div>
locales/en.jsonJSON
locales/en.json
{
  "catalog": {
    "title": "Explore the collection",
    "count": "{count} products"
  }
}
sections/footer.njkNJK
sections/footer.njk
<footer class="starter-footer">
  <span>{{ shop.name }}</span>
  <a href="{{ routes.terms }}">Terms</a>
  {% if shop.contact_url %}<a href="{{ shop.contact_url }}">Contact</a>{% endif %}
</footer>
sections/header.njkNJK
sections/header.njk
<header class="starter-header">
  <a class="starter-brand" href="{{ routes.home }}">
    {% if shop.logo_url %}<img src="{{ shop.logo_url }}" alt="{{ shop.name }}" width="36" height="36">{% endif %}
    <span>{{ shop.name }}</span>
  </a>
  <nav aria-label="Store navigation">
    <a href="{{ routes.home }}#products">Products</a>
    {% if shop.contact_url %}<a href="{{ shop.contact_url }}">Contact</a>{% endif %}
    <button type="button" data-sx-track-order>Track order</button>
  </nav>
</header>
sections/hero.njkNJK
sections/hero.njk
<section class="starter-hero">
  <p class="starter-eyebrow">{{ settings.eyebrow }}</p>
  <h1>{{ themeProperties.hero_title }}</h1>
  <p>{{ themeProperties.hero_copy }}</p>
  <a class="starter-button" href="#products">Explore products</a>
</section>
sections/product-detail.njkNJK
sections/product-detail.njk
<main class="starter-product">
  <a href="{{ routes.home }}">Back to {{ shop.name }}</a>
  <div class="starter-product-grid">
    <div>
      {% if product.image_url %}<img class="starter-product-image" src="{{ product.image_url }}" alt="{{ product.name }}" data-sx-product-image>{% endif %}
      <h1>{{ product.name }}</h1>
      <div class="starter-description">{{ product.description_html | default(product.description) }}</div>
      {% for tab in product.description_tabs %}
        <section><h2>{{ tab.title }}</h2><div>{{ tab.content_html | default(tab.content) }}</div></section>
      {% endfor %}
    </div>
    <aside class="starter-purchase">
      <h2>Make it yours</h2>
      <form data-sx-product-form data-sx-variants="{{ product.variants | dump }}">
        <label>Option
          <select name="variant" data-sx-variant>
            {% for variant in product.variants %}
              <option value="{{ variant.index }}">{{ variant.name }} - {{ variant.price_display }}</option>
            {% endfor %}
          </select>
        </label>
        <label>Quantity <input type="number" name="quantity" value="1" min="1" max="25" data-sx-quantity></label>
        <strong data-sx-total>{{ product.display_price }}</strong>
        <p data-sx-availability>{{ product.stock_label }}</p>
        <button class="starter-button" type="submit" data-sx-checkout-button>Continue to checkout</button>
      </form>
      <a href="{{ product.checkout_url }}">Open checkout directly</a>
    </aside>
  </div>
  {% if product.terms_html or product.terms %}<section><h2>Terms</h2><div>{{ product.terms_html | default(product.terms) }}</div></section>{% endif %}
  {% if product.review_count %}
    <section><h2>Customer reviews</h2>
      {% for review in product.reviews %}<article><strong>{{ review.rating }} / 5</strong><p>{{ review.comment }}</p></article>{% endfor %}
    </section>
  {% endif %}
</main>
sections/products.njkNJK
sections/products.njk
<section id="products" class="starter-products">
  <h2>Explore the collection</h2>
  {% for block_name in settings.block_order %}
    {% render_block block_name %}
  {% endfor %}
</section>
snippets/product-card.njkNJK
snippets/product-card.njk
<article class="starter-card starter-card--{{ themeProperties.card_style }}">
  <a href="{{ product.url }}">
    {% if product.image_url %}<img src="{{ product.image_url }}" alt="{{ product.name }}" loading="lazy">{% endif %}
    <h3>{{ product.name }}</h3>
    <strong>{{ product.display_price }}</strong>
  </a>
  {% if themeProperties.show_description %}<p>{{ product.description }}</p>{% endif %}
  {% if show_stock %}<small>{{ product.stock_label }}</small>{% endif %}
</article>
templates/home.njkNJK
templates/home.njk
{% extends "layouts/base.njk" %}
{% block content %}
  {% for section_name in settings.section_order %}
    {% render_section section_name %}
  {% endfor %}
{% endblock %}
templates/product.njkNJK
templates/product.njk
{% extends "layouts/base.njk" %}
{% block content %}
  {% for section_name in settings.section_order %}
    {% render_section section_name %}
  {% endfor %}
{% endblock %}

Start with a working theme

Import the starter, change one file, and check the result.

Download JSON ↓