Build reusable blocks
Three composable blocks an editor can drop into any page — a rich-text section, an image feature, and a feature grid
Blocks are the units editors compose pages from. Each block is one Liquid file in blocks/ containing the markup plus a {% schema %} describing the fields editors fill in. Once the block exists and is listed on a template's allowed-blocks array, an editor can add it, position it, and configure its content.
You'll build three: a rich-text section, an image feature, and a feature grid. Together they cover most of the field types you'll use day-to-day.
Step 1 — A rich-text block
The simplest useful block: a styled prose section.
Create blocks/text-section.liquid:
<section id="text-section-{{ block.id }}" class="block-text-section s-prose">
{{ block.content_html | raw }}
</section>
{% schema %}
{
"name": "text-section",
"label": "Text section",
"singular": "Text section",
"plural": "Text sections",
"settings": [
{
"type": "richText",
"name": "content",
"label": "Content"
}
]
}
{% endschema %}Things worth pointing out:
block.idis unique per placed instance — useful for IDs or anchor links.richTextfields automatically expose a<setting>_htmlcompanion containing the rendered HTML. The raw value is inblock.content, the rendered HTML is inblock.content_html. Use| rawso Liquid doesn't escape it.nameis the block's identifier. Lowercase, hyphenated. It's what you'll list in template allowed-blocks arrays.label,singular, andpluralare what editors see in the admin. If you omit them, they default toname(and the theme checker warns).
Step 2 — An image feature
A block with three field types — an image upload, a text caption, and a checkbox toggle.
Create blocks/image-feature.liquid:
<section id="image-feature-{{ block.id }}"
class="block-image-feature {% if block.fullWidth %}block-image-feature--full{% endif %}">
{% if block.image %}
<img src="{{ block.image.url }}" alt="{{ block.image.alt | default: block.caption }}">
{% endif %}
{% if block.caption %}
<figcaption>{{ block.caption }}</figcaption>
{% endif %}
</section>
{% schema %}
{
"name": "image-feature",
"label": "Image feature",
"singular": "Image feature",
"plural": "Image features",
"settings": [
{
"type": "upload",
"name": "image",
"label": "Image",
"relationTo": "media"
},
{
"type": "text",
"name": "caption",
"label": "Caption"
},
{
"type": "checkbox",
"name": "fullWidth",
"label": "Full-bleed",
"default": false
}
]
}
{% endschema %}upload fields with "relationTo": "media" give editors the standard media-library picker. The selected file's url, alt, width, and height are all available on block.image.
Step 3 — A feature grid (array with nested fields)
A block where the editor adds an arbitrary number of items, each with its own fields. This is the array field type with a fields sub-array describing the per-item schema.
Create blocks/feature-grid.liquid:
<section id="feature-grid-{{ block.id }}" class="block-feature-grid">
{% if block.title %}
<h2 class="block-feature-grid__title">{{ block.title }}</h2>
{% endif %}
<ul class="block-feature-grid__items">
{% for item in block.items %}
<li class="feature">
{% if item.icon %}
<img class="feature__icon" src="{{ item.icon.url }}" alt="">
{% endif %}
<h3 class="feature__title">{{ item.title }}</h3>
<p class="feature__description">{{ item.description }}</p>
</li>
{% endfor %}
</ul>
</section>
{% schema %}
{
"name": "feature-grid",
"label": "Feature grid",
"singular": "Feature grid",
"plural": "Feature grids",
"settings": [
{
"type": "text",
"name": "title",
"label": "Section title"
},
{
"type": "array",
"name": "items",
"label": "Features",
"fields": [
{
"type": "upload",
"name": "icon",
"label": "Icon",
"relationTo": "media"
},
{
"type": "text",
"name": "title",
"label": "Title"
},
{
"type": "text",
"name": "description",
"label": "Description"
}
]
}
]
}
{% endschema %}Inside the {% for item in block.items %} loop, each item is a drop with the fields defined under fields. Group and array nesting work the same way recursively, though the validator emits a warning if you nest deeper than four levels.
Step 4 — Allow the blocks on your templates
Right now the blocks exist but no template lets editors place them. Open templates/page.liquid and update its schema to list the block names in the blocks array:
{% schema %}
{
"settings": [],
"blocks": ["text-section", "image-feature", "feature-grid"]
}
{% endschema %}Do the same for templates/index.liquid if you want the homepage to support the same blocks. Each entry must match a block file's name exactly — typos produce a warning during validation.
Step 5 — Run the local theme checker
Press c in the dev-server terminal to run the checker against your local files. You should see something like:
Theme check: 0 errors, 0 warnings, 0 infoIf a block schema is missing a name, has invalid JSON, or a template references a block that doesn't exist, the checker tells you which file and line.
Step 6 — Add a block to a page in the admin
Open any page in your Basker admin and look for the blocks panel. Your three new blocks should appear in the picker. Add a "Text section", paste in some content, and save. Switch back to the dev-server browser tab — the block renders below the page body, in the spot reserved by {% stageblocks page %}.
What's next
Variants and theme settings — turn the homepage into a richer variant template and expose theme-wide options through the admin.
Going deeper
- Blocks — block lifecycle, drop semantics, when to reach for snippets vs blocks.
- Block schema — the full schema reference.
- Field types — every field type available in
settings.