Theme App Extensions
Putting your app on the storefront — badges, widgets, chat bubbles — without touching a single theme file. The only approach the App Store accepts, and the one that survives a merchant changing themes.
The problem this solves
So far everything has lived in the admin. But many apps need to show something to shoppers — a badge on a product, reviews, a size chart.
The tempting shortcut is to edit the merchant's theme code directly. Do not. It breaks the moment they switch or update their theme, it is hard to undo, and the App Store rejects apps that do it.
A theme app extension is the proper way: you ship small pieces of Liquid with your app, and the merchant places them using the normal theme editor.
For the merchant
- Drag your block into place in the theme editor.
- No code to edit.
- Works across themes.
- Uninstall, and it disappears cleanly.
For you
- One version works for every store.
- Updates reach every store on deploy.
- Assets hosted on Shopify's CDN.
- Required for App Store storefront changes.
Two kinds of block
| App block | App embed block | |
|---|---|---|
| Where it appears | Inside a section, where the merchant places it | Injected before </head> or </body> on every page |
| Good for | Things with a position — a badge, a reviews widget, a size chart | Floating or invisible things — chat bubbles, pop-ups, tracking |
target | "section" | "body" or "head" |
| Needs | An Online Store 2.0 theme | Works on older themes too |
| After install | Merchant adds it where they want | Off until the merchant switches it on |
Create one
shopify app generate extension
Choose Theme app extension and give it a name. You get a new folder:
extensions/
└── storefront-badges/
├── assets/ CSS, JS, images
├── blocks/ your blocks — one .liquid file each
├── snippets/ reusable Liquid pieces
├── locales/ translations
└── shopify.extension.toml name and type
The generated example shows product ratings, and needs a matching metafield to display anything — so if the preview is blank at first, that is expected.
An extension is part of your app, not a separate project. It deploys with your app's settings when you run shopify app deploy.
An app block
Continuing the badges app from Note 14: show the product's badge on the product page.
extensions/storefront-badges/blocks/product-badge.liquid{%- assign badge = product.metafields.custom.badge.value -%}
{%- if badge and badge.label != blank -%}
<span
class="product-badge"
style="background: {{ badge.color | default: block.settings.fallback_color }};"
>
{{ badge.label | escape }}
</span>
{%- endif -%}
{% schema %}
{
"name": "Product badge",
"target": "section",
"enabled_on": { "templates": ["product"] },
"stylesheet": "product-badge.css",
"settings": [
{
"type": "color",
"id": "fallback_color",
"label": "Colour if none is set",
"default": "#0a6b57"
}
]
}
{% endschema %}
extensions/storefront-badges/assets/product-badge.css
.product-badge {
display: inline-block;
padding: 2px 10px;
border-radius: 999px;
color: #fff;
font-size: 12px;
font-weight: 600;
}
How the pieces connect:
- The top half is Liquid — Shopify's template language. It runs on Shopify's servers when the page is built, with access to
product,shopand so on. {% schema %}describes the block to the theme editor: its name, where it may be used, and the settings the merchant can change.settingsbecome form fields in the theme editor, read back asblock.settings.fallback_color."stylesheet"loads a file fromassets/only on pages where the block is used.
escape merchant text
{{ badge.label | escape }} turns characters like < into safe text. Without it, a label containing HTML would be injected straight into the storefront.
An app embed block
For something that floats over every page instead of sitting inside a section:
extensions/storefront-badges/blocks/help-bubble.liquid<div class="help-bubble">
<a href="{{ block.settings.link }}">{{ block.settings.label | escape }}</a>
</div>
{% schema %}
{
"name": "Help bubble",
"target": "body",
"stylesheet": "help-bubble.css",
"settings": [
{ "type": "text", "id": "label", "label": "Label", "default": "Need help?" },
{ "type": "url", "id": "link", "label": "Link" }
]
}
{% endschema %}
The only structural difference is "target": "body".
App embed blocks are switched off after install, and your app cannot switch them on for the merchant. They must turn it on themselves in the theme editor under Theme settings → App embeds.
Skip telling them and your app appears broken — the most common support request for storefront apps.
Fix: show clear setup instructions in your admin, and give them a button that opens the theme editor with your embed ready to enable. Shopify documents a deep link for exactly this.
Getting your data into a block
This is the key limitation to understand: a block cannot query your database. It runs as Liquid on Shopify's servers, with no connection to your app. Your data has to reach it another way.
| Way | Use when | Read with |
|---|---|---|
| Resource metafields | Data belongs to a product, collection or customer | product.metafields.custom.badge.value |
| App-owned metafields | App-wide settings for this shop | app.metafields.namespace.key |
| Block settings | Things the merchant chooses per placement | block.settings.x |
| App proxy | Live data, or anything computed on your server | fetch() from the block's JavaScript |
App-owned metafields are worth knowing. They belong to your app's installation rather than to any product, so merchants and other apps cannot edit them. Write them with metafieldsSet using your installation's id as the owner:
query { currentAppInstallation { id } } # → gid://shopify/AppInstallation/123
Then read them in any block as app.metafields.namespace.key.
Metafields are fast and render with the page, but hold a copy of your data. An app proxy fetches live from your server every time — slower, but always current, and it stops working if your app is removed. The App Proxy Runbook covers pairing the two.
Previewing
shopify app dev
With an extension present, the CLI walks you through previewing it: it opens the theme editor, you add your block, save, and open the preview link. Changes reload as you save files.
- Choose which theme with
--theme. Without it, the CLI uploads Shopify's reference theme to your dev store. - The live preview works in Google Chrome.
- Test with real data: give a product an actual badge first.
Shipping it
shopify app deploy
That releases a new app version including the extension, and every store with your app gets the update — no per-store work.
Blocks are part of your app's settings, not your server code. Changing a .liquid file needs shopify app deploy; redeploying your server does nothing for it. (Note 11's "two deploys", again.)
Merchants still decide where the block goes. Adding a new block type does not place it on their storefront automatically — they add it in the theme editor.
Common problems
My block does not appear in the theme editor
- The theme is not Online Store 2.0, or the section does not accept app blocks.
enabled_onrestricts it to templates other than the one you are editing.shopify app devis not running, or the extension has not been deployed.
The block is added but shows nothing
Usually the data is missing — the product has no metafield, so the if is false. Temporarily print {{ product.metafields.custom.badge.value | json }} to see what the block receives.
My embed does nothing
It is switched off. Enable it under Theme settings → App embeds.
My changes are not live for merchants
Run shopify app deploy. Extension changes are not part of a server deploy.
Can a block call my database?
No. Put the data in a metafield, or fetch it from an app proxy.
Checklist
- No edits to the merchant's theme files — everything ships as blocks.
- App blocks for positioned content; embeds for floating or invisible content.
- Merchant-entered text passed through
escape. - Data delivered by metafields, block settings or an app proxy.
- Setup instructions in your admin, including how to enable embeds.
- Tested on at least two different themes.
- Deployed with
shopify app deploy.
Cheat sheet
# create
shopify app generate extension → Theme app extension
# block types
"target": "section" app block — merchant places it
"target": "body" app embed — OFF until merchant enables it
# schema essentials
name · target · enabled_on · stylesheet · javascript · settings
# reading data
product.metafields.custom.x.value resource data
app.metafields.ns.key app-wide settings
block.settings.x merchant's choices
fetch('/apps/…') live data via app proxy
# ship
shopify app deploy (a server deploy does NOT update blocks)
# always
{{ text | escape }}