Liquid
Themes are written in Liquid and rendered by Fluid on Abizvo's servers. Everything standard Liquid offers — assign, if, for, case, render, the string, number and array filters — works as in Fluid. This page covers what the platform adds and what it withholds.
The boundary
- Only dictionaries, lists and primitives reach a template. A theme never holds an entity, a service or a database; every object below is built by the platform for the render.
- Every
{{ output }}is HTML-encoded. The only unencoded markup is what the platform's own filters build (image_tag,icon_tag,video_tag,placeholder_svg_tag,stylesheet_tag,script_tag,json,faq_schema_tag,event_schema_tag,payment_type_svg_tag,editable,animation_attributes,effect_attributes,lightbox_attributes,compare_attributes) and the HTML arichtextsetting renders — both from values the platform checked. - Each section renders in isolation. A section whose Liquid throws leaves an error in the render report (
render.section-failed) and nothing on the public page; in the editor an error plate takes its place. The rest of the page stands. The same holds for a block (render.block-failed) and, at the top, for the layout (render.layout-failed, which falls back to a plain document). - A render is bounded: 1,000,000 Liquid steps and a recursion depth of 24 per page. A loop written to never end stops there instead of taking the host with it. A string may reach 4,194,304 characters, a list or a range 100,000 items, one section 8,388,608 characters of output and 10 seconds, and a page 30 seconds. Past any of them the section, block or embed that asked renders nothing and the rest of the page stands.
{% schema %},{% stylesheet %}and{% javascript %}never run. They are taken out of a section or block file when the theme loads (one of each per file; a second is an error) and their bodies used elsewhere: the schema for the editor and validator, the CSS and JavaScript bundled into one file each.
Tags
Beside Liquid's own, six tags exist only while the platform renders a theme page.
{% content_for 'blocks' %}
Renders the current section's — or, inside a block, the current block's — child blocks in their block_order. A section that accepts blocks writes this where they go:
<div class="hero__content">
{% content_for 'blocks' %}
</div>
{% content_for 'block', type: 'heading', id: 'title' %}
Renders one static block the markup places itself. The block is created from the type named if the template has none with that id; the owner can edit and hide it, not remove or move it.
{% sections 'header' %}
In a layout, renders a section group: header, footer, announcement, overlay or aside. A group the theme does not ship renders nothing.
{% form 'contact' %} … {% endform %}
Opens a form the platform routes to the business. The kind is one of contact, newsletter, lead, quote, booking; any other kind is a render error. The tag writes the <form method="post"> with its action (the site's own /sites/{slug}/forms/{sectionId[.blockId]}), a hidden _ab_form field and a honeypot named website, and closes it; the theme writes the fields between. An optional class: argument goes on the form element. A quote form reads, beside name, email, phone and message, each ticked service as offering (the service's id, one value per tick) with its quantity as qty_{id}, and preferred_date (yyyy-MM-dd) and preferred_window (morning, afternoon or evening); the request lands as a lead in the hosted request page's own shape.
{% form 'contact', class: 'contact-form' %}
{% if form.posted_successfully %}<p class="notice">{{ 'forms.thanks' | t }}</p>{% endif %}
{% for error in form.errors %}<p class="error">{{ error }}</p>{% endfor %}
<label>Name <input name="name" required></label>
<label>Email <input name="email" type="email" required></label>
<label>Message <textarea name="message" required></textarea></label>
<button type="submit">{{ 'forms.send' | t }}</button>
{% endform %}
Inside the block, form is an object: id, kind, posted_successfully, errors (a list of one sentence at most), inert. On the editor canvas and the owner's preview the form is inert (action="#", data-ab-inert): nothing a click does there reaches the business.
{% style %} … {% endstyle %}
A <style data-ab-style> element the editor can refresh in place. The body is rendered unencoded (a font stack keeps its quotes) and then every < is written as its CSS escape, so nothing inside can close the element whatever a value held. This is where a layout turns global settings into custom properties:
{% style %}
:root {
--page-width: {{ settings.page_width }}px;
--heading-weight: {{ settings.heading_weight }};
}
{% endstyle %}
The {% style %} block goes after theme.css when both declare the same custom properties on :root. Both are declarations at equal specificity and the later one wins, so a {% style %} emitted before the stylesheet has every owner setting silently overruled by the stylesheet's defaults — the measure simply stops moving. Found building Quire; QuireThemeTests.The_owners_settings_are_not_overruled_by_the_stylesheets_defaults holds it.
{% paginate collection.products by 24 %} … {% endpaginate %}
Cuts one page out of a listing — collection.products on a collection page, search.results on the search page — and describes the pages as paginate, in Shopify's shape: current_page, current_offset, items (every product that passes the visitor's filters), page_size, pages, page_param (page), and previous, next (nil at either end) and parts, each title, url and is_link (the current page is a part that is not a link; a gap is …). The page asked for is ?page=; the size is the tag's own (1 to 100), usually a section setting. window_size: (1–5, default 2) says how many pages either side of the current one parts names. Outside the block, collection.products is the first 48. Anything else a theme paginates is drawn whole, as one page.
{%- paginate collection.products by section.settings.products_per_page -%}
{%- for item in collection.products -%}{% render 'product-card', item: item %}{%- endfor -%}
{%- if paginate.pages > 1 -%}<nav class="pagination" aria-label="Pages">{{ paginate | default_pagination }}</nav>{%- endif -%}
{%- endpaginate -%}
Every address paginate builds keeps the visitor's order and filters, and a page past the last is an empty page with a link back.
Filters
The filters the platform registers beside Liquid's own. Every one that returns markup builds it from validated values and encodes what it takes from them.
| Filter | Meaning | Example |
|---|---|---|
asset_url |
The content-versioned address of a file in assets/. A name the theme does not have is reported and renders as nothing. |
{{ 'theme.css' \| asset_url }} |
image_url |
A library picture's copy nearest the width asked for: sm up to 480, md up to 1024, lg beyond (default 1600). A plain string is returned only if it is a safe address. |
{{ settings.favicon \| image_url: width: 64 }} |
image_tag |
A complete <img>: src, a srcset across the library's sizes (480w, 1024w, 1920w) with sizes (default 100vw), intrinsic width and height, the owner's alt (empty for a decorative picture, or the alt: argument), loading="lazy" unless loading: 'eager', decoding="async", fetchpriority: 'high' when asked, a class, and the focal point as object-position. |
{{ section.settings.image \| image_tag: sizes: '100vw', loading: 'eager', class: 'hero__img' }} |
stylesheet_tag |
<link rel="stylesheet"> for a safe address; nothing otherwise. |
{{ 'theme.css' \| asset_url \| stylesheet_tag }} |
script_tag |
A deferred <script src> — only where the theme's scripts may run (the site's own host, a trusted theme); elsewhere nothing, which is why a theme's pages must work without their script. module: true writes type="module". |
{{ 'theme.js' \| asset_url \| script_tag }} |
t |
Translation: the site's language, then its base language, then the theme's default locale. {{ name }} placeholders take named arguments. A plural key (one / other) is chosen by count. A missing key renders the key and records a warning. |
{{ 'gallery.photos' \| t: count: gallery.photos_count }} |
animation_attributes |
The data-ab-anim… attributes and timing custom properties the motion runtime reads, from an animation setting; nothing for none. Marks the page as using motion. |
<div {{ section.settings.entrance \| animation_attributes }}> |
effect_attributes |
data-ab-fx and data-ab-fx-k from an effect setting — never an inline style. Marks the page as using effects. |
<article {{ block.settings.effect \| effect_attributes }}> |
lightbox_attributes |
data-lightbox="{group}" on a container of pictures: the platform's lightbox opens any picture inside it, with previous and next across the group. Marks the page, so the lightbox script and its styles are sent to it and to no other (never on the editor canvas). |
<div {{ 'album' \| lightbox_attributes }}> |
compare_attributes |
data-compare="true" on an rn-compare pair (rn-compare-before, rn-compare-after): the platform's before/after slider turns it into one frame with a divider. Marks the page, so the slider and its styles are sent to it and to no other. |
<div class="rn-compare" {{ block.id \| compare_attributes }}> |
editable |
In the editor, wraps text so a click edits it in place and the change lands on that setting; everywhere else, the text exactly as {{ }} would write it. Takes the owner (section or block) and the setting id. |
{{ block.settings.text \| editable: block, 'text' }} |
placeholder_svg_tag |
A neutral placeholder SVG where no picture is chosen — image (400×300), portrait (300×400), landscape (480×270) or square (400×400); an optional class argument. Plainly a placeholder, never a stock photo. |
{{ 'landscape' \| placeholder_svg_tag }} |
json |
JSON safe inside <script type="application/json"> — <, >, & and the line separators written as escapes. The one way to hand data to a script. |
<script type="application/json" id="hero-data">{{ section.settings \| json }}</script> |
faq_schema_tag |
The page's one FAQPage JSON-LD <script>, built by the platform from the published FAQ (answers as plain words). Written the first time a render asks and empty every time after, and empty when no published question has an answer. |
{{ faq \| faq_schema_tag }} |
event_schema_tag |
One event's Event JSON-LD <script> (name, start and end with the business's offset, attendance mode, place or online address, organizer), built by the platform from the events global — so only for a published, upcoming event. Written the first time a render asks for that event and empty after, and empty on the editor canvas. |
{{ ev \| event_schema_tag }} |
handleize / handle |
Lowercase, accents folded, anything that is not a letter or digit collapsed to one hyphen. | {{ block.settings.title \| handleize }} |
color_to_rgb |
#1a2b3c as 26, 43, 60, for rgba(var(--x), .5) patterns. |
--accent-rgb: {{ settings.accent \| color_to_rgb }} |
font_family |
A font_picker value's stack (the platform's first font when nil). |
font-family: {{ settings.heading_font \| font_family }} |
link_url |
A stored link (page:{id}, action:booking, an address) resolved to an address. |
<a href="{{ block.settings.link \| link_url }}"> |
payment_type_svg_tag |
A payment type from shop.payment_types as a badge the platform draws — its own words in a rounded box, named for screen readers (role="img", aria-label), never a card brand's artwork; an unknown type draws nothing. A class argument, ab-payment-icon by default. |
{% for type in shop.payment_types %}{{ type \| payment_type_svg_tag: class: 'footer__payment' }}{% endfor %} |
icon_tag |
One of the platform's icons as inline SVG (24×24, currentColor stroke, aria-hidden); a class argument, ab-icon by default. |
{{ 'phone' \| icon_tag: class: 'contact__icon' }} |
video_tag |
A lazy, titled <iframe> for a YouTube (no-cookie) or Vimeo video_url value or a product's external video, the platform's own <video controls> for a product's library clip (COM-019); nothing for anything else. |
{{ section.settings.video \| video_tag: title: section.settings.heading }} |
variant_picker_styles |
The stylesheet that makes a variant picker work with no script (Abizvo Commerce): inside the scope — one #id or .class — each element marked data-variant="{{ variant.id }}" shows only while the radios named option1…option3 that are checked are that variant's values, and data-variant="unavailable" shows while they name no variant. Without :has() only the element marked data-variant-current shows (put it on the selected variant's). A complete <style>, at most one rule pair per variant (100), every value written as a CSS escape. Nothing for a product without variants or a scope that is not one id or class. |
{{ product \| variant_picker_styles: '#product-main' }} |
default_pagination |
Shopify's pagination links from a paginate object: <span class="prev">, a <span class="page"> per page (page current with aria-current for this one, deco for a gap) and <span class="next">, every address encoded; nothing for one page. previous: and next: replace the two labels. |
{{ paginate \| default_pagination: next: 'More' }} |
app_asset_url |
Inside an app's block or embed only: the content-versioned address of a file under the app's own assets/. Outside an app, and for a file the app does not ship, reported and nothing. |
{{ 'calendar.css' \| app_asset_url }} |
Objects and globals
On every page
| Global | Shape |
|---|---|
settings |
Every global setting by id, in its Liquid shape (see Settings) |
page |
id, title, handle, url, canonical_url (the absolute address), is_home, template, seo_title, seo_description, current |
site |
name, language, url (the home page), pages (a list of page objects) |
localization |
The languages the page is offered in (COM-045): language — the one drawn — and available_languages, each iso_code (fr-CA), code (fr, the address prefix), name and endonym_name (the language's own name), url (this page in that language), current and primary; and multiple (more than one). A language switch is a plain list of links, no form and no script: {% for l in localization.available_languages %}<a href="{{ l.url }}" hreflang="{{ l.iso_code }}">{{ l.name }}</a>{% endfor %}. An owner offers other languages on the site's page and writes their words under Website → Translations; each is served under its short code (/fr/…), with the theme's own locales/fr.json answering t, the owner's translated text settings and page titles in place of the originals, lang on the page, and hreflang links between the versions written by the platform. On a one-language site it holds that one language. Currency is not yet translated |
business |
name, phone, phone_url, email, email_url, address_lines (empty unless the owner chose to publish the address), booking_url, quote_url, logo_url, instagram_url, facebook_url, tiktok_url |
linklists |
Every menu the owner built under Website → Menus, by handle (linklists['footer-legal']), plus main and footer — which are the pages marked for navigation, in the site's order, until the owner builds them. A menu is handle, title, levels and links; a link is title, url, type (frontpage_link, page_link, collection_link, blog_link, product_link, policy_link, http_link), handle, current (it is this page), active (it or a link below it is), child_current, child_active, levels and its own links — three levels deep at most, so a mega menu is link.links[].links[]. A link to something the site no longer has (a deleted page, a policy never written) is left out. A link_list setting holds a handle: linklists[section.settings.menu]. Read live, never frozen |
shop |
The store (COM-026): name, url, email, phone, policies (each written policy: handle, title, body as HTML from the owner's rich text, url — the page at /policies/{handle}), Shopify's own names for each (refund_policy, privacy_policy, terms_of_service, shipping_policy, contact_information; nil when not written) password_message (the owner's words on the storefront password page, COM-032; nil elsewhere) and payment_types — the ways the site's checkout takes payment (visa, master, american_express, apple_pay, google_pay), empty on a site that does not sell or cannot take a card, for payment_type_svg_tag. Read live, never frozen |
policies |
The same list as shop.policies |
collections |
The site's own content types by handle: collections[handle] is handle, title, name, entries (published entries), entries_count, url |
blogs |
The site's blogs by handle (COM-029) — every collection with the Blog preset's shape (a date field and a formatted body; Start a blog makes one): blogs[handle] is handle, title, url (its list, /{handle}), articles (every published post, newest first), articles_count, all_tags, tags (title, handle, url, count), comments_enabled (false) and pagination |
albums |
The site's galleries: id, name, description, category, date (yyyy-MM-dd), url, cover_url, cover_small_url, cover_focal, photos_count, videos_count, locked |
forms |
In a theme's own sections, and in an app that asks for the forms permission (the Forms app): the business's published forms, read live — id, key, title, intro, submit_label, thank_you, url (the hosted page), action (nil on the canvas and the preview), inert, source, honeypot, and fields (id, name, label, kind, required, options) |
services |
The published services from the business's catalogue: id (the offering's own id — what a quote form posts for a ticked service, as offering, with its quantity as qty_{id}), name, description, category, price (already worded, in the right currency) |
products |
The products marked for the website: name, description (the plain words), category, price, taxable, bookable, booking_url, url (the product's own page, /products/{handle}), and the product data a card or a page reads beside them — handle, sku, vendor (the brand or designer; nil when none), vendor_url (/collections/vendors?q=…, the brand's own page), tags (a list), type and kind (physical, service or digital), created_at, published_at (its first day on the website — a date for the date filter), new (true for its first 30 days on the website), seo_title (the owner's search title, or the name), description_html (the owner's formatted description rendered by the platform, or the plain words as paragraphs — the one key a page needs) and collections (the published product collections it is in, each title, handle, url), image (the product's first picture, shaped for image_tag / image_url; nil when it has none — {% if item.image %} is the one test) and images (all of them, in the owner's order, empty when none), and for a product whose stock is counted availability (in_stock, low or sold_out — never a count) and in_stock; nil for both otherwise. On a site that sells (Abizvo Commerce), and only there, each product also carries product_id and — unless it is sold out — add_to_cart_url: a plain <form method="post" action="{{ item.add_to_cart_url }}"> with a hidden product field set to product_id adds it to the visitor's cart. Otherwise never an id. Options and variants (Shopify's shape): has_only_default_variant (true for a product with no options — it adds as above, and everything below is empty or nil), options (each name, position 1–3, style — buttons, swatches or dropdown, the owner's hint, swatches for a "Color"/"Colour" option — selected_value, and values, each value, swatch (a hex or nil), swatch_image (a picture or nil), available (some variant with it is for sale) and selected), variants (each id, title — "Medium / Sand" — option1…option3, options (the values in order), sku, price and compare_at_price (worded; the latter nil unless above the price), available, availability, image (the variant's own picture or nil), url (/products/{handle}?variant={id}) and selected), selected_variant (the one ?variant= named on the product's own page, else nil), selected_or_first_available_variant, first_available_variant, and price_min, price_max (worded) and price_varies. A product with variants needs one in the add form: a hidden variant set to an id, or radios (or a select) named option1…option3 whose values are the option values — the server resolves them and refuses a missing option ("Choose a size.") or a combination that does not exist, in words, on the cart page. A product's availability is its best variant's. Price model (COM-016): compare_at_price (the worded "was" price, nil unless it is above what the product sells for — a product's own compare-at price, the usual price while a sale runs, or for a product with variants its cheapest variant's), on_sale, saving (worded in the site's language: "Save 20%", the percentage rounded down) with saving_percent and saving_amount (worded), unit_price and unit_price_measure ("$4.20" and "100 ml" — print them as {{ item.unit_price }} / {{ item.unit_price_measure }}; nil without a measure), currency, and the numbers behind the words in minor units — price_value, compare_at_price_value, saving_value, price_min_value, price_max_value — for sorting and comparing only: a theme prints the worded keys and never calculates a price. price is what the product sells for now: a sale price while the owner's sale window is open. Each variant carries on_sale, saving, saving_percent, saving_amount, price_value, compare_at_price_value, unit_price and unit_price_measure of its own. Exact stock and pickup (COM-027): by default a page sees only the word (availability); where the owner opted into exact counts on the site (Show exact counts below on the site page), a product, each variant and each cart item whose stock is counted carry inventory_quantity — only while it is below the owner's number ("Only 3 left"), nil otherwise — and max_quantity for a quantity stepper (the count below the number, 99 above it; nil without the opt-in). On a product's own page, where the store offers pickup and the business marked locations for it, pickup_availability (also pickup) lists each location's name, available (it has the product — or, on each variant's own pickup_availability, that variant — on its shelf; always true for a product whose stock is not counted), ready_in (the owner's words, "Usually ready in 2 hours") and, under the opt-in, quantity; the product's list is the shown variant's. Empty elsewhere Subscribe and save (COM-042): selling_plans (each id, name, cadence — "Every month" — discount_percent and price on the plan, worded); a product form posts selling_plan set to a plan's id (blank buys once), and a cart line on a plan carries selling_plan (its words). A cart holding a plan checks out alone, by card, on one schedule. |
product_collections |
The business's published product collections, in the owner's order (made under Inventory → Collections) — the name is not collections, which is the site's content types. Each is title, handle, url (/collections/{handle}), description and description_html (the owner's rich text, already HTML), image (the collection's own picture, nil without one), featured_image (that or its first product's), products_count, all_products_count, sort_by / default_sort_by (manual, title-ascending, title-descending, price-ascending, price-descending, created-descending, created-ascending), seo_title and products — entries shaped exactly like products, in the collection's order, at most 48 (products_count always says how many there are). A manual collection lists the products the owner chose; an automatic one every website product its rules match (category, brand, tag, kind, name, price, stock), read again on every page view. Only products marked for the website are ever listed. Handed to an app only with the products permission |
bundles |
The business's published bundles (configurable products — a ring builder, a gift box, a build-your-own set — made under Inventory → Bundles), by name. Each is name (also title), handle, url (/bundles/{handle}, the platform's own step-by-step page inside the theme's chrome) and description. Empty on a site that does not sell, and read only when the theme names bundles. A bundle ends in the cart as one line per chosen product, so a theme's cart shows its parts; every part carries a visible Bundle property and a hidden _bundle one tying them together. Handed to an app only with the products permission |
recently_viewed_products |
The products this visitor opened on the site, newest first, as product entries — the one on the page left out, a product off the website left out, at most 12. Kept by the platform in the visitor's own cookie (no script, no account) and read only by a theme whose Liquid names it; empty on the canvas, the preview and a static copy. Shopify themes do this with local storage; here it needs nothing |
wishlist |
The visitor's saved products: count, empty, products (product entries), handles (for a heart: {% if wishlist.handles contains product.handle %}) and url — a plain <form method="post" action="{{ wishlist.url }}"> with product (the handle), wishlist_action (add, remove or toggle) and return_to=back. Kept in the visitor's own cookie, on any theme site with products whether or not it sells; nil on the canvas, the preview and a static copy |
cart |
Abizvo Commerce, the visitor's own cart: enabled (false, with nothing in it, on a site that does not sell — {% if cart.enabled %} is the one test a theme needs), can_checkout, count, empty, items (each product_id, name — "Linen shirt — Medium / Sand" for a variant — quantity, price, line_total, availability, url — the product's own page — image, the variant's own picture or the product's first, shaped for image_tag, nil without one, and line_id, variant_id, variant_title, sku and options (each name and value), the last four nil or empty for a product without variants), subtotal, tax, total and taxable as worded labels, with subtotal_value, tax_value and total_value (minor units, for a progress bar or a comparison — never printed) and currency; each item also carries price_value, line_total_value, compare_at_price (worded, while the line is on sale) and on_sale, and properties (COM-024: the shopper's details for the line, each name, value and hidden — true for a theme's own property whose name begins with _, which a cart should not print), problem (why the last change or checkout was refused, in the visitor's words — on the cart page only), and the addresses url (the cart page), add_url, update_url (a line field set to the item's line_id — or, for a product without variants, a product field — and a quantity, or remove; a quantity of 0 removes), checkout_url (where the platform's checkout form posts) and checkout_page_url (the platform's checkout step, for a theme's own cart page to link to). A form posting to add_url or update_url with return_to=back returns the visitor to the page it was posted from on the same host, with return_anchor (letters, digits, hyphens) as the fragment — how a cart drawer opened by :target stays open after adding, with no script. A refused change always goes to the cart page, which says why. Cart extras (COM-024): note (the shopper's note to the business, at most note_max characters, carried to the order's notes) — post it as note to update_url, alone or with a line change; discount_url (post code to apply a discount code in the cart, or remove to take it off — checked by the same rules as the checkout, refused in words on the cart page), discount_code (the code applied), discount_problem (why it takes nothing off now — under its minimum, say), discounts (each code, title, amount worded and amount_value) and discount_total / discount_total_value — already off total, with the tax figured after it; free_shipping (nil unless delivery is free above a threshold: threshold, remaining worded, threshold_value, remaining_value, qualified, percent 0–100 for a progress bar), also as free_shipping_threshold_value and free_shipping_remaining_value; gift_wrap (nil unless the business chose a gift-wrap product: product_id for a form posting to add_url, name, price, price_value, in_cart and line_id to remove it through update_url). Points (COM-040): loyalty_url (a signed-in shopper posts rewards — 0 takes them off — to spend loyalty points, capped by their balance and the goods), loyalty_rewards, loyalty_points and loyalty_amount (worded) for what the cart is spending now — already off total. Gift cards (COM-038): gift_card_url (post code to enter a gift card, remove to take it off), gift_card (nil, or last4, amount it pays and left_after on it, worded), gift_card_problem, and amount_due — what is left to pay after the card, which is a payment: total stands. A product of kind gift card is never taxed; its properties[Recipient email], properties[Recipient name], properties[From] and properties[Message] say who the code is e-mailed to. Line properties: a product form may carry up to five properties[Name] fields (an engraving, a gift message; at most 250 characters each, blank ones dropped) — they ride on the line, a line with different properties is a separate line, and the visible ones become part of the line's name on the order, the invoice, the packing slip and Stripe ("Ring · Engraving: J & M"). Several products at once: add_url also takes items[0][product], items[0][variant] (or items[0][option1]…), items[0][quantity] and items[0][properties][Name] for up to 20 items — all or nothing, the first refusal said in words. Buy now: return_to=checkout on an add (a second submit button named return_to with that value) goes straight to the checkout step. routes.cart_url is the cart page too, nil where the site does not sell. Read for the visitor's cookie on a public page only; the canvas and the preview see it disabled. Handed to an app only with the products permission |
customer |
Customer accounts (Abizvo Commerce): enabled (false, with nothing in it, on a site that does not sell — {% if customer.enabled %} is the one test a theme needs), signed_in, and while the visitor is signed in first_name (nil when they have given none — never the e-mail), name, email and orders_count; and the addresses account_url (the account; a visitor who is not signed in is sent from there to sign in, so a header's account icon can always link here), login_url and logout_url (signing out is a POST: <form method="post" action="{{ customer.logout_url }}"><button>Sign out</button></form>). routes.account_url is the account too, nil where the site does not sell. Read for the visitor's own cookie on a public page only: the canvas, the preview and a static copy see it disabled, it is never frozen into a publication, and a page a signed-in visitor sees is never cached. Never handed to an app Loyalty (COM-040): loyalty is nil unless the visitor is signed in and the business's programme is on; otherwise points, rewards_available, reward_points and reward_value (worded), spend_this_year (worded, before tax), tier and tier_perk (nil below the first tier), next_tier, to_next_tier (worded) and progress (0–100), and tiers (each name, from, perk, current). |
booking |
enabled (false with an empty list when booking is off — {% if booking.enabled %} is the one test a theme needs), time_zone, services_count, window, earlier_url, later_url, days_count, and services: each name, description, category, price, duration_minutes, url, handle, next_times and days (date, weekday, day, month, today, open, times_count, times); a time is label, time, url, starts_at |
reviews |
The business's published customer reviews: count, average (worded, one decimal — "4.7"; empty with none), average_value (the number, for structured data), rounded (whole stars), and items (featured first, then newest, at most 24): each rating (1–5), body, name (what the customer signed with), date (yyyy-MM-dd), date_label (worded in the site's language), response (the business's public reply), featured. Nothing unpublished, and nothing a customer did not write for the public — no e-mail, no invoice |
payments |
Only in an app that asks for the payments permission (the Payments app): ready (the business can take card payments right now — its plan and its Stripe account), action (where a pay form posts; nil on the editor canvas and the preview), inert (true where it is nil), honeypot (the platform's hidden field, as markup), and on the canvas only owner_note / settings_url (why it is not ready, and where to fix it). Never an invoice, a customer or an amount. |
newsletter |
Only in an app that asks for the newsletter permission (the Newsletter app): entitled (the plan includes email marketing), ready (entitled and at least one signup form is switched on), source, honeypot, and forms — each live signup form's id, title, description, button_label, success_message, the collect_* switches, require_consent, consent_text, double_opt_in, url, action (nil on the canvas and the preview) and inert. Never a contact, an audience or a count. Read live, never frozen |
faq |
The business's published FAQ — filled only on a site whose installed app asked for the faq permission, empty otherwise: enabled, items_count, categories_count, items (each id, handle, question, answer as HTML, answer_text, category, category_handle) and categories (each name, handle, items_count, items). Read live, never frozen |
portal |
Only in an app that asks for the portal permission (the Customer Portal app): action (where the Get my account link form posts; nil on the editor canvas and the preview), inert (true where it is nil), honeypot (the platform's hidden field, as markup). Never a customer, an address or a link. |
file_requests |
Only in an app that asks for the files permission (the File Requests app): count, honeypot, and items — each open file request the business marked public on its website, not yet full: id, title, instructions, remaining, max_mb, accept, kinds, action (nil on the editor canvas and the preview) and inert. Never a link token, a customer or a received file. Read live, never frozen |
careers |
Only in an app that asks for the careers permission (the Careers app): count, board_url (the business's page on the Abizvo careers board, or nil when none of its jobs is listed there) and jobs — each open posting, newest first: id, title, category, location, employment_type, workplace, pay (nil when the posting states none), summary, posted_at, posted_label, closes_on, url, apply_url (absolute addresses on the careers board) and kind (job or project). Never an applicant, an application or a resume. Read live, never frozen |
courses |
Only in an app that asks for the courses permission (the Education app): count, enroll_url (the business's registration page, /enroll/{slug} on the platform, or nil when the business has no Education) and items — each published course the business lists on its website: id, name, subject, level, description (HTML from the platform's Markdown, HTML disabled), price_label (worded and in the business's currency, e.g. $180.00 per month), open_classes and enroll_url. Never a student, a guardian or a class roster. Read live, never frozen |
social |
In a theme's own sections, and in an app that asks for the social permission (the Social Links app): count and profiles — each social profile the business filled in under Settings → Social profiles, in the platform's order: network (the key, e.g. facebook), label, url (an https address on that network), icon (the network's mark as inline SVG in currentColor, nil where the platform has none), has_icon and mark (the letter badge). Read live, never frozen |
loyalty |
Only in an app that asks for the loyalty permission (the Loyalty app): enabled, points_per_dollar, redemption_points and redemption_value (worded, "C$5.00") — the loyalty programme's public rules while it is on; enabled is false and the rest empty while it is off. Never a customer, a balance or a count. Read live, never frozen |
pet_care |
Only in an app that asks for the pets permission (the Pet Care app): count, request_url (the business's own request-a-stay page, through the site's link policy) and services — each service open to requests, with name and kind. Never an owner, a pet or a stay. Read live, never frozen |
team |
Only in an app that asks for the team permission (the Team app): the people the business switched on under HR → Employees → On the website — enabled, count, members in the owner's order, each with name, title, bio (the plain-text introduction as encoded paragraphs), bio_text, initials, order and photo (nil without one; else src, src_sm/src_md/src_lg, srcset, width, height, alt — hand it to image_tag). Never an id, an e-mail, a phone, pay or a role. Read live, never frozen |
events |
Only in an app that asks for the events permission (the Events app): the business's published events that have not ended, soonest first — enabled, count, time_zone, honeypot (the platform's hidden field, as markup) and items, each with id, handle, title, description (HTML from the rich-text markup), description_text, starts_at/ends_at (ISO with the business's offset), date, date_label, end_date_label, time_label, multi_day, day, month, weekday (worded in the site's language, in the business's zone), is_online, location, online_url, capacity, seats_left (nil with no limit), full, registration_open, register_action (where the form posts; nil on the canvas, the preview and when registration is not open), inert, book_url (into /book/{slug}/{service} for an event booked through a service), calendar_url and image. Never who registered. Read live, never frozen |
locations |
In a theme's own sections (read only for a theme whose Liquid names locations), and in an app that asks for the locations permission (the Locations app): the locations the business marked for its website, primary first — enabled, count, maps_count, items (each id, handle, name, address_lines, address, city, province, place, phone, phone_href, service_area, primary, has_map, map_url, directions_url, osm_url, hours_count, hours with day/opens/closes). The street and the map addresses are empty unless the business publishes its address. Read live, never frozen |
announcements |
Only in an app that asks for the announcements permission (the Announcements app): the announcement showing on this page right now — enabled and current (nil when none is showing), with text (plain words), link_label, link_url (through the link policy; nil when there is none), tone (info, warning or promo — a name, never a colour: the app draws it with the theme's scheme tokens), placement (bar or popup) and dismissible. Chosen at each request from the business's switched-on announcements for this site or every site whose window holds the moment, so one scheduled for tomorrow appears tomorrow with no publish. While one shows, the theme's own announcement-bar section renders nothing. Read live, never frozen |
request |
design_mode (the editor canvas), preview_mode, page_type, locale, scripts (whether the theme's script runs here), form_sent / form_error / form_error_reason (what a form post came back with, keyed by the form's own key — for a block that draws its own form) |
template |
name (the type), key, suffix |
routes |
root_url, cart_url and account_url (both nil where the site does not sell), chat_url (nil where the chat is off), collections_url (/collections) and all_products_collection_url (/collections/all), both nil on the editor canvas, and search_url (/search) and predictive_search_url (/search/suggest), nil where the theme has no search template and on the canvas |
content_for_header, content_for_layout |
In a layout only: the head the platform writes, and the page's sections inside <main id="MainContent"> |
services, products, product_collections, booking and reviews are read live on every page view and never frozen into a publication: a price change, a new service or a time that has since gone reaches a published page without republishing. A theme prints a price; it never calculates one. A bookable thing carries a url into the platform's own booking flow.
On the cart page
A theme that ships templates/cart.json draws the cart page (/cart) itself from the cart global, as one that ships product draws product pages; one that does not gets the platform's cart page inside the theme's chrome. Paying is the platform's own checkout step either way — contact, pickup or delivery, a discount code and the button to Stripe — at cart.checkout_page_url, and the thank-you after paying is the platform's too. The cart page is never cached and never indexed.
Customer accounts
On a site that sells, a shopper can have an account with the business, as on a Shopify store. They sign in with their e-mail and a six-digit code sent to it — there is no password — and the first sign-in makes the account. The pages are the platform's own, inside the theme's chrome, at /account/login, /account (their orders, newest first, with each order's stage and tracking), /account/orders/{number} (the order's items, totals, pickup or delivery and its parcels), /account/profile and /account/addresses; a signed-in shopper's checkout is filled in from the account and offers their saved addresses. A theme draws only the way in: an account link in the header, to routes.account_url or customer.account_url, and customer.first_name if it greets. A header link that should read differently when signed in tests customer.signed_in:
{%- if routes.account_url -%}
<a class="header__account" href="{{ routes.account_url }}">
{{- 'user' | icon_tag -}}
<span class="visually-hidden">{% if customer.signed_in %}Account{% else %}Sign in{% endif %}</span>
</a>
{%- endif -%}
The account pages are never cached and never indexed.
A theme may draw the account pages itself (COM-030) by shipping Shopify's templates — templates/customers/login.json, customers/account.json, customers/order.json and customers/addresses.json (stored as customers.login and so on; template.name is customers, template.suffix the page). Without one, that page stays the platform's. Sign-in stays passwordless, so there is no register, activate or reset template; the profile page stays the platform's. On those pages:
| Global | Holds |
|---|---|
account |
page (login, account, order, addresses), step on the login page (email, then code), pending_email (where the code went), problem (why the last post was refused, in the visitor's words), saved, next, and the addresses account_url, login_url, verify_url, resend_url, logout_url, profile_url, addresses_url |
customer |
Signed in, beside the fields above: last_name, phone, orders (newest first: name, number, url, created_at, date_label, stage, stage_label, total, items, carrier, tracking_number, tracking_url), addresses (id, label, name, address1, address2, city, province, zip, country_code, phone, default, one_line, lines, url), addresses_count and default_address — on the account pages only, never elsewhere |
order |
On the order page: the order's summary fields plus line_items (title, quantity, price, line_total, image), subtotal, tax (nil when none), total, fulfilment (delivery, pickup or none), shipping_address (lines) and shipments (date_label, carrier, tracking_number, tracking_url) |
Every form posts to the platform's own account endpoints through a form kind — the theme writes no address:
| Form | Posts | Fields |
|---|---|---|
{% form 'customer_login' %} |
the e-mail, for a code | email |
{% form 'customer_login_code' %} |
the code | code |
{% form 'customer_login_resend' %} |
a new code | — |
{% form 'customer_logout' %} |
sign out | — |
{% form 'customer_profile' %} |
the profile | first_name, last_name, phone |
{% form 'customer_address' %} / {% form 'customer_address', id: address.id %} |
a new address / that one, edited | label, name, address1, address2, city, province, postal, country (CA/US), phone, default (1) |
{% form 'customer_address_default', id: address.id %}, {% form 'customer_address_delete', id: address.id %} |
make it the default / delete it | — |
The sign-in forms carry next themselves. An address form names only one of the customer's own addresses — any other id leaves it inert — and every one of these forms is inert anywhere but its own account page. form.errors holds account.problem on the sign-in forms and the new-address form, and form.posted_successfully is account.saved.
On a policy page
/policies/{handle} is each store policy the business wrote under Settings → Store policies — refund, privacy, terms of service, shipping and contact information — drawn by the platform inside the theme's layout (the header and footer groups around it, no template involved, as on Shopify). policy there is the policy object (handle, title, body, url). A policy never written is the theme's 404. Link to them from a footer with shop.policies, or let the owner put one in a menu.
On a product page
| Global | Holds |
|---|---|
product |
The product the product template is drawing, with the same fields as an entry of products. A theme that ships templates/product.json draws every product's page itself; one that does not gets the platform's own product page inside the theme's chrome, so every product has a page either way. ?variant={id} on the page's address selects that variant (product.selected_variant); no other query key is read. The official themes' picker:<br>{{ product \| variant_picker_styles: '#product-main' }}<br><article id="product-main"> … {% for v in product.variants %}<p data-variant="{{ v.id }}"{% if v.selected %} data-variant-current{% endif %}>{{ v.price }}</p>{% endfor %}<p data-variant="unavailable">Unavailable</p> … {% for option in product.options %}<fieldset><legend>{{ option.name }}</legend>{% for value in option.values %}<label><input type="radio" name="option{{ option.position }}" value="{{ value.value }}"{% if value.selected %} checked{% endif %}> {{ value.value }}</label>{% endfor %}</fieldset>{% endfor %} |
Product fields
Every product entry — in products, a collection, a search, the product global — carries the owner's product custom fields (COM-018), made under Inventory → Product fields and filled in on each product's page:
| Key | Holds |
|---|---|
fields |
Each field the product has a value for, by key, in its type's own shape: text, textarea, select, date (yyyy-MM-dd) and color (a hex) are words; number a number; checkbox true or false; richtext the platform's rendered HTML; url an address (checked like every link setting); image a picture shaped for image_tag; measurement an object with value, unit (mm, cm, m, in, ft, g, kg, oz, lb, ml, l, fl oz, ct) and label ("30.5 cm"); list a list of lines; product a product entry and product_list a list of them, in the owner's order (a product no longer on the website drops out). A field without a value is absent, so {% if product.fields.condition %} is the one test |
field_list |
The same fields in the owner's order, each key, label, type, value (as above) and text — the value as plain words for a specification table ("Yes"/"No", "30.5 cm", lines joined by commas; empty for a picture, a link, formatted text or products) |
metafields.custom.{key} |
Shopify's name for the same field: value, type and label, so a ported theme's {{ product.metafields.custom.condition.value }} reads unchanged |
A product named by another product's field (and each product in related and complementary) carries everything an entry does except its own fields naming products, related and complementary, so a page can never loop. A select or checkbox field the owner marked Filter collections by it is offered as a list filter on a collection or search page (filter.field.{key}; Shopify's filter.p.m.custom.{key} is read too, and its 1/0 mean true/false), and a collection's rules can look at any field (field:{key} is / is not / is more than / is less than). A theme declares the fields it reads in config/product_fields.json (Package layout).
{%- if product.field_list.size > 0 -%}
<dl>{%- for field in product.field_list -%}<dt>{{ field.label }}</dt><dd>{{ field.text }}</dd>{%- endfor -%}</dl>
{%- endif -%}
{%- if product.fields.certificate -%}{{ product.fields.certificate | image_tag: sizes: '20rem' }}{%- endif -%}
Product media
Every product entry also carries its media (COM-019): the pictures and the videos the owner added on the product's page (Inventory → the product → Videos) — a clip from the media library, or a YouTube or Vimeo address of which only the provider and the id are kept, so every embed address is the platform's own.
| Key | Holds |
|---|---|
media |
Pictures and videos as one list in the owner's order (a video sits after as many pictures as the owner chose). Each has media_type — image, video (a library clip) or external_video (YouTube or Vimeo) — position (from 1), alt and preview_image (a picture shaped for image_tag, nil for a video without a poster). A picture is also everything image is, so {{ media | image_tag }} draws it; a clip carries sources (each url, mime_type) and video_url; an external video host (youtube or vimeo), external_id, embed_url (YouTube's privacy-enhanced player, Vimeo's with tracking off) and watch_url |
featured_media |
The first of them, nil when there is none |
media_count |
How many |
videos |
The videos alone, in order |
{{ video | video_tag: title: video.alt }} draws either kind: a lazy player frame for YouTube or Vimeo, and the platform's own <video controls> (its poster the preview picture) for a library clip. Behind a poster, a <details> keeps the provider from loading until the visitor plays it:
{%- for video in product.videos -%}
{%- if video.media_type == 'external_video' and video.preview_image -%}
<details><summary>{{ video.preview_image | image_tag: alt: '' }}</summary><iframe src="{{ video.embed_url }}&autoplay=1" title="{{ video.alt }}" loading="lazy" allowfullscreen></iframe></details>
{%- else -%}
{{ video | video_tag: title: video.alt }}
{%- endif -%}
{%- endfor -%}
Recommendations
Worked out by the platform from the website's own products (COM-023) — no query of the theme's, no script:
| Key | Holds |
|---|---|
product.related |
"You may also like": other website products that share a published collection with it, then the same category or brand, then a tag — never the product itself and never one sold out; ties by the number of shared tags, then name, so a page never reshuffles; at most 12. On a product page Shopify's recommendations object carries the same list (performed, intent related, products, products_count) |
product.complementary |
"Pairs with": the products the owner chose in the product field with the key complementary (a list of products or one product), in their order, those still on the website; at most 12 |
cart.recommendations |
On every page of a site that sells: each cart line's complementary products, then its related ones, never one already in the cart; at most 8 |
Each is a list of product entries without references of their own (no related, complementary or product fields naming products), so a page never loops. The official themes ship a product-recommendations section (source: related, complementary or cart) on their product and cart templates.
Product reviews
Every product entry carries rating (the mean of its published reviews, one decimal, nil without one) and rating_count, for stars on a card, and reviews (COM-025):
| Key | Holds |
|---|---|
reviews.count, average, average_value, rounded |
How many published reviews, the mean as words ("4.5") and as a number, and in whole stars |
reviews.histogram |
Five entries, five stars first, each rating, count and percent |
reviews.items |
On the product's own page only: the published reviews, featured first then newest, at most 24 — each rating, body (plain words), name (the name the reviewer chose), date, date_label, verified (a paid order of the product, read from the orders), response (the business's answer) and photos (pictures for image_tag) |
reviews.form |
On the product's own page only: action (where it posts; nil and inert on the canvas and the preview), token (the post-purchase request's, from ?review= — carry it as a hidden token field), signed_in, can_write (signed in, or a buyer with a request), sign_in_url, status (pending, published, signin, already, invalid) and reason (rating, body, name, photo, unavailable) from the last post, honeypot (print it inside the form) and max_photos (3) |
The form is a plain <form method="post" action="{{ form.action }}" enctype="multipart/form-data"> with rating (1–5), body, name and up to three photos (JPEG, PNG or WebP, 10 MB each, through the media library's own checks into its "Review photos" folder) — no script. It writes as the buyer who came by the request's link or as the signed-in shopper, once per product; anyone else comes back with signin. Nothing is public until the business publishes it (Reviews), unless it chose to publish without reading. A business may ask its buyers for a review a set number of days after a website order is paid: one e-mail, a link per product to its page with ?review= on it. A product's reviews are never among the business's own (reviews, the Reviews app).
Structured data
The platform writes the JSON-LD a search engine reads into the head of every public page that has a subject (COM-028), from its own figures — a theme does not hand-roll it:
- a product's page:
Product— name, description (plain words), pictures, SKU,brand(the vendor),gtin(a barcode of 8, 12, 13 or 14 digits), anOfferper live variant (or one for a product without variants) with the price as a number, the currency, the availability (InStock,LimitedAvailability,OutOfStock) and the variant's address, andaggregateRatingwith up to fivereviews once it has published reviews; - a collection's page (
/collections/{handle},all, a brand's or a type's):CollectionPagewith anItemListof the products shown; - a blog's post:
BlogPosting— headline, date, author, picture, summary, tags; - each of them: a
BreadcrumbListfrom the home page.
A theme that writes JSON-LD of a type itself keeps it: the platform leaves its own of that type out rather than describe the page twice (an Article counts as a BlogPosting). Nothing is written on the editor canvas or the owner's preview. A product page drawn by the platform (a theme without a product template) carries none yet.
On a blog page
/{blog} lists a blog's posts and /{blog}/tagged/{tag} the ones with that tag (never indexed), twelve to a page with ?page=n — or the theme's own page size with {% paginate blog.articles by 6 %}…{% endpaginate %}, whose paginate object pages the list with the blog's own addresses; the theme's blog.{handle} or blog template draws it, and without one the platform lists the posts inside the theme's layout. blog there is the blog with articles holding the current page only, current_tag, and pagination — current_page, pages, page_size, items, previous_url (newer posts) and next_url (older), nil at either end. current_tags is the tag the list is narrowed to, as a one-item list. A tag nobody used, or a page past the end, is the theme's 404.
On a post
A blog's post is its entry's own page (/{blog}/{post}), drawn by article.{handle} or article when the theme has one (else the entry templates, as before). article is the entry object plus Shopify's names: author, tags (the words), tag_links (title, url of each tag's list), published_at (yyyy-MM-dd, from the date field), published_label, excerpt (the summary), content (the post's HTML), image, url, reading_time (minutes), previous and next (title, url of the newer and older post), blog_handle, comments_count (0) and comments_enabled (false — comments are not taken). blog is the post's blog.
On a collection page
| Global | Holds |
|---|---|
collection |
The product collection the collection template is drawing, shaped like an entry of product_collections (with its products). The template answers /collections/{handle} for every published collection, /collections/all (every website product, in name order), /collections/vendors?q={brand} (one brand's products — product.vendor_url links there) and /collections/types?q={physical\|service\|digital}; an unknown handle, a brand or kind with nothing on the website, or a draft collection is the theme's own 404. Each page has its own canonical address, and every published collection, /collections/all and /collections are in the site's sitemap.xml |
The list-collections template answers /collections and draws from product_collections. A theme without a collection (or list-collections) template has no collection pages — they answer its 404 — so a theme that links to collections ships both. A content type's entry page also sets a collection, but the two are never on the same page.
On its own page the collection also carries its listing controls (COM-021), read from the address — ?sort_by=, ?page= and the filters — and from nothing else on it:
| Key | Holds |
|---|---|
products |
The products that pass the visitor's filters, in the order asked for — the first 48 outside a paginate block, the page asked for inside one |
products_count, all_products_count |
How many pass the filters, and how many the collection has |
sort_by, default_sort_by |
The order in force and the collection's own |
sort_options |
The orders on offer, each value, name (worded in the site's language) and selected: manual (only for a manual collection, labelled Featured), best-selling (quantities sold over the last year, every channel), title-ascending, title-descending, price-ascending, price-descending, created-ascending, created-descending |
filters |
Each label, param_name, type (list or price_range), active, values (a list filter's: value, label, count, active, param_name, url_to_add, url_to_remove), active_values, inactive_values and url_to_remove (the filter cleared). A price filter has range_max (the highest price, rounded up) and min_value / max_value, each param_name and value. The filters are availability (filter.availability=in_stock / sold_out), price (filter.price.gte / filter.price.lte, numbers in the site's currency), brand (filter.vendor), product type (filter.type, the kind), category (filter.category) and tag (filter.tag); values of one filter are alternatives, different filters all apply, and a value's count is what it would leave with the other filters applied. Shopify's names (filter.v.availability=1, filter.p.vendor, filter.p.product_type, filter.p.tag, filter.v.price.gte) are read too |
clear_filters_url |
The page with every filter off, its order kept |
form_url, form_fields |
Where a filter-and-sort <form method="get"> submits, and the hidden fields it must carry (name, value — a brand page's q), because a GET form drops its action's own query |
current_vendor, current_type, current_tags |
On a brand's or a kind's page, its name; the tags filtered on |
The form needs no script: checkboxes named {{ value.param_name }}, two number inputs for the price, a sort_by select and a submit button. A narrowed page (any filter, an order other than the collection's own, or a page past the first) answers with noindex and keeps the collection's own address as its canonical.
<h1>{{ collection.title }}</h1>
{{ collection.description }}
<ul>
{%- for item in collection.products -%}
<li><a href="{{ item.url }}">{{ item.name }}</a> — {{ item.price }}{% if item.new %} · New{% endif %}</li>
{%- endfor -%}
</ul>
<p>{{ collection.products_count }} products</p>
On the search page
| Global | Holds |
|---|---|
search |
The search template answers /search?q=… (COM-022): performed (something was typed), terms (what was typed, at most 200 characters), results — the website products that match, then the site's own pages whose name or search words match (left out once a product filter is on), each with object_type (product, shaped like an entry of products, or page: title, url, handle) — the first 48 outside paginate; results_count, products_count, pages_count, types, url, and the same sort_options (with relevance, the default), sort_by, default_sort_by, filters, clear_filters_url, form_url and form_fields as a collection page |
A product matches when every word typed is the start of a word in its name, SKU, brand, category, tags or description; the name weighs most. The search page is never indexed. A theme without templates/search.json has no search page — /search is then a page of the site's own by that name, or the 404 — and routes.search_url is nil.
Predictive search. /search/suggest?q=… returns the theme's sections/predictive-search.liquid alone, drawn from predictive_search: performed, terms, search_url (the full results), results_count and resources — products (at most 6), pages and collections (at most 3 each) and queries (empty). A theme without that section gets the same results as JSON (terms, url, products with title, url, price, availability, pages, collections). The platform's behaviour runtime fetches it into the header's search panel; without a script the header's search box simply submits to /search, so a theme's search works with no JavaScript at all. Both are rate-limited per visitor.
On an entry page
entry — id, title, handle, published, fields (the entry's values by field id), image, summary, body, date, date_label, link, url, collection_handle — and collection, the collection object it belongs to.
On a gallery page
gallery — id, name, description, date, cover_url, cover_focal, unlocked, wrong_code, allow_downloads, unlock_url, download_url (only when downloads are allowed and the gallery is unlocked), photos_count, videos_count, items_count, favorites (enabled, started, name, count, sent, start_url, send_url, error, or nil when proofing is off) and items: each id, kind (photo or video), url, small_url, medium_url, large_url, width, height, caption, embed_url, download_enabled, download_url, watch_url, favorite, favorite_url.
Inside a section
section — id, type, index (1-based position), location (template or the group's name), name, settings, settings_tablet, settings_mobile, blocks (the child block objects in order), block_count.
The platform wraps what a section renders in its own element: <section id="ab-s-{id}" class="ab-section ab-section--{type} {schema class}" data-ab-section="{id}" data-ab-section-type="{type}">. The element is the schema's tag when it is one of section, div, header, footer, aside, nav, article, and section otherwise. A section's responsive overrides are written as custom properties on #ab-s-{id}.
Inside a block
block — id, type, name, settings, settings_tablet, settings_mobile, abizvo_attributes (write it on the block's outermost element so the editor can select and move the block), blocks (its own children). section stays in scope.
<{{ tag }} class="{{ block.settings.scale }}" {{ block.abizvo_attributes }}>
{{- block.settings.text | editable: block, 'text' -}}
</{{ tag }}>
Inside a form
form — id, kind, posted_successfully, errors, inert.
Besides the business's forms (contact, newsletter, lead, quote, booking), {% form 'storefront_password' %} posts to the platform's password gate (an input named password; the address the visitor was going to rides along as a hidden return). It is live only on the password page and inert everywhere else.
Inside an app's block or embed
app — key, name, version, settings (the app-level settings the owner filled in); block as for a theme block; embed — name, target, settings. Only the site globals the app's manifest asked for are readable there; the rest are nil. In a layout, app_embeds is the installed apps' body embeds as one string, for a theme that places them itself (see Apps).
The {{ settings.size }} trap
Liquid answers size, first, last, keys, values and empty from a dictionary itself, so a setting with one of those ids can never be read: {{ block.settings.size }} is the number of settings in the block, whatever the owner chose. The editor still shows the control, which is what makes it expensive to find — it shipped in five official themes and did nothing in any. The theme check refuses those six ids ("size" cannot be a setting id); name a size scale.
Rich text
A richtext or inline_richtext value is the platform's own markup, never HTML — blank lines between paragraphs, ## for a heading, - for a list item, links as the platform's link syntax. It reaches Liquid already rendered to safe HTML, so {{ section.settings.body }} writes paragraphs. A default written as HTML renders its tags as words (<p>Open with the sentence…); MarketplaceThemesTests.No_rich_text_default_is_written_as_html holds every paid theme to it.