This page builds one real section — a testimonial strip — from an empty file to something a merchant can add, arrange, and edit. Every section you will ever write follows these same steps.
1. Create the file#
A section is one file in your theme's sections/ folder. The file name is yours; the section's identity is the "name" in its schema. Start with static markup and the smallest valid schema — a name.
{# sections/testimonials.vein #}
<section class="testimonials">
<h2>What customers say</h2>
</section>
{% schema %}
{ "name": "testimonials" }
{% endschema %}
2. Make it configurable#
Replace the hardcoded heading with a setting. A setting needs an id, a type, and — to be merchant-editable — an "editor" block with a label. Give it a real default: a section should look designed the moment it is added.
<section class="testimonials">
<h2>{{ settings.heading }}</h2>
</section>
{% schema %}
{
"name": "testimonials",
"settings": [
{ "id": "heading", "type": "text", "default": "What customers say",
"editor": { "label": "Heading", "group": "content" } }
]
}
{% endschema %}
3. Add repeatable content: blocks#
Each testimonial is a block: the merchant adds as many as they like, reorders them, hides one, removes one. Declare the allowed block types (each with its own settings), then render settings.blocks in the markup — branch on b.type, read b.settings, skip b.disabled, and wrap each block in the editor markers (no-ops on the live storefront, click-to-select in the editor).
<section class="testimonials">
<h2>{{ settings.heading }}</h2>
{% for b in settings.blocks %}
{% unless b.disabled == true %}
{% if b.type == 'quote' %}
{{ b._editor_block_open }}
<blockquote>
<p>{{ b.settings.text }}</p>
<cite>{{ b.settings.author }}</cite>
</blockquote>
{{ b._editor_block_close }}
{% endif %}
{% endunless %}
{% else %}
<p>No testimonials yet.</p>
{% endfor %}
</section>
{% schema %}
{
"name": "testimonials",
"settings": [
{ "id": "heading", "type": "text", "default": "What customers say",
"editor": { "label": "Heading", "group": "content" } }
],
"blocks": {
"max": 6,
"default": ["quote", "quote"],
"types": [
{ "type": "quote",
"editor": { "label": "Quote" },
"settings": [
{ "id": "text", "type": "textarea", "default": "Absolutely love it.",
"editor": { "label": "Quote", "group": "content" } },
{ "id": "author", "type": "text", "default": "A happy customer",
"editor": { "label": "Author", "group": "content" } }
] }
]
}
}
{% endschema %}
4. What review checks#
When this file is saved or imported, the parser enforces the contract: the schema is valid JSON with a name; every setting type is from the closed set; every block type has at least one editable setting; every type in blocks.default exists in blocks.types; the markup compiles; and any context data you read is declared in ctx_needs. An invalid section fails at review — never at a shopper's render.
Where to go next#
The Template Language is the full markup reference (tags, filters, loops, truthiness). The {% schema %} Contract lists every setting type and rule. Host Filters covers money, t, and placeholder_svg — prices, translations, and placeholder artwork. Theme Locale Files shows how your theme ships its translations.