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(). |
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.
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.