A Vein section is a single template file with one {% schema %} block. The schema is the authority for the section itself: its settings, its blocks, and the editor metadata used to render its controls. Where a section may be placed — which surfaces accept it, and which are required — is owned separately by the theme's surface rules, not by the section schema. This page is the exact section contract the parser enforces.
Anatomy of a section#
A section file is template markup followed by a {% schema %} block containing valid JSON. The markup renders the section; the schema declares what a merchant can configure. A file has exactly one {% schema %} block. It may appear anywhere in the file — top, bottom, or between markup — and it must not be nested inside another tag (an if, a for). Markup before and after the schema renders normally; the schema itself produces no output. Putting it at the end is a common convention, not a requirement.
<section class="hero" data-tf-field="hero">
<h1>{{ settings.heading }}</h1>
{% if settings.show_button %}
<a href="{{ settings.button_url | default: '/' }}">{{ settings.button_label }}</a>
{% endif %}
</section>
{% schema %}
{
"name": "hero",
"kind": "section",
"settings": [
{ "id": "heading", "type": "text", "default": "Welcome",
"editor": { "label": "Heading", "group": "content" } },
{ "id": "show_button", "type": "checkbox", "default": true,
"editor": { "label": "Show button", "group": "content" } },
{ "id": "button_label", "type": "text", "default": "Shop now",
"editor": { "label": "Button label", "group": "content" } },
{ "id": "button_url", "type": "text",
"editor": { "label": "Button link", "group": "content" } }
]
}
{% endschema %}
Top-level schema fields#
Field |
Required |
Meaning |
|---|---|---|
name |
Yes |
The section id (unique). Referenced by a theme's surface to place this section. |
kind |
No |
section (default) | layout | scene | snippet. Most sections are kind "section". |
target |
No |
The surface slot this section is built for (e.g. global_body_start). |
ctx_needs |
No |
Data the section reads at render — declared so the host loads exactly what's needed. Supported roots: shop, menus, localization, localization_prompt, restock_notifications, cart, wishlist, reviews, product, products, collection, category, page, policy, blog, blog_recent, blog_post, blog_category, vendor, brand, list, customer, customer_settings, csrf_token, form_error, auth_notice. Sub-keys select cheap tiers (cart.item_count, wishlist.item_count, reviews.stats). Shapes: see the CTX Runtime context reference. |
settings |
No |
Section-level controls: an array of setting definitions. |
blocks |
No |
Repeatable content units the merchant can add/reorder/remove. |
Settings#
A setting is one configurable value. A setting is editor-visible only if it carries an "editor" block (label + optional group). A setting without "editor" is runtime-injected by the host and hidden from the merchant — absence of "editor" never means editable.
Key |
Required |
Meaning |
|---|---|---|
id |
Yes |
Unique within the section (and within a block). Read in markup as settings. |
type |
Yes |
The control type (see below). |
default |
No |
Default value. The section should look designed on add — give real defaults. |
options |
For select |
Array of { value, label } for select controls. |
fields |
For object |
Array of { key, type, label } for an object (grouped) setting. |
editor |
To be editable |
{ label, group }. Present ⟹ shown in the editor; absent ⟹ hidden/runtime-only. |
Setting control types#
type |
Renders as |
Value stored |
|---|---|---|
text |
Single-line text input |
string |
textarea |
Multi-line text input |
string |
richtext |
Rich text editor (bold, italic, lists, links, headings) |
HTML string — the platform sanitizes it to a safe allowlist at render and emits it as trusted markup; you just write {{ settings. |
vendor |
Search-and-pick a vendor |
stable id — resolved to the live vendor object at render (see Resource pickers) |
category |
Search-and-pick a category |
stable id — resolved to the live category object at render |
brand |
Search-and-pick a brand |
stable id — resolved to the live brand object at render |
product |
Search-and-pick a product |
stable id — resolved to the FULL listing card at render (title, url, vendor, featured_image, price / price_range in the visitor's market currency, available_for_sale, purchase_state); nil when hidden or deleted |
collection |
Search-and-pick a collection |
stable id — resolved to the live collection (name, slug, url, image_url, product_count) at render |
blog_post |
Search-and-pick a blog post |
stable id — resolved to the live post (title, slug, url, excerpt, image_url) at render |
blog_category |
Search-and-pick a blog category |
stable id — resolved to the live category (name, slug, url, post_count) at render |
page |
Search-and-pick a page |
stable id — resolved to the live page (title, slug, url) at render |
policy |
Pick a policy |
the policy's fixed type (privacy, terms...) — resolved to the live policy (title, slug, url) at render |
number |
Number input |
number |
checkbox |
Toggle |
boolean |
select |
Dropdown (needs options) |
one option value |
color |
Color picker |
hex string |
image |
Image picker |
file id → resolved to /assets/ |
url |
URL input |
string |
menu |
Menu picker |
menu handle (host-resolved to nav items) |
object |
Grouped sub-fields |
object of { field key → value }, fields declared in "fields" |
Resource picker settings#
A resource picker (vendor, category, brand, product, collection, blog_post, blog_category, page, policy) stores the picked entity's STABLE id — never its name or slug, which go stale. At render the host resolves the id to the live entity IN PLACE: {{ settings.vendor }} IS the vendor object. A deleted entity resolves to nil — guard and render a placeholder. List pickers (product_list, collection_list) store an ordered id list and resolve to a dense, ordered list of the same card objects. Menus, pickers, list pickers, and @dynamic sources are covered in depth — including their identical behavior on block settings — in Bound Settings.
{% if settings.vendor %}
<a href="{{ settings.vendor.url }}">{{ settings.vendor.name }}</a>
{% else %}
{{ 'logo' | placeholder_svg }}
{% endif %}
{% schema %}
{
"name": "featured-vendor",
"settings": [
{ "id": "vendor", "type": "vendor",
"editor": { "label": "Vendor", "group": "content" } }
]
}
{% endschema %}
Blocks#
Blocks are the repeatable content units inside a section — the merchant adds, reorders, hides, and removes them. A section declares which block types it allows; each block type has its own settings, validated exactly like section settings.
"blocks": {
"max": 8,
"default": ["slide", "slide"],
"types": [
{ "type": "slide",
"editor": { "label": "Slide" },
"settings": [
{ "id": "image", "type": "image",
"editor": { "label": "Image", "group": "content" } },
{ "id": "caption", "type": "text",
"editor": { "label": "Caption", "group": "content" } }
] }
]
}
Field |
Meaning |
|---|---|
max |
Maximum number of blocks (0 / omitted = unbounded). |
default |
Ordered list of block types created when the section is added. Every type listed must exist in types. |
types |
The allowed block types. Each: { type, editor, settings }. |
Rendering blocks#
In the markup, iterate settings.blocks and branch on b.type. Read a block's own values from b.settings. Skip a hidden block with b.disabled. Wrap each block's output in the editor markers so it is selectable in the editor (these are no-ops on the live storefront).
{% for b in settings.blocks %}
{% unless b.disabled == true %}
{% if b.type == 'slide' %}
{{ b._editor_block_open }}
<figure class="slide">
{% if b.settings.image %}<img src="{{ b.settings.image }}" alt="{{ b.settings.caption }}">{% endif %}
{% if b.settings.caption %}<figcaption>{{ b.settings.caption }}</figcaption>{% endif %}
</figure>
{{ b._editor_block_close }}
{% endif %}
{% endunless %}
{% endfor %}
Rules the parser enforces#
Rule |
|
|---|---|
1 |
name is required; ids are unique within the section and within each block. |
2 |
Every editable setting declares a supported control type (text, textarea, richtext, number, checkbox, select, color, image, object, menu, url, vendor, category, brand); an unknown type is rejected. |
3 |
A select setting must declare options; an object setting must declare fields with unique non-empty keys. |
4 |
Every exposed block type must declare at least one editable setting. |
5 |
Every type in blocks.default must be declared in blocks.types. |
6 |
There is exactly one {% schema %} block; it may appear anywhere in the file but must not be nested inside another tag. |
7 |
These rules are enforced by the section parser when the file is loaded. An invalid schema fails at save/review time, not during storefront render. |
8 |
Separately, the save/validation contract enforces the data shape: if a section declares blocks, its stored settings.blocks is required; and stored settings may contain only schema-declared keys — section settings at the section level, block settings per block type. Unknown keys are rejected on save. |