Basker Docs

Blocks

Create reusable content blocks editors can place in pages, events, and other records

Blocks are reusable content components editors add to pages and other documents from the admin. Each block is a .liquid file in blocks/ with a schema describing its fields and Liquid markup describing its rendering.

Prerequisites

Steps

1. Create a block file in blocks/

Block files use .liquid. The filename should match the block's name in its schema.

blocks/text-section.liquid

2. Define the block schema

Every block must include a {% schema %} tag with a JSON object.

Required:

PropertyTypeDescription
namestringUnique identifier across all blocks in the theme
settingsarrayField definitions for the editor inputs

Recommended:

PropertyTypeDescription
singularstringSingular display label (e.g. "Text Section"). Defaults to name.
pluralstringPlural display label (e.g. "Text Sections"). Defaults to name.
labelstringAdmin display label. Defaults to name.
{% schema %}
{
  "name": "text-section",
  "singular": "Text Section",
  "plural": "Text Sections",
  "label": "Text Section",
  "settings": [
    {
      "type": "text",
      "name": "heading",
      "label": "Heading",
      "required": false
    },
    {
      "type": "richText",
      "name": "body",
      "label": "Body content",
      "required": true
    },
    {
      "type": "checkbox",
      "name": "show_divider",
      "label": "Show divider below section",
      "defaultValue": false
    }
  ]
}
{% endschema %}

3. Write the block markup

Block field data is accessible through block. Each schema field is exposed as block.field_name.

<section class="text-section">
  {% if block.heading != blank %}
    <h2 class="text-section__heading">{{ block.heading }}</h2>
  {% endif %}

  {% if block.body %}
    <div class="text-section__body rich-text">
      {{ block.body_html }}
    </div>
  {% endif %}

  {% if block.show_divider %}
    <hr class="text-section__divider">
  {% endif %}
</section>

The block object also includes:

PropertyTypeDescription
block.idstringUnique instance ID for this block
block.blockTypestringThe block type name

4. Handle rich text fields

Rich text fields split into two properties:

  • block.field_name — boolean, true if the field has content.
  • block.field_name_html — the rendered HTML string.
{% if block.body %}
  <div class="rich-text">
    {{ block.body_html }}
  </div>
{% endif %}

5. Handle relationship fields

Relationship fields resolve to the related document object (or an array for multi-value relationships). Each related document includes a __collection hint.

{
  "type": "relationship",
  "name": "events",
  "label": "Events to feature",
  "relationTo": "events",
  "required": true
}
<section class="featured-events">
  <h2>{{ block.heading }}</h2>

  {% for event in block.events %}
    <article class="featured-events__card">
      {% if event.image %}
        <img
          src="{{ event.image | image_url: width: 600, height: 400, fit: 'cover' }}"
          alt="{{ event.image.alt | default: event.title }}"
        >
      {% endif %}
      <h3>
        <a href="/events/{{ event.slug }}">{{ event.title }}</a>
      </h3>
      {% if event.venue %}
        <p class="featured-events__venue">{{ event.venue.title }}</p>
      {% endif %}
    </article>
  {% endfor %}
</section>

6. Use container field types

array and group allow nesting fields within a block.

Group creates a single nested object:

{
  "type": "group",
  "name": "cta",
  "label": "Call to action",
  "fields": [
    { "type": "text", "name": "label", "label": "Button label" },
    { "type": "url", "name": "link", "label": "Button URL" }
  ]
}
{% if block.cta.link != blank %}
  <a href="{{ block.cta.link }}" class="button">{{ block.cta.label }}</a>
{% endif %}

Array creates a list of repeatable groups:

{
  "type": "array",
  "name": "slides",
  "label": "Slides",
  "fields": [
    { "type": "upload", "name": "image", "label": "Image" },
    { "type": "text", "name": "caption", "label": "Caption" }
  ]
}
{% for slide in block.slides %}
  <div class="slide">
    <img src="{{ slide.image | image_url: width: 1200 }}" alt="{{ slide.caption }}">
    <p>{{ slide.caption }}</p>
  </div>
{% endfor %}

