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/. Its schema describes its fields, and its Liquid markup describes its rendering.

Start with a theme directory that has a blocks/ folder. The block schema reference lists every supported field.

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

Access block field data through block. Liquid exposes each schema field 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. Multi-value relationships resolve to an array of objects instead.

{
  "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 and GTM reporting hooks: an Analytics Key and an optional Analytics Label. These hooks stay constant even when the block's generated block.id changes. Adding them to a custom theme takes two steps.

First, add "analytics": true to the block's schema. This adds the Analytics Key and Analytics Label fields to the block's settings in the admin. Authors do not need to define them by hand.

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

Second, apply the analytics_attributes filter inside the opening tag of the block's wrapper element. This filter renders the attributes. Pass it the block object. The filter reads the editor-entered key and label. It emits data-analytics-key and data-analytics-label.

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

If the editor does not set an Analytics Key, the filter outputs nothing. 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 to track. Do not use block.id as a reporting key. It is a generated technical identifier for rendering and debugging only. It can change. Configure reporting against data-analytics-key instead.

8. Add settings to the Custom Liquid block (optional)

Sites on a plan that includes Custom Liquid get a built-in Custom Liquid block. The editor writes Liquid code into its Liquid Code field. Basker renders that code directly. It does not use a block file.

A theme can add its own fields to this block. Create blocks/liquidBlock.liquid. Put only a {% schema %} tag in the file. Set name to liquidBlock. Basker ignores the markup in this file.

{% schema %}
{
  "name": "liquidBlock",
  "settings": [
    { "type": "text", "name": "heading", "label": "Heading" },
    { "type": "checkbox", "name": "show_border", "label": "Show border" }
  ]
}
{% endschema %}

The editor sees the fields below the Liquid Code field. The Liquid code reads the values from block.*, the same as any other block:

<h2>{{ block.heading }}</h2>
{% if block.show_border %}<hr />{% endif %}

Do not use the names code, isHidden, hydrationAnalysisStatus, hydrationAnalysisCodeHash, hydrationAnalysisErrors, or hydrationHints. Basker uses these names for the built-in fields and ignores declared fields with the same name.

Themes that do not include blocks/liquidBlock.liquid are not changed. Sites on a plan without Custom Liquid do not see the block.

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 does not appear in the admin: Confirm the template's blocks array lists the block's name.

"Unsupported field type": The field type is not in the allowed list. See Block schema reference.

On this page