Skip to content

Block Schema

Content blocks are reusable components that editors can add to pages, events, and other content types. Each block has a schema that defines its configuration and available fields.

A block file consists of two parts:

  1. Liquid template - The HTML/Liquid code that renders the block
  2. Schema tag - JSON configuration defining the block’s metadata and settings
{# blocks/hero.liquid #}
<section id="{{ block.id }}" class="hero-block">
<h1>{{ block.heading }}</h1>
<p>{{ block.subheading }}</p>
</section>
{% schema %}
{
"name": "hero",
"singular": "Hero",
"plural": "Heroes",
"label": "Hero Block",
"settings": [
{
"type": "text",
"name": "heading",
"label": "Heading"
},
{
"type": "text",
"name": "subheading",
"label": "Subheading"
}
]
}
{% endschema %}
PropertyTypeRequiredDescription
namestringYesUnique identifier for the block (used in templates)
singularstringNoSingular display name
pluralstringNoPlural display name
labelstringNoHuman-readable label shown in admin
settingsarrayYesArray of field configurations

Inside block templates, the block object provides access to:

PropertyDescription
block.idUnique identifier for this block instance
block.typeThe block type name (e.g., "hero")
block.[fieldname]Value of any field defined in settings
block.[fieldname]_htmlHTML-rendered version of richtext fields
<section id="{{ block.id }}" class="block-{{ block.type }}">
<h2>{{ block.title }}</h2>
{{ block.content_html }}
</section>

Block settings support all standard field types. See Input Settings for complete documentation.

{
"type": "text",
"name": "title",
"label": "Title"
}
<h2>{{ block.title }}</h2>
{
"type": "richtext",
"name": "content",
"label": "Content"
}
<div class="prose">
{{ block.content_html }}
</div>
{
"type": "checkbox",
"name": "show_border",
"label": "Show Border"
}
<div class="card {% if block.show_border %}card--bordered{% endif %}">
...
</div>
{
"type": "upload",
"name": "image",
"label": "Image",
"relationTo": "media"
}
{% if block.image %}
<img
src="{{ block.image | image_url: width: 800 }}"
alt="{{ block.image.alt }}"
>
{% endif %}
{
"type": "relationship",
"name": "featured_event",
"label": "Featured Event",
"relationTo": "events",
"hasMany": false
}
{% if block.featured_event %}
<a href="/events/{{ block.featured_event.slug }}">
{{ block.featured_event.title }}
</a>
{% endif %}

Groups organize related fields together:

{
"type": "group",
"name": "cta",
"label": "Call to Action",
"fields": [
{
"type": "text",
"name": "text",
"label": "Button Text"
},
{
"type": "text",
"name": "url",
"label": "Button URL"
}
]
}
{% if block.cta.text and block.cta.url %}
<a href="{{ block.cta.url }}" class="btn">
{{ block.cta.text }}
</a>
{% endif %}

Arrays allow multiple items of the same structure:

{
"type": "array",
"name": "items",
"labels": {
"singular": "Item",
"plural": "Items"
},
"fields": [
{
"type": "text",
"name": "title",
"label": "Title"
},
{
"type": "richtext",
"name": "content",
"label": "Content"
}
]
}
{% for item in block.items %}
<div class="item">
<h3>{{ item.title }}</h3>
{{ item.content_html }}
</div>
{% endfor %}

Here’s a comprehensive example of a feature panel block:

{# blocks/feature-panel.liquid #}
<section id="{{ block.id }}" class="feature-panel {% if block.right_align %}feature-panel--right{% endif %}">
<div class="feature-panel__inner">
{% if block.image %}
<div class="feature-panel__media">
<img
src="{{ block.image | image_url: width: 800, height: 600 }}"
alt="{{ block.image.alt | default: block.title }}"
>
</div>
{% endif %}
<div class="feature-panel__content">
{% if block.prefix %}
<span class="feature-panel__prefix">{{ block.prefix }}</span>
{% endif %}
{% if block.title %}
<h2 class="feature-panel__title">{{ block.title }}</h2>
{% endif %}
{% if block.details %}
<div class="feature-panel__details prose">
{{ block.details_html }}
</div>
{% endif %}
{% if block.cta.text and block.cta.url %}
<a href="{{ block.cta.url }}" class="btn">
{{ block.cta.text }}
</a>
{% endif %}
</div>
</div>
</section>

Templates can specify which blocks are available using the blocks array in their schema:

{# templates/event.liquid #}
{% layout 'layouts/default.liquid' %}
{% capture content_for_layout %}
<h1>{{ event.title }}</h1>
{% stageblocks event %}
{% endcapture %}
{% schema %}
{
"name": "event",
"settings": [...],
"blocks": [
"accordion",
"content",
"feature-panel",
"image-gallery",
"video-gallery",
"well"
]
}
{% endschema %}

When the blocks array is defined, only those block types will be available when editing content of that type. If omitted, all blocks are available.

Templates can also define their own settings that become custom fields on the content type. These are accessible via [content].theme:

{# templates/event.liquid #}
{% schema %}
{
"name": "event",
"settings": [
{
"type": "upload",
"name": "hero_image",
"label": "Hero Image",
"relationTo": "media"
},
{
"type": "group",
"name": "primaryBtn",
"label": "Primary Button",
"fields": [
{ "type": "text", "name": "title", "label": "Title" },
{ "type": "text", "name": "link", "label": "Link" }
]
}
],
"blocks": [...]
}
{% endschema %}

Accessing template settings:

{% if event.theme.settings.hero_image %}
<img src="{{ event.theme.settings.hero_image | image_url: width: 1200 }}">
{% endif %}
{% if event.theme.settings.primaryBtn.title %}
<a href="{{ event.theme.settings.primaryBtn.link }}">
{{ event.theme.settings.primaryBtn.title }}
</a>
{% endif %}
  1. Use semantic names - Block names should describe their purpose (e.g., feature-panel not block-1)

  2. Provide helpful labels - Clear labels help editors understand what each field does

  3. Group related fields - Use group type to organize related fields together

  4. Handle empty states - Always check if fields have values before rendering

  5. Use unique IDs - Include {{ block.id }} in section IDs for JavaScript targeting and anchor links

  6. Document your blocks - Consider adding comments in the template explaining complex logic

  7. Test with empty content - Ensure blocks render gracefully when fields are empty