App blocks

Template and hosted surface app blocks: modes, pages, the data each mode reads, the markup policy, tf-ab-* classes, caching, and a complete instalments example.

An app block is a piece of storefront markup your app ships in a release. Merchants add it in the theme editor to a section that accepts app blocks, on the pages the block lists, and set its settings there. A block runs in one of two modes: template blocks are rendered into the page itself, with no frame; hosted_surface blocks render their markup and then open one of your hosted surfaces in a frame for interactive UI.

Modes#

Mode

What the shopper sees

Data it can read

Use it for

template

Your Vascula markup, rendered by the storefront into the page. No frame, no script.

shop, routes, product, collection

Information next to the product or collection: instalment amounts, badges, delivery notes, size charts, links.

hosted_surface

Your Vascula markup, then the hosted surface named by surface_key in a frame directly after it.

shop, routes, product, collection, cart

Anything the shopper interacts with: forms, configurators, adding to the cart.

A template block is cached with the page and shown to every shopper, so it reads only public page data: never the cart or the customer. If what you show depends on the shopper, use a hosted surface.

A block keeps its mode for good. A release that gives an existing key the other mode is refused (block "…" is template in the current release and cannot become hosted_surface; declare the hosted_surface version under a new key), because merchants placed that block as it was. Declare a new key for the new mode. A key you remove from a release is retired and cannot be used again.

Declare a block#

Blocks are listed under storefront_blocks in tringify.app.json, or in the Developer Portal under the app's configuration. In tringify.app.json each block names its Vascula file in source_file; the CLI reads it in when you push. The Developer API carries the same file inline as source.

json
{
  "storefront_blocks": [
    {
      "key": "instalments",
      "name": "Instalments",
      "description": "Shows the price split into interest-free payments under the product price.",
      "mode": "template",
      "pages": [
        "product"
      ],
      "source_file": "blocks/instalments.vasc"
    },
    {
      "key": "size-guide",
      "name": "Size guide",
      "description": "Opens an interactive size finder.",
      "mode": "hosted_surface",
      "pages": [
        "product"
      ],
      "surface_key": "size-guide",
      "source_file": "blocks/size-guide.vasc"
    }
  ]
}

Field

Meaning

key

Lowercase letters and digits joined by - or _, up to 40 characters. Themes store placed blocks as app:<app_id>:<key>, so keep it stable across releases.

name

What merchants see in the theme editor. Up to 80 characters.

description

One sentence for merchants about what the block shows, up to 300 characters. The theme editor shows it when they add a block.

mode

template or hosted_surface. Required; there is no default.

pages

The page types the block may be placed on (below). At least one, each once.

surface_key

hosted_surface blocks only: the hosted surface the block opens, declared in the same release. A template block has none.

source, source_file

A complete Vascula block file: a {% schema %} header with "kind": "block", its settings and ctx_needs, then the markup. Up to 128 KB.

An app can declare up to 10 blocks. Every problem is reported with the block's key and what to change, both when you push the configuration and when you create a release.

Pages#

Page

Where the block can go

product

Product pages, in the default and alternate product templates.

collection

Collection pages, in the default and alternate collection templates.

cart

The cart page.

page

Content pages.

account

The customer account page.

blog

Blog posts, the blog index and blog categories.

home

The home page.

These seven are the only page types an app block can be placed on, and the list doesn't grow with your app. Search results, category, brand and vendor pages, the wishlist, sign-in pages and the 404 page don't take app blocks, and neither do the header and footer every page shares. For something that must show on every page, use an app embed.

A block that reads product must list exactly ["product"], and one that reads collection exactly ["collection"]: those pages are the only ones that have that data. The theme editor, the Store API (GET /storefronts/{storefront_id}/app-blocks) and the MCP tool list_app_blocks offer a block only on its pages, and any save that places it on another template is refused with the block's name. Other templates (search, sign-in, the header and footer sections shared by every page) take no app blocks.

