Storelib Developer HubSections

Sections

A section is one file, sections/<type>.storelib, that describes a section type completely: how it looks, how it is styled, what a creator can change about it, and what it does. It has four blocks, in any order, at most one of each.

BlockRequiredWhat it holds
<template>yesThe markup, in the template language below.
<style>noCSS for this section's instances only. <style scoped> means the same thing. See Styling.
<script>noBehaviour. Checked when saved, then run in a sandbox against a copy of the section. See JavaScript.
<schema>yesJSON: the section's identity, settings, blocks and presets. See Schema.

Rules the parser enforces#

  • A block's opening and closing tags count only when they start a line. </style> inside a string in the template is text, not the end of the style block.
  • A second <template>, or a second of any block, is refused, naming the line of the first.
  • A block that is never closed is refused at the line it opened.
  • A file without <template> or <schema> is refused.
  • There is no unscoped <style> in a section. Global CSS belongs in layout/theme.storelib.

The template language#

The template is HTML plus output, conditions, loops and comments. It is not Vue, React, JSX or Liquid, although it looks like Liquid: v-if, @click, :class and JSX braces are refused by the compiler. It is compiled to a tree and walked, with no eval anywhere, which is what keeps rendering deterministic and lets the storefront run under a strict Content Security Policy.

.storelib
<h2>{{ settings.heading }}</h2>

{% if settings.show_subheading and settings.subheading != "" %}
  <p>{{ settings.subheading }}</p>
{% elsif settings.show_subheading %}
  <p>No subheading yet</p>
{% endif %}

<ul>
{% for block in blocks %}
  <li data-block-id="{{ block.id }}" class="{% if forloop.first %}is-first{% endif %}">
    {{ forloop.index }}. {{ block.settings.title | upcase }}
  </li>
{% else %}
  <li>Add an item</li>
{% endfor %}
</ul>

