Shopify app notes · 17

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.

Liquid storefront

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 blockApp embed block
Where it appearsInside a section, where the merchant places itInjected before </head> or </body> on every page
Good forThings with a position — a badge, a reviews widget, a size chartFloating or invisible things — chat bubbles, pop-ups, tracking
target"section""body" or "head"
NeedsAn Online Store 2.0 themeWorks on older themes too
After installMerchant adds it where they wantOff 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.

It lives in the same repo

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, shop and so on.
  • {% schema %} describes the block to the theme editor: its name, where it may be used, and the settings the merchant can change.
  • settings become form fields in the theme editor, read back as block.settings.fallback_color.
  • "stylesheet" loads a file from assets/ only on pages where the block is used.
Always 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".

Trap — the app that "does nothing"

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.

WayUse whenRead with
Resource metafieldsData belongs to a product, collection or customerproduct.metafields.custom.badge.value
App-owned metafieldsApp-wide settings for this shopapp.metafields.namespace.key
Block settingsThings the merchant chooses per placementblock.settings.x
App proxyLive data, or anything computed on your serverfetch() 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 vs app proxy

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.

Code deploy vs extension deploy

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_on restricts it to templates other than the one you are editing.
  • shopify app dev is 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 }}