Inside a page, a block can go only in a section whose blocks slot sets accept_app_blocks in its schema. Which sections those are is up to the theme; see Theme blocks.

Data each mode reads#

Declare the ctx roots the block uses in ctx_needs, as a theme section does; see the CTX reference. A block that declares a root its mode cannot read is refused when you push it.

Mode

ctx_needs may include

template

shop, routes, product, collection

hosted_surface

shop, routes, product, cart, collection

Settings work as in theme blocks: the merchant's values arrive as settings.<id>. Format prices with the money filter, and split a price into payments with money_split; never do arithmetic on money in the markup.

Markup#

App markup is placed in a merchant's page without a frame, so it is held to one allowlist, the same for app blocks, Vascula app embeds and surface posters. It allows reading markup and links only. The source is checked when you push it; each rendered block is checked again before it reaches the page, and a block whose output breaks the rules or is larger than 32 KB is left out of that page.

Allowed

Values

Elements

a, abbr, b, blockquote, br, caption, cite, code, dd, del, details, div, dl, dt, em, figcaption, figure, h2, h3, h4, h5, h6, hr, i, img, ins, kbd, li, mark, ol, p, pre, q, s, small, span, strong, sub, summary, sup, table, tbody, td, tfoot, th, thead, time, tr, u, ul

Attributes on every element

class, dir, lang, role, title, aria-*

Attributes on <a>

href, rel, target

Attributes on <blockquote>

cite

Attributes on <details>

open

Attributes on <img>

alt, decoding, height, loading, src, width

Attributes on <ol>

reversed, start

Attributes on <q>

cite

Attributes on <td>

colspan, rowspan

Attributes on <th>

colspan, rowspan, scope

Attributes on <time>

datetime

  • No <script>, <style>, <iframe>, forms, inputs, buttons, SVG or media players; no style, id, data-* or event-handler attributes; no HTML comments.

  • class lists only the tf-ab-* classes below, written literally (a template tag cannot build a class name).

  • href and cite are http: or https: addresses, mailto: or tel: links, or paths on the store (/…, #…, ?…); <img src> is an https:// address.

  • <a target> is _blank only; rel takes noopener, noreferrer, nofollow, sponsored and ugc.

  • width, height, colspan, rowspan and start are whole numbers; loading is lazy or eager; decoding is async, sync or auto; dir is ltr, rtl or auto.

  • Template tags ({{ }}, {% %}) stand in text and inside attribute values, never as an element or attribute name.

  • A block cannot render, include or section other templates.

Layout classes#

Style blocks with the platform's tf-ab-* classes. They use only currentColor, em sizes and the inherited font, so a block takes the font and colours of whatever theme it sits in. The storefront adds their stylesheet once on any page that shows a template block.

Class

Effect

tf-ab-row

Lays children out in a row, centred vertically, with a small gap; wraps on narrow screens.

tf-ab-stack

Stacks children vertically with a small gap.

tf-ab-wrap

Lets a row wrap.

tf-ab-center

Centres content and text.

tf-ab-end

Aligns content and text to the end.

tf-ab-between

Spreads a row's children to both ends.

tf-ab-box

Padding and a faint rounded border.

tf-ab-badge

A small rounded pill on a faint tint of the text colour.

tf-ab-muted

Softer text.

tf-ab-small

Smaller text.

tf-ab-large

Larger text.

tf-ab-strong

Semibold text.

tf-ab-link

An underlined link in the text colour.

tf-ab-icon

An image sized like an icon (1.25em).

tf-ab-thumb

A small rounded square thumbnail (2.5em).

tf-ab-full

Full width.

tf-ab-gap-s

A smaller gap in a row or stack.

tf-ab-gap-l

A larger gap in a row or stack.

vascula
<div class="tf-ab-box tf-ab-stack">
  <p class="tf-ab-row tf-ab-between">
    <span class="tf-ab-strong">Free delivery</span>
    <span class="tf-ab-badge">2–3 days</span>
  </p>
  <p class="tf-ab-small tf-ab-muted">Order before 2 pm to ship today.</p>
</div>

The storefront wraps every block in an element of its own, and headings and paragraphs inside a block have no outer margin, so space it with tf-ab-stack and tf-ab-gap-*.

Limits#

Limit

Value

Blocks per app

10

Block source (schema and markup)

128 KB

Rendered output of one block

32 KB

description

300 characters

name

80 characters

key

40 characters

Rendering is also bounded per block and per page. A block that runs over is left out of the page; the rest of the page renders as usual.

Caching#

Template blocks are part of the page: they are rendered with it and cached with it at the edge, so they cost nothing per visit. A page refreshes when the store's content changes (a product's price, for example) and when the merchant publishes theme changes, so a block reading product.price always shows the current price. A store gets new block markup when it installs or updates to the release that contains it.

