# 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: ```liquid {% 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: ```liquid {% block 'product-price', display: 'inline' %} ``` ```json { "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: ```liquid {% 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: | Property | Description | Example | |------------|-----------------------------------------------------------------------------|---------| | `name` | The display name of the block, usually localized for different languages. **Required.** | `{ "en": "FAQ Item", "fi": "UKK-kohta" }` | | `description` | Optional description shown in the editor and used for discovery. | `{ "en": "A single question and answer pair." }` | | `category` | The category for the block, must match a category in [`settings_schema.json`](/build-with-finqu/liquid-themes/settings#theme-settings-settings_schemajson). **Required.** | `"content"` | | `tag` | The HTML tag or identifier for the block. | `"div"` | | `class` | CSS classes applied to the block container. | `"block block-faq-item"` | | `templates` | Template types the block is available on. Empty or absent = all templates. | `["product"]` | | `private` | If `true`, hidden from the general block picker; only addable where whitelisted. Defaults to `false`. | `true` | | `display` | Editor rendering mode for the block wrapper. Currently only `"inline"` is supported. | `"inline"` | | `allowed_blocks` | Optional 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"]` | | `settings` | [Configuration options](/build-with-finqu/liquid-themes/settings) for the block, such as text fields, images, etc. | `{ /* ...block settings... */ }` | | `containers` | Declares this block as a layout block with child drop zones. | See [Layout blocks](#layout-blocks-and-containers) | | `containers[].id` | Container id used by `{% container 'id' %}`. **Required.** | `"details"` | | `containers[].title` | Container label in the designer. | `{ "en": "Details" }` | | `containers[].type` | Container type, e.g. `"static"` for a fixed container. | `"static"` | | `containers[].allowed_blocks` | Whitelist of blocks accepted by the container. Empty = all public blocks. | `["product-title", "product-price"]` | | `containers[].preset` | Blocks seeded into this container when a statically imported block/section has no stored configuration. See [Container preset blocks](#container-preset-blocks). | `{ "blocks": ["product-title"] }` | | `presets` | Predefined variants shown in the add-block picker. See [Block presets](#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. ## 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**. ```liquid {% 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 case | `templates` | |----------|-------------| | Product part-payment calculator | `["product"]` | | Cart-level calculator | `["cart"]` | | Generic marketing block | omit (available everywhere) | Section-level `templates` works the same way. See [Sections](/build-with-finqu/liquid-themes/sections#section-creatability-and-placement-flags). ## 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` ```liquid
{% container 'media' %}
{% container 'details' %}
{% container 'actions' %}
{% 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 | Field | Type | Description | |-------|------|-------------| | `id` | string | Container identifier, referenced by `{% container 'id' %}`. **Required.** | | `title` | string \| localized object | Label shown for the container in the designer. | | `type` | string | Container type, e.g. `"static"` for a fixed container. | | `allowed_blocks` | string[] | Optional whitelist of block names addable into this container. Empty/absent = any public block. | | `preset` | object | Blocks seeded into this container when a statically imported block has no stored configuration. See [Container preset blocks](#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. ```json { "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](#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](/build-with-finqu/liquid-themes/sections#section-presets)): ```json { "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: ```json { "name": { "en": "Rich text" }, "category": "content", "presets": [ { "name": { "en": "Intro paragraph" }, "settings": { "body": "

Welcome to our store.

" } } ] } ``` | Field | Type | Description | |-------|------|-------------| | `id` | string | Optional stable identifier for this preset variant. | | `name` | string \| localized object | Label shown for this preset in the add-block picker. | | `default` | boolean | If `true`, this is the default preset offered. | | `settings` | object | Setting id → value pairs applied to the block on add. | | `blocks` | array | Nested child block instances for layout blocks. When present, [container presets](#container-preset-blocks) 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](/build-with-finqu/liquid-themes/sections#preset-block-instances): `name`, optional `id`, `settings`, `container`, and nested `blocks`. For localized preset copy and locale-keyed preset maps, see [Localization](/build-with-finqu/liquid-themes/localization#presets). ## 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` ```liquid {{ product.price | money }} {% 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](#layout-blocks-and-containers)). ### On a section schema Restricts which blocks may be added at the section's top level. `sections/main-product.liquid` ```liquid
{% container %}
{% 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. **Template** — `B.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: ```liquid {% 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. > **Warning:** 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.