Skip to content

Sections and blocks

Declaring sections, the body dispatcher, double rendering, bands, product sources, and the runtime modules.

Updated 2 Sept 20264 min read
On this page

This page is for anyone who wants the merchant to compose their own page in the visual editor. By the end you know how to declare a section, how its body reaches the page, and which rules break rendering if you forget them.

The model#

  1. The theme declares section types in config/settings_schema.json under sections (type, settings, blocks, allowed pages).
  2. The merchant composes instances of those types in the editor; they are stored in the settings document under pages.<page>.
  3. At render the platform resolves the document into page_sections: visible, ordered, validated instances, with resolved products for those declaring sources, and no expired countdowns.
  4. The template places {% include 'snippets/sections.liquid' %} where merchant sections should appear, and the theme writes snippets/section-body.liquid to render each type's body.

The body dispatcher#

The only file you write is snippets/section-body.liquid: a switch over section.type:

snippets/section-body.liquidLiquid
{% case section.type %}
{% when 'hero_banner' %}{% include 'snippets/section-hero-banner.liquid' %}
{% when 'featured_products' %}{% include 'snippets/section-featured-products.liquid' %}
{% when 'rich_text' %}{% include 'snippets/section-rich-text.liquid' %}
{% endcase %}

Inside each body, section is available with id, type, settings, blocks, band, products and products_total. The wrapper <section class="sec sec--<type>" data-dukkan-section data-section-id="…"> is emitted by the platform from the injected snippets/sections.liquid; you never write it.

Rules that break rendering#

  • Bodies render twice. The dispatcher renders each body into a capture first to detect emptiness, then for real. A body must be side-effect-free: no {% increment %}, no {% decrement %}, no cycle state.
  • A blank body emits no wrapper. Whatever renders to whitespace disappears with its wrapper, so an unconfigured section leaves no empty stripe. Lean into it: render nothing when there is nothing to show.
  • Alternation. Every odd non-blank, non-band section carries the class sec--alt; design an alternating treatment on it. Sections declaring "band": true (full-bleed strips) are excluded from the count so inserting a banner never flips the alternation around it, and they appear in the context as section.band.
  • Never emit wrapper markers. Any data-dukkan-section or data-section-id= in your output is rejected at push.

Allowed pages#

Each type declares the pages it may be placed on through enabled_on, a subset of home, product, category and cart. Absent means home only. Narrowing it later drops stored instances on the removed pages silently and never breaks the merchant's save.

Product sources#

A product or category setting carrying "source": true makes the platform fetch its products before rendering and place them in section.products (and block.products for blocks). The theme fetches nothing; it renders what it receives in the merchant's order, unavailable products included, so it can show an honest sold-out state instead of hiding them.

  • The per-section limit comes from settings.limit, or settings.count when absent.
  • section.products_total is the sum of the available products' prices in minor units, computed by the platform for "buy together" buttons.
  • Leave a category source blank to fall back to the default behavior (featured products first).
snippets/section-featured-products.liquidLiquid
{% if section.products.size > 0 %}
<h2>{{ section.settings.title }}</h2>
<ul>
  {% for product in section.products %}
  <li><a href="{{ product.url }}">{{ product.name }}</a> {{ product.price | money }}</li>
  {% endfor %}
</ul>
{% endif %}

Blocks#

A block is an instance nested inside a section (a slide in a banner, a question in a FAQ, an item in a bundle). Its types are declared in blocks inside the section definition, and they arrive ordered in section.blocks with id, type, settings and products.

The showcase and first-install document#

config/showcase.json is a complete settings document that serves as the gallery demo and as the composition source at first install. The following annotation keys are stripped from the output:

Key Where Effect at install
__demo_only: true section dropped entirely (fabricated urgency or social proof stays demo-only)
__install_empty: true section the type survives with empty settings as an honest placeholder
__demo_only_keys: ["k"] document or section those keys are stripped (demo contact details, fabricated dates)
__requires: { products?, categories?, images?, promotion?, whatsapp? } section or block dropped unless the store satisfies it; products stays at most 12

Thin-catalog rule: under 4 products only the first product-requiring section per page survives.

Runtime modules#

The theme is CSS only; interactivity comes from the runtime through declarative attributes:

Attribute Effect
data-dukkan-add-to-cart, data-dukkan-cart-count, data-dukkan-cart-anchor add to cart and the cart badge
data-dukkan-add-many with data-dukkan-fly-item="<variantId>" bundle add with a fly-to-cart effect per item
data-dukkan-reveal-item scroll reveal; <html> is stamped data-dukkan-reveal when motion is wanted, and the element receives data-dukkan-inview
data-dukkan-sticky-atc with data-dukkan-atc-watch a sticky buy button shown while the buy box is out of view
data-dukkan-to-top back-to-top button
data-dukkan-scheme-toggle dark mode toggle; design your palette under [data-scheme="dark"]
data-dukkan-stock-alert with data-product-id a "notify me" form
data-dukkan-quick-view on a product-page anchor quick view; the link still navigates without JavaScript

Hide content only behind data-dukkan-reveal on the root; without it everything must be visible.