If a block cannot be shown (the app was uninstalled, or the installed release no longer has the block), the page renders without it. A hosted_surface block's frame loads after the page, from your server.

Build and ship#

  1. Write the block file and declare it in storefront_blocks.

  2. tringify app block check runs the same checks as the platform: the declaration (mode, pages, surface), the data the block reads and its markup. Problems print as FILE:LINE: message.

  3. tringify app block context --page product prints the sample data each block receives on that page, cut to its ctx_needs.

  4. tringify app block preview --page product renders your blocks inside a theme, with the production rules, so you can see them as shoppers will.

  5. tringify app config push saves the configuration (it runs the check first). tringify app release submits a marketplace app's draft as a new version, or sends a private app's as an update that each store accepts in its admin; once a marketplace version is approved, tringify app publish VERSION lets stores install or update to it. The Developer Portal does the same.

  6. Install the app on a development store, open the theme editor on a product page, select a section that accepts app blocks, and choose your block under Add block; app blocks are listed with their app's name.

Example: pay in instalments#

This block shows "or 3 payments of …" under the product price. Its source is blocks/instalments.vasc; the merchant chooses the number of payments and whether to show a badge. money_split divides the current price into payments that add up to it exactly in the currency's minor units; the first payment carries any remainder, so it is the one to show.

json
{
  "storefront_blocks": [
    {
      "key": "instalments",
      "name": "Instalments",
      "description": "Shows the price split into interest-free payments under the product price.",
      "mode": "template",
      "pages": [
        "product"
      ],
      "source_file": "blocks/instalments.vasc"
    }
  ]
}
vascula
{% schema %}
{
  "name": "Instalments",
  "kind": "block",
  "ctx_needs": ["product"],
  "settings": [
    {
      "id": "payments",
      "type": "range",
      "min": 2,
      "max": 12,
      "step": 1,
      "default": 3,
      "editor": { "label": "Number of payments" }
    },
    {
      "id": "show_badge",
      "type": "checkbox",
      "default": true,
      "editor": { "label": "Show the interest-free badge" }
    }
  ]
}
{% endschema %}
{%- assign price = product.price.current -%}
{%- if price -%}
{%- assign parts = price | money_split: settings.payments -%}
<div class="tf-ab-row tf-ab-small">
  {%- if settings.show_badge -%}<span class="tf-ab-badge">0% interest</span>{%- endif -%}
  <span class="tf-ab-muted">or {{ settings.payments }} payments of</span>
  <strong class="tf-ab-strong">{{ parts.first | money }}</strong>
  <a class="tf-ab-link" href="https://pay-in-3.example/how-it-works" target="_blank" rel="noopener">How it works</a>
</div>
{%- endif -%}

Because it reads product, the block lists only product in pages. It renders into the page with the theme's font and colours, follows the selected variant's price when the product section re-renders, and is cached with the product page. tringify app init starts a new project with this block.