Skip to Content

Blocks

Learn how to create and customize theme blocks.

Introduction to Blocks

Blocks are modular content elements that can be added to sections in Finqu themes. They allow for flexible and customizable layouts within a section. Most blocks render content directly; some blocks are layout blocks that contain child blocks in named containers (nesting is limited to a single level).

How Blocks Are Rendered

Blocks are rendered inside sections using the container tag. Each block is defined in the blocks/ directory and rendered in the order specified by the theme editor or default configuration.

You can also render a block statically with the block tag:

{% block 'product-title' %} {% block 'app/part-payment' id: 'product-part-payment' %}

Inline display

For side-by-side block layout (e.g. product card rows), use display: 'inline' on the block tag or in the block schema:

{% block 'product-price', display: 'inline' %}
{ "name": { "en": "Price" }, "category": "content", "display": "inline" }

Inline blocks flow horizontally in the designer instead of stacking vertically.

Block Directory

All block files are stored in the blocks directory of your theme. Each file represents a single block and contains its Liquid markup, logic, and schema.

Block Schema

Each block file defines its configuration using a {% schema %} tag. The schema describes the block’s name, description, tag, class, category, and settings. For example:

{% schema %} { "name": { "en": "FAQ Item", "fi": "UKK-kohta" }, "description": { "en": "A single question and answer pair." }, "tag": "div", "class": "block block-faq-item", "category": "content", "settings": { // ...block settings... } } {% endschema %}
  • The category property must match one of the categories defined in your theme’s settings_schema.json file.

Block Schema Properties

Below is a table describing the main properties available in a block schema, along with explanations and examples for each:

PropertyDescriptionExample
nameThe display name of the block, usually localized for different languages. Required.{ "en": "FAQ Item", "fi": "UKK-kohta" }
descriptionOptional description shown in the editor and used for discovery.{ "en": "A single question and answer pair." }
categoryThe category for the block, must match a category in settings_schema.json. Required."content"
tagThe HTML tag or identifier for the block."div"
classCSS classes applied to the block container."block block-faq-item"
templatesTemplate types the block is available on. Empty or absent = all templates.["product"]
privateIf true, hidden from the general block picker; only addable where whitelisted. Defaults to false.true
displayEditor rendering mode for the block wrapper. Currently only "inline" is supported."inline"
allowed_blocksOptional whitelist on a layout block schema for which child blocks may be added. Container-level allowed_blocks take precedence when adding into a specific container.["product-title", "product-price"]
settingsConfiguration options for the block, such as text fields, images, etc.{ /* ...block settings... */ }
containersDeclares this block as a layout block with child drop zones.See Layout blocks
containers[].idContainer id used by {% container 'id' %}. Required."details"
containers[].titleContainer label in the designer.{ "en": "Details" }
containers[].typeContainer type, e.g. "static" for a fixed container."static"
containers[].allowed_blocksWhitelist of blocks accepted by the container. Empty = all public blocks.["product-title", "product-price"]
containers[].presetBlocks seeded into this container when a statically imported block/section has no stored configuration. See Container preset blocks.{ "blocks": ["product-title"] }
presetsPredefined variants shown in the add-block picker. See Block presets.[{ "name": "Default", "default": true }]

Each property helps define how the block appears and behaves in the theme editor and on the storefront. For more details on settings, see the relevant documentation sections below.

See the full block schema definition  for the authoritative, machine-validated shape.

Template Scope (templates)

Both blocks and sections may declare a templates array in their {% schema %}. The block is then only offered on matching template types.

{% schema %} { "name": { "en": "Part-payment calculator" }, "category": "content", "templates": ["product"] } {% endschema %}
  • The template type is the part before the first dot. For example the templates product and product.custom both have type product.
  • Template names can also be nested paths, e.g. "marketing/cart" — the type-matching rule still applies to whatever precedes the first dot.
  • An empty or absent templates array means “available on all templates”.
  • Use this as your general placement context. It is coarse and predictable; prefer it over trying to encode fine-grained semantic context.
Use casetemplates
Product part-payment calculator["product"]
Cart-level calculator["cart"]
Generic marketing blockomit (available everywhere)

Section-level templates works the same way. See Sections.

Layout Blocks and Containers

A layout block is a block whose schema declares one or more containers. Each container is a drop zone that can hold child blocks. Nesting is limited to a single level — a child block cannot itself contain further blocks.

Parent (layout) block

blocks/product-card.liquid

<div class="product-card"> <div class="product-card__media"> {% container 'media' %} </div> <div class="product-card__details"> {% container 'details' %} </div> <div class="product-card__actions"> {% container 'actions' %} </div> </div> {% schema %} { "name": { "en": "Product card" }, "category": "content", "templates": ["product", "collection", "index"], "containers": [ { "id": "media", "title": { "en": "Media" }, "allowed_blocks": ["product-image", "product-badge"] }, { "id": "details", "title": { "en": "Details" }, "allowed_blocks": ["product-title", "product-price", "product-rating"] }, { "id": "actions", "title": { "en": "Actions" }, "allowed_blocks": ["buy-button", "wishlist-button"] } ] } {% endschema %}

Container fields

FieldTypeDescription
idstringContainer identifier, referenced by {% container 'id' %}. Required.
titlestring | localized objectLabel shown for the container in the designer.
typestringContainer type, e.g. "static" for a fixed container.
allowed_blocksstring[]Optional whitelist of block names addable into this container. Empty/absent = any public block.
presetobjectBlocks seeded into this container when a statically imported block has no stored configuration. See Container preset blocks below.

The {% container %} tag renders the matching child blocks. An unnamed {% container %} holds blocks that have no container assignment.

Container Preset Blocks

Each containers[].preset.blocks array is a fallback: it is only used when the block is statically imported (rendered via {% block %} in the theme layout) and has no stored configuration. Preset blocks render read-only in the designer until the merchant makes the first change, at which point they are persisted as real block instances.

{ "id": "actions", "title": { "en": "Actions" }, "preset": { "blocks": [ "buy-button", { "name": "wishlist-button", "settings": { "style": "outline" } } ] } }

This is the only place a bare block-type name string (shorthand for { "name": "..." }) is allowed. Settings not specified default to the block’s own schema defaults. A preset block that is itself a layout block can nest further preset blocks via its own blocks array.

Block Presets

The presets property defines ready-made variants shown in the add-block picker. Unlike container preset blocks, which are a read-only fallback for static imports, a block preset is applied eagerly — when a merchant picks it, its settings and nested child blocks are written into the block’s stored configuration immediately.

presets accepts a plain array or a locale-keyed map (same schemaPresetList shape as section presets):

{ "name": { "en": "Product card" }, "category": "content", "presets": [ { "name": { "en": "Default" }, "default": true, "blocks": [ { "name": "product-image", "container": "media" }, { "name": "product-title", "container": "details" }, { "name": "product-price", "container": "details" }, { "name": "buy-button", "container": "actions" } ] } ], "containers": [ { "id": "media", "title": { "en": "Media" } }, { "id": "details", "title": { "en": "Details" } }, { "id": "actions", "title": { "en": "Actions" } } ] }

For a simple (non-layout) block, presets typically set settings only:

{ "name": { "en": "Rich text" }, "category": "content", "presets": [ { "name": { "en": "Intro paragraph" }, "settings": { "body": "<p>Welcome to our store.</p>" } } ] }
FieldTypeDescription
idstringOptional stable identifier for this preset variant.
namestring | localized objectLabel shown for this preset in the add-block picker.
defaultbooleanIf true, this is the default preset offered.
settingsobjectSetting id → value pairs applied to the block on add.
blocksarrayNested child block instances for layout blocks. When present, container presets are not used — the preset’s blocks takes over entirely.

Each entry in a preset’s blocks array uses the same shape as section preset block instances: name, optional id, settings, container, and nested blocks.

For localized preset copy and locale-keyed preset maps, see Localization.

Private Blocks (private)

Set private: true on a block to remove it from the general block picker. A private block can only be added where it is explicitly listed in an allowed_blocks whitelist (a container, or a section).

This is exactly what you want for child blocks that only make sense inside one specific parent block.

blocks/product-price.liquid

<span class="price">{{ product.price | money }}</span> {% schema %} { "name": { "en": "Price" }, "category": "content", "private": true } {% endschema %}

Because product-price is private and listed in the details container’s allowed_blocks above, it:

  • never appears in the section-level “add block” picker,
  • only appears when adding into the details container of a product-card,
  • cannot be placed anywhere else — by construction, not by convention.

private is the right tool for “child-only” blocks. It differs from a section’s is_creatable: false (which means “cannot be added at all”); a private block can be added, but only where it is whitelisted.

allowed_blocks Whitelist

allowed_blocks is a list of block names that a target accepts. It can be declared in three places:

On a layout block schema

On a layout block, allowed_blocks whitelists which child blocks may be added into its containers. When a container also declares allowed_blocks, the container-level list takes precedence for that container.

On a layout block container

Restricts which blocks may be added into that container (see Layout blocks).

On a section schema

Restricts which blocks may be added at the section’s top level.

sections/main-product.liquid

<section class="main-product"> {% container %} </section> {% schema %} { "name": { "en": "Main product" }, "category": "content", "templates": ["product"], "allowed_blocks": ["product-title", "product-price", "product-card", "buy-button"] } {% endschema %}

Resolution rule

A block B may be added to a target T (a container or a section) when all apply:

  1. TemplateB.templates is empty or includes the current template type.
  2. Privacy — if T has no whitelist, B must not be private.
  3. Whitelist — if the target declares allowed_blocks, B must be in that list. For containers inside a layout block, the container’s list takes precedence over the layout block schema’s list. A whitelisted target shows exactly its listed blocks (private blocks included).

Empty/absent allowed_blocks means no restriction: all public, template-compatible blocks are accepted; private blocks are hidden.

Worked Examples

Product card with private children

product-card templates: ["product","collection","index"] containers: details -> allowed_blocks: ["product-title","product-price"] actions -> allowed_blocks: ["buy-button","wishlist-button"] product-title private: true product-price private: true buy-button (public — also usable elsewhere)

Result: product-title / product-price can only be dropped into the details container of a product card; they never show up anywhere else. buy-button is whitelisted for the actions container but, being public, can also be used in other unrestricted targets.

Cart-level calculator

cart-calculator templates: ["cart"] main-cart (section) allowed_blocks: ["cart-summary","cart-calculator","cart-note"]

Result: cart-calculator is only offered on cart templates, and only inside the main-cart section’s block list.

Third-Party App Blocks

App-provided blocks are merged into the same block registry and use the same schema format as theme blocks. That means an app block can set:

  • templates — to scope itself to product/cart/etc. pages,
  • private — if it is meant to be a child of an app-provided layout block,
  • container allowed_blocks — if the app ships a layout block of its own.

A theme can also constrain app blocks without knowing app internals, by listing (or omitting) them in its own section/container allowed_blocks. This gives themes final say over placement.

For integrations that must appear in an exact spot (e.g. a part-payment calculator pinned to the product card), prefer a static block slot in the theme markup:

{% block 'app/part-payment' id: 'product-part-payment' %}

A static block is owned by the theme layout: the merchant can configure it but cannot drag it elsewhere.

Enforcement

Placement rules are enforced in two places:

  • Designer UI — the block picker is filtered to blocks that pass the resolution rule for the target container/section.
  • Server — the add-block API re-validates templates, private, and allowed_blocks for the resolved target and rejects invalid placements. Do not rely on UI filtering alone; the server is the source of truth.

Current scope: validation runs on block add. Reordering or moving an existing block between containers is not yet re-validated server-side; treat cross-container moves with care until that is added.

Block Best Practices

  • Use clear and descriptive names and categories for your blocks.
  • Organize settings logically for ease of use.
  • Keep block logic simple and focused on a single purpose.
  • Use private and allowed_blocks to enforce intentional block placement instead of relying on merchant convention.
  • Test your blocks in different sections to ensure flexibility and compatibility.