Theme App Extension Install: One-Click AI Chat for Shopify, No theme.liquid Edits
How a Shopify Theme App Extension makes AI chat widget install a one-click toggle in the theme editor — and why it bundles cart-attribute tracking for conversion attribution.
Installing a chat widget on Shopify used to mean editing theme.liquid. Open the code editor, find the right place above </body>, paste a script tag, save, hope the merchant didn't skip a quote mark on the way through. It's the kind of step that turns a five-minute install into a half-hour support thread.
The Theme App Extension flips the install model. The merchant opens the theme editor, enables the "Milly Chat" block, and the widget renders. No script tag pasting, no theme code edits. Same outcome, lower-friction path.
Note: this entry is pending Shopify App Store approval as of the v2.4.6 ship date. Existing custom-distro installs continue to work via the previous script-tag approach; this post describes the public-listing path that opens up after approval lands.
The friction the old way had
The script-tag-in-theme.liquid path leaks at three places:
- Theme update wipes — when the merchant updates the parent theme, customizations to
theme.liquiddon't always survive. The script tag disappears silently and the widget stops loading. By the time it's noticed, the merchant's lost a chunk of traffic. - Theme switching — testing a new theme means re-pasting the script tag every time. Most merchants forget on at least one switch.
- Per-merchant variation — the script tag carries a
data-store-idattribute. Every install requires the merchant to grab their store ID from the dashboard and paste it correctly. One typo and the widget loads but talks to the wrong store.
All three failure modes go away when the install lives in the theme editor instead of the theme code.
The new flow
After approving the app, the merchant goes to Online Store → Themes → Customize → App embeds and sees the "Milly Chat" block listed. Toggle it on, save, done. The widget renders on every page that uses theme.liquid (which is every page on a standard Shopify theme).
The block carries a target: "body" directive — Shopify's spec for app embeds that inject into the document body rather than a specific section. There's nothing for the merchant to configure on the block itself; the single checkbox is the entire UX.
The hidden second job: cart-attribute injection
The block doesn't just inject the widget script. It also wires up cart-attribute injection for conversion tracking — the glue that connects in-chat product recommendations to actual orders.
The flow:
- The widget sets a
milly_visitor_idin localStorage when a visitor first loads it - The Theme App Extension reads that ID and POSTs it to
/cart/update.jsas a cart attribute (alongsidemilly_widget_shown) - When the visitor checks out, Shopify carries the cart attributes through to
order.note_attributes - Our orders/create webhook reads the visitor ID off the order and joins it back to widget activity for attribution
Without the cart-attribute step, the visitor ID is locked to the visitor's browser and never makes it onto the order. Attribution would have to depend on UTM tags or pixel-side tracking, both of which are noisy. The cart attribute is first-party Shopify data — it stays clean through checkout.
Bundling the attribution wiring into the same block means a merchant who enables the embed gets both capabilities in one toggle. They don't have to wire attribution separately, and they can't accidentally enable the widget without enabling tracking. Single concept, single switch.
The metafield handles the per-merchant variation
The store-ID-in-script-tag problem from the old install path gets solved cleanly via a Shopify metafield. The OAuth install flow writes the store's UUID into a per-shop metafield:
// On install, the OAuth callback runs:
await setShopMetafield({
namespace: "milly_chat",
key: "store_id",
value: "<store UUID>",
type: "single_line_text_field",
});The Liquid block reads it back at render time:
{% assign milly_store_id = shop.metafields.milly_chat.store_id %}
{% if milly_store_id != blank %}
<script
src="https://cdn.millysoftware.com/widget.js"
data-store-id="{{ milly_store_id }}"
async
></script>
{% endif %}The merchant never sees their store ID. They never type it. They never miss a quote mark. The metafield is set on install and the block reads from it on every page load. The whole per-merchant install variation collapses into nothing the merchant has to think about.
Existing custom-distro stores from before this entry shipped had their metafields backfilled via a one-shot script — the widget didn't need to change behavior for them, just gain access to a metafield that hadn't existed before.
Approval path + what changes when it lands
Theme App Extensions ship through Shopify's public app listing process. The new install flow lives behind app store approval — until that lands, new merchants on the public listing path still go through manual install (or use the custom-distribution path, which doesn't require app store review). Existing custom-distro merchants are unaffected either way.
When approval lands, the merchant onboarding path simplifies:
- Merchant installs the app from the Shopify App Store
- OAuth flow writes the store-ID metafield
- Merchant opens the theme editor and toggles the "Milly Chat" embed on
- Widget loads and tracking starts in the same step
That's the whole install. No code edit, no script paste, no per-merchant store ID handling. The friction the old path had goes away — and so do the failure modes.
Frequently Asked Questions
How do I install the chat widget with the Theme App Extension?
You enable the 'Milly Chat' block in the Shopify theme editor under Online Store → Themes → Customize → App embeds — toggle it on and save. There's no theme.liquid edit and no script tag to paste, and there's nothing to configure on the block itself; the single checkbox is the whole setup, and the widget renders on every page that uses theme.liquid.
Why is the theme-editor install better than pasting a script tag?
The old script-in-theme.liquid path broke in three ways: a theme update could silently wipe the tag and stop the widget loading, switching themes meant re-pasting it, and every install required pasting the store ID by hand — one typo and the widget talked to the wrong store. Living in the theme editor removes all three failure modes, and a per-shop metafield supplies the store ID so the merchant never types it.
Does the app embed also handle conversion tracking?
Yes — that's its hidden second job. The block wires cart-attribute injection: the widget sets a visitor ID, the extension writes it (and a widget-shown flag) onto the cart via /cart/update.js, Shopify carries those attributes through checkout onto the order, and a webhook joins the visitor ID back to widget activity. Bundling it into the same toggle means you can't enable the widget without enabling tracking, and it uses first-party Shopify data so attribution stays clean without UTM tags or pixels.