{# A comment. It is read and thrown away, and never reaches the page. #}

Output#

{{ expression }} prints a value, always HTML-escaped. The only way to print markup is the raw filter, and only on a richtext setting, whose value was cleaned against an allow list before it was stored.

Expressions#

  • Literals: "text", 'text', 12, 1.5, true, false, null.
  • Paths: settings.heading, block.settings.url, theme.colors.primary, items[0], items["key"]. Indexes must be literal; there are no computed lookups.
  • Comparisons: ==, !=, <, <=, >, >=, contains.
  • Logic: and, or, not, and parentheses.
  • Filters: value | name: arg1, arg2, chained left to right.

A path reads plain data only. __proto__, constructor and prototype cannot be reached, a function is never called, and a missing value is null rather than an error. Arrays have .size, .first and .last.

Every name must be declared#

{{ settings.foo }} compiles only when the schema declares a setting with the id foo, and {{ block.settings.bar }} only when that block type declares bar. A typo is a compile error with a line number. A setting the schema declares and the template never reads is a warning: it would be a control in the panel that does nothing.

What a template can read#

NameWhat it is
settings.*This instance's settings, resolved: defaults applied, assets turned into URLs, responsive values as { desktop, tablet, mobile }.
blocksThis instance's blocks in order, each { id, type, settings }. Hidden blocks are left out.
section.id, section.typeThis instance. Use section.id to build element ids, so the section can appear twice on a page.
scheme.*The colour scheme this instance chose, resolved, with the section's own overrides applied. See Styling.
theme.colors.*, theme.typography.*, theme.radius.*, theme.spacing.*, theme.settings.*The theme's tokens.
page.templateWhich page this is on: index, product, page.about and so on.
page.in_editortrue inside the builder. Put prompts for the creator behind it so a visitor never sees them.
productsThe creator's published products, newest first, each { id, type, settings } with settings.title, price (already formatted), compare_price, image, url and tag. The same shape as a block, so one card works for both.
forloop.index, index0, first, last, lengthInside a for.

Filters#

FilterExampleWhat it does
default{{ settings.label | default: "Shop" }}The argument when the value is empty, null or false.
upcase{{ settings.tag | upcase }}Upper case.
downcase{{ settings.tag | downcase }}Lower case.
capitalize{{ settings.name | capitalize }}The first letter in upper case.
strip{{ settings.name | strip }}Removes spaces from both ends.
size{{ blocks | size }}How many items a list has, or characters a string has.
first{{ blocks | first }}The first item of a list, or the first character of a string.
last{{ blocks | last }}The last item of a list, or the last character of a string.
join{{ settings.tags | join: " · " }}A list joined into one string, with , unless given another separator.
truncate{{ settings.text | truncate: 80 }}Cut to that many characters, 50 unless given, ending with or the second argument.
append{{ settings.price | append: " each" }}Adds the argument to the end.
prepend{{ settings.code | prepend: "#" }}Adds the argument to the start.
replace{{ settings.text | replace: "old", "new" }}Replaces every occurrence of the first argument with the second.
plus{{ forloop.index | plus: 1 }}Adds a number.
minus{{ settings.count | minus: 1 }}Subtracts a number.
times{{ settings.columns | times: 2 }}Multiplies by a number.
divided_by{{ settings.width | divided_by: 2 }}Divides by a number. Dividing by zero gives 0.
round{{ settings.ratio | round: 2 }}Rounds to that many decimal places, 0 unless given.
jsondata-config="{{ settings | json }}"The value as JSON, escaped for an attribute.
escape{{ settings.text | escape }}HTML-escaped. Output already is, so this is rarely needed.
raw{{ settings.body | raw }}Printed without escaping. Only on a richtext setting, which was cleaned before it was stored.
urlhref="{{ settings.link | url }}"Keeps a link starting /, http://, https://, mailto:, tel: or #, keeps an empty value empty, and turns anything else into #. Use it on every link built from a setting.
embedsrc="{{ settings.video | embed }}"A player address from a pasted YouTube, Vimeo, Spotify or SoundCloud link, or an empty string. Needs the embed capability.
asset{{ block.settings.clip | asset }}An asset:// reference as the URL this account serves it from. A plain URL passes through.
alpha{{ settings.color | alpha: 0.4 }}A hex colour at that opacity, as rgba().

Two rules worth remembering: put | url on every href built from a setting, and use raw only on a richtext setting.

What the compiler refuses#

  • A <script> tag inside <template>. A section's script goes in its own <script> block, beside <template>, where it runs in a sandbox.
  • An on*= attribute. Add the listener in the section's <script> block instead, or use HTML that needs none: <details>, a radio input with :checked, or popovertarget.
  • A javascript: URL.
  • An <object> or <embed>.
  • An <iframe>, unless the schema declares the embed capability and its src comes from the embed filter.
  • An unclosed {{ or {%, an {% if %} without {% endif %}, an unknown tag, an unknown filter, or a malformed expression.
  • A name the schema does not declare, such as {{ settings.headng }}.

Vue syntax is refused by name, with what to write instead, because it is the first thing anyone arriving from a framework reaches for:

VueWrite instead
v-if{% if … %} … {% endif %}.
v-else-if{% elsif … %}.
v-else{% else %}.
v-for{% for item in list %} … {% endfor %}.
v-show{% if … %} … {% endif %}.
v-bind:An attribute written out: href="{{ settings.link }}".
v-modelA setting in the schema; a section does not hold state.
v-html{{ settings.body | raw }}, for a richtext setting.
v-text{{ settings.text }}.
v-on:HTML where it does it alone (<details> for open and close, a radio input with :checked for tabs, popovertarget for a pop-up), or a listener in the section's <script> block: root.querySelector(".x").addEventListener("click", …).
@clickHTML where it does it alone (<details> for open and close, a radio input with :checked for tabs, popovertarget for a pop-up), or a listener in the section's <script> block: root.querySelector(".x").addEventListener("click", …).
:keyNothing: a {% for %} needs no key.
:attributeThe attribute itself, with the value in it.

Errors#

Nothing in the framework throws on bad input. Every stage returns either a result or a list of errors in one shape, which the code editor shows beside the line, the AI receives to fix its own output, and the importer attaches to a refused package.

TypeScript
interface ThemeError {
  stage: "parse" | "schema" | "template" | "css" | "script" | "package"
  message: string
  file?: string     // "sections/hero.storelib"
  line?: number     // 1-based, in that file
  column?: number
  path?: string     // "settings[3].default", for errors about data
}

Errors inside the schema's JSON are reported at the line of the .storelib file, not the line of the JSON, so an error always points where you would click. A warning, such as a setting the template never reads, does not stop a save.

StageRefusesExample message
parseA malformed file.<style> is never closed
schemaJSON that breaks the schema rules.default "big" is not a range at settings[2].default
templateBad markup or a refused construct.a <script> tag inside <template>
cssSyntax errors, @import, size.@import is not allowed
scriptForbidden names, unknown capabilities, size."window" is not available to a section
packageA theme file that fails an import check.sha256 does not match the manifest

Checklist for a new section#

  • type in the schema equals the file name.
  • Every setting has an id you are willing to keep for ever. It is the key the creator's content is stored under.
  • Every default is a valid value for its type.
  • The template reads every setting, and declares every setting it reads.
  • Repeated content is blocks, each root carrying data-block-id="{{ block.id }}".
  • Colour comes from scheme, type from the theme, spacing from range settings.
  • Laid out for a phone first, with container queries for wider. See Styling.
  • No fixed id="…"; build ids from {{ section.id }}.
  • At least one preset, or the section is not offered in Add Section.
Sections | Storelib Developer Hub