7. Expose analytics hooks (optional)

Editors can give a block stable GA4/GTM reporting hooks — an Analytics Key and an optional Analytics Label — that stay constant even when the block's generated block.id changes. Surfacing them in a custom theme takes two steps.

First, opt the block in by adding "analytics": true to its schema. This adds the Analytics Key and Analytics Label fields to the block's settings in the admin, so authors don't define them by hand.

{
  "name": "rich-text",
  "label": "Rich Text",
  "analytics": true,
  "settings": [
    { "type": "richText", "name": "content", "label": "Content" }
  ]
}

Second, render the attributes by applying the analytics_attributes filter inside the opening tag of the block's wrapper element. Pass the block object; the filter reads the editor-entered key and label and emits data-analytics-key and data-analytics-label.

<section class="rich-text"{{ block | analytics_attributes }}>
  {{ block.content_html }}
</section>

The filter outputs nothing when no Analytics Key is set, so blocks without it render exactly as before. The rendered result lets GTM and GA4 target the wrapper:

<section class="rich-text" data-analytics-key="homepage-rich-text" data-analytics-label="Homepage intro">

You choose which wrapper carries the attributes — apply the filter to whichever element you want tracked. Don't use block.id as a reporting key: it's a generated technical identifier for rendering and debugging only and can change. Configure reporting against data-analytics-key instead.

Complete example

A full blocks/image-gallery.liquid for an arts venue theme:

<section class="image-gallery">
  {% if block.heading != blank %}
    <h2 class="image-gallery__heading">{{ block.heading }}</h2>
  {% endif %}

  {% if block.description %}
    <div class="image-gallery__description rich-text">
      {{ block.description_html }}
    </div>
  {% endif %}

  <div class="image-gallery__grid image-gallery__grid--{{ block.columns }}">
    {% for item in block.images %}
      <figure class="image-gallery__item">
        {% if item.image %}
          <img
            src="{{ item.image | image_url: width: 800, height: 600, fit: 'cover' }}"
            alt="{{ item.image.alt | default: item.caption }}"
            loading="lazy"
          >
        {% endif %}
        {% if item.caption != blank %}
          <figcaption class="image-gallery__caption">{{ item.caption }}</figcaption>
        {% endif %}
      </figure>
    {% endfor %}
  </div>
</section>

{% schema %}
{
  "name": "image-gallery",
  "singular": "Image Gallery",
  "plural": "Image Galleries",
  "label": "Image Gallery",
  "settings": [
    {
      "type": "text",
      "name": "heading",
      "label": "Gallery heading"
    },
    {
      "type": "richText",
      "name": "description",
      "label": "Gallery description"
    },
    {
      "type": "select",
      "name": "columns",
      "label": "Number of columns",
      "options": [
        { "label": "Two", "value": "2" },
        { "label": "Three", "value": "3" },
        { "label": "Four", "value": "4" }
      ],
      "defaultValue": "3"
    },
    {
      "type": "array",
      "name": "images",
      "label": "Gallery images",
      "fields": [
        {
          "type": "upload",
          "name": "image",
          "label": "Image",
          "required": true
        },
        {
          "type": "text",
          "name": "caption",
          "label": "Caption"
        }
      ]
    }
  ]
}
{% endschema %}

Troubleshooting

"Block schema is missing" — The block file has no {% schema %} tag. Add one with name and settings.

"Block schema must include a name" — Add a unique name to the schema.

"Block schema settings must be an array"settings must be a JSON array, even if empty: "settings": [].

Block doesn't appear in the admin — Confirm the block's name is listed in the template's blocks array.

"Unsupported field type" — The field type isn't in the allowed list. See Block schema reference.

On this page