Skip to Content
Sections & settings schema

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:

TypeNotes
text, textarea, richtextText inputs. richtext renders as HTML.
number, rangeNumeric. range takes min / max / step.
select, radioTake options: [{ value, label }].
checkboxBoolean toggle.
colorSolid hex only - no rgba().
image_pickerMerchant-uploaded image; value is a full URL.
urlA link the merchant picks.
font_pickerFont family.
headerLabel-only divider in the editor (no value).
menu_handlePicker of the tenant’s menus; store the handle, read via linklists[value].
link_listStructured 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, srcset descriptors, 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.