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 |
|---|---|---|---|
|
Your Vascula markup, rendered by the storefront into the page. No frame, no script. |
|
Information next to the product or collection: instalment amounts, badges, delivery notes, size charts, links. |
|
Your Vascula markup, then the hosted surface named by |
|
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.
{
"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 |
|---|---|
|
Lowercase letters and digits joined by |
|
What merchants see in the theme editor. Up to 80 characters. |
|
One sentence for merchants about what the block shows, up to 300 characters. The theme editor shows it when they add a block. |
|
|
|
The page types the block may be placed on (below). At least one, each once. |
|
|
|
A complete Vascula block file: a |
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 pages, in the default and alternate product templates. |
|
Collection pages, in the default and alternate collection templates. |
|
The cart page. |
|
Content pages. |
|
The customer account page. |
|
Blog posts, the blog index and blog categories. |
|
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 |
|
|---|---|
|
|
|
|
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 |
|
Attributes on every element |
|
Attributes on |
|
Attributes on |
|
Attributes on |
|
Attributes on |
|
Attributes on |
|
Attributes on |
|
Attributes on |
|
Attributes on |
|
Attributes on |
|
No
<script>,<style>,<iframe>, forms, inputs, buttons, SVG or media players; nostyle,id,data-*or event-handler attributes; no HTML comments.classlists only thetf-ab-*classes below, written literally (a template tag cannot build a class name).hrefandcitearehttp:orhttps:addresses,mailto:ortel:links, or paths on the store (/…,#…,?…);<img src>is anhttps://address.<a target>is_blankonly;reltakesnoopener,noreferrer,nofollow,sponsoredandugc.width,height,colspan,rowspanandstartare whole numbers;loadingislazyoreager;decodingisasync,syncorauto;dirisltr,rtlorauto.Template tags (
{{ }},{% %}) stand in text and inside attribute values, never as an element or attribute name.A block cannot
render,includeorsectionother 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 |
|---|---|
|
Lays children out in a row, centred vertically, with a small gap; wraps on narrow screens. |
|
Stacks children vertically with a small gap. |
|
Lets a row wrap. |
|
Centres content and text. |
|
Aligns content and text to the end. |
|
Spreads a row's children to both ends. |
|
Padding and a faint rounded border. |
|
A small rounded pill on a faint tint of the text colour. |
|
Softer text. |
|
Smaller text. |
|
Larger text. |
|
Semibold text. |
|
An underlined link in the text colour. |
|
An image sized like an icon (1.25em). |
|
A small rounded square thumbnail (2.5em). |
|
Full width. |
|
A smaller gap in a row or stack. |
|
A larger gap in a row or stack. |
<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 |
|
300 characters |
|
80 characters |
|
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#
Write the block file and declare it in
storefront_blocks.tringify app block checkruns the same checks as the platform: the declaration (mode, pages, surface), the data the block reads and its markup. Problems print asFILE:LINE: message.tringify app block context --page productprints the sample data each block receives on that page, cut to itsctx_needs.tringify app block preview --page productrenders your blocks inside a theme, with the production rules, so you can see them as shoppers will.tringify app config pushsaves the configuration (it runs the check first).tringify app releasesubmits 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 VERSIONlets stores install or update to it. The Developer Portal does the same.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.
{
"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"
}
]
}
{% 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.