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 use to compose pages. Each block is one Liquid file in blocks/. It contains the markup plus a {% schema %} tag that describes the fields editors complete. Once the block exists and appears in a template's allowed-blocks array, an editor can add it to a page. The editor can then position it and set its content.
You will build three blocks: a rich-text section, an image feature, and a feature grid. Together they cover most of the field types you use every day.
Step 1: A rich-text block
The simplest useful block is a styled prose section.
Create blocks/text-section.liquid:
<section id="text-section-{{ block.id }}" class="block-text-section s-prose">
{{ block.content_html }}
</section>
{% schema %}
{
"name": "text-section",
"label": "Text section",
"singular": "Text section",
"plural": "Text sections",
"settings": [
{
"type": "richText",
"name": "content",
"label": "Content"
}
]
}
{% endschema %}A few notes:
block.idis unique for each placed instance. This makes it useful for IDs or anchor links.richTextfields expose a ready-to-render HTML companion,block.content_html. Theblock.contentfield itself is alwaystrueonce the field exists. Testblock.content_htmlinstead, to check whether there is anything to render.nameis the block's identifier: lowercase and hyphenated. It is what you list in a template's allowed-blocks array.label,singular, andpluralare what editors see in the admin. If you omit them, they default toname. The upload validator also warns you about this when you upload the theme.
Step 2: An image feature
This block uses 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",
"defaultValue": 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)
This block lets the editor add any number of items, each with its own fields. This is the array field type with a fields sub-array that describes 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. 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. Update its schema to list the block names in the blocks array:
{% schema %}
{
"settings": [],
"blocks": ["text-section", "image-feature", "feature-grid"]
}
{% endschema %}If you want the homepage to support the same blocks, do the same for templates/page.homepage.liquid. 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. If you added a schema to templates/page.homepage.liquid in Step 4, you should see something like:
Theme check: 0 errors, 0 warnings, 0 infoIf you skipped that optional edit, page.homepage.liquid is still schema-less and the checker reports one warning instead:
Theme check: 0 errors, 1 warnings, 0 info
- [warning] templates/page.homepage.liquid: Template is missing a schema tag. (template-missing-schema)The checker reports the file and line for three kinds of problems. A block schema might have no name. A block schema might have invalid JSON. A template might reference a block that does not exist.
Step 6: Add a block to a page in the admin
Open any page in your Basker admin. Look for the blocks panel. Your three new blocks should appear in the picker. Add a "Text section" block. Paste some content into it. Save the page. 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 use snippets instead of blocks.
- Block schema: the full schema reference.
- Field types: every field type available in
settings.