Sections & settings schema
Every file in sections/ ends with a {% schema %}...{% endschema %} block of
JSON. The platform reads this and renders it as a form in the merchant’s visual
builder. All user-facing content must be a schema setting - no hardcoded
copy or images anywhere.
Anatomy of a section
<!-- sections/hero.liquid -->
<section class="hero" data-section-id="{{ section.id }}">
{% for block in section.blocks %}
{% if block.type == 'slide' %}
<div class="hero__slide" {{ block.attributes }}>
<h1>{{ block.settings.heading }}</h1>
{% assign btn_url = block.settings.cta_url %}
{% if block.settings.cta_label != blank and btn_url != blank and btn_url != '#' %}
<a href="{{ btn_url }}" class="btn">{{ block.settings.cta_label }}</a>
{% endif %}
</div>
{% endif %}
{% endfor %}
</section>
{% schema %}
{
"name": "Hero",
"tag": "section",
"class": "hero-wrapper",
"settings": [
{ "type": "range", "id": "height_vh", "label": "Height (vh)", "min": 60, "max": 100, "default": 88 },
{ "type": "color", "id": "dot_active_color", "label": "Active dot color", "default": "#FFFFFF" }
],
"blocks": [
{
"type": "slide",
"name": "Slide",
"settings": [
{ "type": "image_picker", "id": "image", "label": "Image" },
{ "type": "text", "id": "heading", "label": "Heading", "default": "New season" },
{ "type": "url", "id": "cta_url", "label": "CTA URL", "default": "/collections/all" },
{ "type": "text", "id": "cta_label", "label": "CTA label", "default": "Shop now" }
]
}
],
"presets": [
{
"name": "Hero (default)",
"blocks": [
{ "type": "slide", "settings": { "heading": "New season", "cta_label": "Shop now" } }
]
}
]
}
{% endschema %}Setting types
Every setting needs id, type, label. Supported types:
| Type | Notes |
|---|---|
text, textarea, richtext | Text inputs. richtext renders as HTML. |
number, range | Numeric. range takes min / max / step. |
select, radio | Take options: [{ value, label }]. |
checkbox | Boolean toggle. |
color | Solid hex only - no rgba(). |
text_alignment | Left, centre or right, drawn as three icon buttons. Value is left, center or right. |
image_picker | Merchant-uploaded image; value is a full URL. |
url | A link the merchant picks. |
font_picker | Font family. |
header | Label-only divider in the editor (no value). |
menu_handle | Picker of the tenant’s menus; store the handle, read via linklists[value]. |
link_list | Structured multi-row editor. Value is Label|/url lines by default. |
Optional keys: default, info, placeholder, min / max / step,
options, fields.
Blocks - repeating content
For anything that repeats (slides, testimonials, FAQ rows, footer link columns,
social icons, trust-strip items), model it as blocks so the merchant can
add / remove / reorder in the builder. Never model repeating content as fixed
slot_1..N settings.
"blocks": [
{ "type": "testimonial", "name": "Testimonial", "settings": [
{ "type": "textarea", "id": "quote", "label": "Quote" },
{ "type": "text", "id": "author", "label": "Author" }
]}
]Read them with {% for block in section.blocks %} and branch on block.type.
Put {{ block.attributes }} on each block’s outer element. It outputs
data-block-id=<id>, which is how the visual editor finds the block in the
preview.
Blocks declared in a section’s schema are flat: they can’t hold other blocks. When the merchant should arrange the inside of a section themselves, use theme blocks instead.
Theme blocks - blocks inside blocks
A theme block is a file in blocks/ with its own markup and its own
{% schema %}. Any section, or any other theme block, can hold it, so a
merchant can build a product section from a group that holds a row that holds a
price and a button, and reorder any of them.
<!-- blocks/group.liquid -->
<div class="group group--{{ block.settings.direction }}" {{ block.attributes }}>
{% content_for 'blocks' %}
</div>
{% schema %}
{
"name": "Group",
"settings": [
{ "type": "select", "id": "direction", "label": "Direction", "default": "column",
"options": [{ "value": "column", "label": "Stacked" }, { "value": "row", "label": "Side by side" }] }
],
"blocks": [{ "type": "@theme" }],
"presets": [{ "name": "Group" }]
}
{% endschema %}A section opts in the same way, by listing theme block types in its blocks
array: { "type": "@theme" } accepts any theme block, { "type": "price" }
accepts only that one. Use either theme blocks or section-defined blocks in a
section’s list, not both. A static theme block is the exception: the theme
places it with content_for 'block', so it doesn’t go in the list and can sit
in any section, including one with its own blocks. That is how to add
arrangeable content to an existing section without converting its blocks.
| Tag | What it renders |
|---|---|
{% content_for 'blocks' %} | The children of the current section or theme block, in the order the merchant set. |
{% content_for 'block', type: 'price', id: 'price' %} | One static block that the theme places itself. The merchant can edit its settings but can’t move or remove it. If the merchant never changed it, it renders with its schema defaults. |
Inside a theme block you have block.id, block.type, block.settings (with
schema defaults filled in), block.attributes and block.blocks_count, the
number of saved children that content_for 'blocks' would render. The surrounding section and
every global (product, shop, settings…) are still available, so a price
block inside a product section reads product.price directly.
Use block.blocks_count to show default content when a store has saved no
children, for example a store that updated from a version of your theme that
had no blocks. Without it, such a block renders empty until the merchant adds
blocks:
{%- if block.blocks_count > 0 -%}
{%- content_for 'blocks' -%}
{%- else -%}
{%- content_for 'block', type: 'product-title', id: 'product-title' -%}
{%- content_for 'block', type: 'product-price', id: 'product-price' -%}
{%- content_for 'block', type: 'buy-buttons', id: 'buy-buttons' -%}
{%- endif -%}Limits and behaviour:
- Blocks nest up to 8 levels deep, with at most 50 blocks per section, counting every level. Saving a page past either limit fails, and the storefront stops rendering past level 8.
- A block whose Liquid fails to render is replaced by an HTML comment. Its siblings and the rest of the page still render.
- Section CSS is loaded per section type (
assets/sections/<type>.css), not per block. Style theme blocks from the CSS of the sections that use them, or fromcommon.csswhen several sections share them.
Style settings on blocks
Size, colour, alignment, background and padding are ordinary block settings that your theme declares and your CSS reads; the platform adds none of its own and never writes CSS onto your markup. A pattern that keeps a block’s look unchanged until the merchant moves a control:
- Give every style setting a default that matches the block’s current look,
and put type controls behind a “Theme default / Custom” select with
visible_ifon the custom fields. - Turn only the values the merchant changed into CSS, scoped to the block with
[data-block-id=<id>](unquoted:{{ }}output is HTML-escaped). Add"responsive": trueto sizes and padding so mobile can differ. - An empty
colorvalue means “use the theme’s”; set the setting’splaceholderto say so (“Theme default”, or “None” for a background).
Avenu’s snippets/block-style.liquid is a worked example.
In templates/*.json, a theme block saves its children the same way a section
does, with blocks and block_order. A static block is saved under the id the
theme gave it, with "static": true. List static blocks in block_order too,
in the order your Liquid places them: the storefront skips them there, and the
editor uses that order to show them.
"main": {
"type": "main-product",
"settings": {},
"blocks": {
"price": { "type": "price", "static": true, "settings": {} },
"info": {
"type": "group",
"settings": { "direction": "column" },
"blocks": {
"title": { "type": "product-title", "settings": {} },
"buy": { "type": "buy-buttons", "settings": {} }
},
"block_order": ["title", "buy"]
}
},
"block_order": ["price", "info"]
}Presets
presets[] defines the default content a section ships with when the merchant
drops it onto a page fresh from the picker. Always supply at least one preset.
For sections listed in templates/*.json, the template’s settings win - the
preset is what appears when the section is added anew.
Global settings - config/settings_schema.json
An array of category groups, using the same setting types. Merchant overrides
merge on every render, read as {{ settings.<id> }}.
[
{
"name": "Brand",
"settings": [
{ "type": "image_picker", "id": "logo", "label": "Logo" },
{ "type": "text", "id": "logo_text", "label": "Logo text" }
]
},
{
"name": "Colors",
"settings": [
{ "type": "color", "id": "color_bg", "label": "Background", "default": "#FFFFFF" },
{ "type": "color", "id": "color_primary", "label": "Primary", "default": "#0F0F0F" }
]
},
{
"name": "Typography",
"settings": [
{ "type": "font_picker", "id": "font_heading_family", "label": "Heading font", "default": "Space Grotesk" }
]
}
]The no-hardcoded-content rule
Every visible character and image must be merchant-editable. The only acceptable hardcoded strings in Liquid are:
- Platform data -
product.*,collection.*,cart.*,customer.*,linklists.*,shop.*,settings.*. - Structural / accessibility strings the merchant can’t meaningfully edit -
visually-hidden ARIA labels,
srcsetdescriptors, engine microcopy.
Everything else - section headings, hero headlines, button labels, eyebrow text,
“or” separators, trust-strip copy, footer column titles, the copyright line,
social URLs - must be a text / richtext / image_picker / url setting.
Payment icons
Do not hardcode the card set. Render the payment row from
shop.enabled_payment_types (or output shop.payment_icons_html verbatim),
gated by one boolean setting show_payment_icons. This reflects the store’s
real accepted methods.
Guard empty CTAs
A button whose _label is filled but _url is empty (or still #) must not
render:
{% assign btn_url = section.settings.button_url %}
{% if section.settings.button_label != blank and btn_url != blank and btn_url != '#' %}
<a href="{{ btn_url }}" class="btn">{{ section.settings.button_label }}</a>
{% endif %}White-label rule
Theme source must contain zero platform-brand mentions - no visible strings, no marketing links, no logos, and no “powered by” markup or setting. The powered-by credit is injected by the platform at render time; you never ship it.