Liquid sections and blocks

Liquid is the safe presentation language used inside sections and blocks. It can display data supplied by Upstorr, loop over lists and make simple choices. It cannot read the database, secrets, server files or private APIs.

A complete section

<section id="{{ section.dom_id }}" class="feature-{{ section.id }}">
  <h2>{{ section.settings.heading }}</h2>
</section>

<style>
  .feature-{{ section.id }} { padding: 48px 24px; }
</style>

{% schema %}
{
  "name": "Feature",
  "settings": [
    { "id": "heading", "type": "text", "label": "Heading", "default": "Welcome" }
  ],
  "block_types": [],
  "allowed_blocks": [],
  "presets": [{ "name": "Feature" }]
}
{% endschema %}

The markup is the shop appearance. The style belongs to this section. The schema creates Level 1 right-panel controls. The template JSON supplies each installed instance's starting values.

Use section.dom_id on the selectable root. A reusable block uses block.dom_id.

Shared controls

Put a repeated group in theme/controls/shared/box.json:

{
  "settings": [
    { "id": "width", "type": "select", "label": "Width", "default": "page", "options": [
      { "value": "page", "label": "Page" },
      { "value": "full", "label": "Full" }
    ] },
    { "id": "gap", "type": "range", "label": "Gap", "default": 16, "min": 0, "max": 64, "step": 1, "unit": "px" }
  ]
}

Reference it from a schema with { "$ref": "shared/box" }. An override may change a label, default, range, options or visibility. It may not change a field's ID or type. Missing packs, unknown overrides and reference cycles fail theme check.

Reusable blocks

A visible piece used by several sections belongs in theme/blocks/shared/<type>.liquid or theme/blocks/retail/<type>.liquid. Its file owns its markup, styling and schema. The compiler discovers the filename automatically; there is no React registry.

A section-local block is declared inside its parent section's schema. It remains part of the main section editor. Nested blocks never open separate code editors.

Standard Liquid values

Depending on the route, Upstorr supplies:

  • shop.name and shop.currency
  • global settings
  • routes
  • localization.current and localization.available
  • request.template, request.kind and request.preview
  • section.id, section.dom_id, section.settings and section.blocks
  • product, products, collection, collections, blog, article, page and cart
  • cart_view with prepared lines, totals and checkout availability
  • local_blocks, local_block_defaults, preset_blocks and trusted preset slots when declared
  • shared translated labels

Missing store data stays empty. A theme must not invent inventory, customer access, discounts, taxes or totals.

Useful safety filters include safe_rich_text, plain_text, link_url, media_url, media_alt, video_url, video_sources, video_poster, money, money_with_currency and json. All final HTML and URLs are still validated.

Behaviours

Visual HTML remains in Liquid. A schema registration can opt into a documented platform behaviour. Checkout uses:

{
  "registration": { "behavior": "checkout" },
  "settings": []
}

The SDK then supplies checkout state and handles the approved checkout attributes. The section still owns all visible labels, groups, inputs, buttons and CSS.

Menus use a registration such as:

{
  "registration": {
    "menus": { "navigation": { "setting": "menu" } }
  },
  "settings": [
    { "id": "menu", "type": "link_list", "label": "Menu", "default": "main-menu" }
  ]
}

The section can then loop through navigation.items without fetching anything.

Safety rules

Section code cannot contain script tags, inline event handlers, private API calls or server code. Put optional theme-only browser interactions in theme/assets/theme.ts. Use documented SDK helpers for navigation and commerce actions.

Run upstorr theme check <theme> after adding or renaming a section, block, template, control pack or JavaScript import.