Adding a Headless CMS to an Existing DTC Storefront Without Replatforming
If every campaign page on your Shopify storefront waits on a developer to edit Liquid or a Hydrogen route, you do not need to replatform to fix it. Yes, you can add structured content alongside the store.
If every campaign page on your Shopify storefront waits on a developer to edit Liquid or a Hydrogen route, you do not need to replatform to fix it. Yes, you can add structured content alongside the store. With Sanity, Sanity Connect syncs Shopify products, variants, and collections into the Content Lake as documents, with Shopify-owned data under a read-only `store` field. Editors can reference real products, and Shopify keeps running cart and checkout.
The stakes are practical. Replatforming puts checkout, payments, tax rules, apps, and order flows at risk to solve a problem that lives in the editorial layer. Meanwhile, the workarounds keep growing. Product storytelling gets hardcoded into templates, launch pages get built in metafields nobody can preview, and campaign copy gets copied by hand between channels.
This guide treats the problem as an add-alongside pattern, not a migration. Commerce stays the system of record, content gets its own schema, and the rollout happens in phases that start with pages that never touch templates. The guide also covers the three risks that sink these projects: duplicated data, preview across two systems, and caches that clear on two separate signals.
What is the add-alongside pattern for a DTC storefront?
The add-alongside pattern is an architecture where the commerce platform stays the system of record for products, variants, pricing, inventory, cart, and checkout. A separate structured content platform sits next to it and handles everything editorial. Nothing in the transaction path moves. The new layer owns pages, campaigns, buying guides, and product storytelling, and the storefront frontend combines both sources when it renders a page.
The pattern exists because most DTC content problems are not commerce problems. A brand that needs a seasonal lookbook, a founder story, or a comparison page for two product lines usually runs into the limits of theme sections and product description fields, not the limits of checkout. Replatforming to fix that means re-validating payments, tax, shipping rules, apps, and order flows. That is months of risk aimed at the wrong layer.
There is an honest alternative to weigh first. Shopify metaobjects and metafields give you structured content inside Shopify itself, and for a small catalog with one storefront and one editor, that is often enough. The trade-off is that schemas and the editing experience are limited to what Shopify admin offers. Once several teams, several channels, or campaign pages with real layout needs show up, a dedicated content layer is worth adding. The two options can work together, because content written elsewhere can still be pushed back into Shopify as metafields and metaobjects.
In Sanity, the add-alongside pattern maps to the "Model your business" pillar. Products arrive as synced documents in the Content Lake, editorial types are defined in code in your Studio schemas, and references connect the two. Sanity positions itself as the AI Content Operating System, the intelligent backend for companies building AI content operations at scale. For a storefront, the practical result is that commerce data and editorial content sit on one queryable foundation. The frontend no longer has to reconcile two separate silos.
How does Shopify product data get into Sanity?
Shopify product data gets into Sanity through Sanity Connect, an app installed from the Shopify App Store. It writes products, variants, and collections into the Content Lake as documents. Each synced document gets a predictable ID, such as `shopifyProduct-7696133062907` for a product or a `shopifyProductVariant-` prefix for a variant, so references stay stable across syncs.
Sanity Connect has two modes. Direct sync writes every product, product variant, and collection as its own document. Custom sync sends updates to an endpoint you control, usually a serverless handler, where you can reshape the data and apply business logic before it lands. Document count is a common reason to choose custom sync. Synced documents count toward your Sanity document usage limit, and syncing variants as objects on the product, instead of as separate documents, keeps a large catalog leaner.
Everything Shopify owns sits under a `store` property on each document. The docs recommend marking those fields `readOnly` or `hidden` in your Studio schemas. That is the simplest way to stop editors from changing commerce data that the next sync would overwrite anyway. Your editorial fields, such as a long-form story, a size guide reference, or a hero image, sit beside `store` on the same document.
Timing matters for planning. Automatic sync runs from Shopify webhooks delivered to a Google Cloud Pub/Sub topic that Sanity operates, so you don't maintain webhook infrastructure yourself. Updates usually land in the Content Lake after a couple of seconds. A daily drift-correction run repairs missed webhooks and deletions.
Two steps reduce first-week surprises. First, run the first sync against a non-production dataset so you can check document shapes and counts before editors see them. Second, if you want to enrich synced documents after they land, use Functions. These are small TypeScript or JavaScript handlers declared in a `sanity.blueprint.ts` Blueprint. They react to `create`, `update`, and `delete` events, and they can read and write the dataset, traverse references, and call external services.
Drift correction stops at custom sync
What should stay owned by the commerce platform?
Price, inventory, variants, product status, collection membership, cart, and checkout should stay owned by the commerce platform, and the content layer should treat them as read-only inputs. Sanity Connect enforces much of this by design, but a few behaviors are worth knowing before you build schemas around them.
Shopify metafields arrive as a read-only `store.metafields` array that you can query in GROQ. Shopify stays the source of truth, and each sync overwrites the array, so any edit made in Sanity to that data is lost. Collection sync brings over collection properties but not which products belong to each collection. A merchandising page that needs collection membership should get it from Shopify, not from the synced collection document.
Product status is mirrored. A Shopify product in Draft becomes an unpublished Sanity document, and an Active product becomes a published one. Archiving a product triggers an attempt to unpublish its Sanity document. That attempt fails while another document still references the product, and the sync retries on the next run. In practice, a campaign page that references a discontinued product keeps that product document published until someone removes the reference, so add a check for it to your launch checklist.
Duplicated data is the main architectural risk of the add-alongside pattern. Synced fields are copies, and copies can fall out of date. One approach is to reference synced product documents for editorial needs like titles, images, and slugs, and fetch price and inventory from the Storefront API at render time. That way, the numbers a customer sees always come from Shopify. Another approach is to keep only Shopify product IDs in content and fetch all product data live. Sanity's docs don't prescribe either one. The right choice depends on how much product data your editorial queries actually need.
Content can also flow in the other direction. Sanity fields can sync back into Shopify as metafields and metaobjects, so Liquid themes or the Storefront API can use editorial content. The one exception is Draft products, which don't get write-back.
What does a phased rollout look like for an existing storefront?
A phased rollout starts with the parts of the store where the content layer can't break checkout. New editorial routes come first, then campaigns, then product storytelling, and templates come last. Each phase delivers value on its own, so the project never depends on switching everything over at once.
Phase 1 is editorial pages. Brand stories, buying guides, and lookbooks go on new routes that the current theme or frontend doesn't already own. Editors learn the Studio, developers agree on schema conventions, and nothing on product or collection pages changes. Start a new project with `npm create sanity@latest -- --template shopify`, or follow the docs' path for adding Sanity Connect to an existing Studio if you already run one.
Phase 2 is campaigns and landing pages. These are time-bound, so this is the phase where launch scheduling with Content Releases or Scheduled Drafts comes in. Campaign pages reference synced products, and one GROQ query can return the campaign and its products together, for example `*[_type == "campaign" && slug.current == $slug]{title, "products": products[]->store}`. The `->` operator dereferences each product in the same round trip. That is the practical payoff of having product documents in the Content Lake.
Phase 3 is product storytelling. Editorial modules such as ingredient stories, care guides, or comparison blocks attach to product pages through slots that the existing template renders. Price, variants, and add-to-cart stay exactly where they are.
Phase 4 is templates. Moving product and collection templates to content-driven layouts is the riskiest step, so it comes last. Wait until the team trusts how sync, preview, and caching behave in production.
The frontend can be whatever you already run. Sanity Connect supports Hydrogen and Next.js, and it can sync content back into Shopify for Liquid themes or the Storefront API. This is the "Power anything" pillar in practice. The same schema can feed a Hydrogen storefront today and a different channel later without restructuring content.
How do Content Releases and Visual Editing help merchandisers launch?
Content Releases and Visual Editing let merchandisers stage a launch across many documents and see it on the real storefront before it goes live. Neither one schedules Shopify-owned product changes, though, and understanding that limit is most of the planning work.
Content Releases groups changes across multiple Sanity documents so they can be previewed, validated, scheduled, and published as one unit. A product drop might bundle a homepage hero, a campaign page, three product story modules, and a navigation update into one release. Releases can be previewed in the Presentation Tool. Content Releases is available on certain Enterprise plans and requires Studio v3.77.0 or later. Teams on other plans can use Scheduled Drafts. The limits are 1,000 documents per release and 200 active releases per dataset.
Visual Editing connects Sanity Studio to the frontend. In the Presentation Tool, editors see drafts render on the live site or a preview environment, click any element to jump to the field behind it, and drag and drop sections to rearrange a page. Under the hood, it uses stega encoding, Content Source Maps, perspectives, draft mode, and the Live Content API. There are quickstarts for Next.js, Remix and React Router, Astro, SvelteKit, Nuxt, and React Native. For a merchandiser, this means editing a campaign page in place instead of filling in a form and checking the result on a staging URL later.
The limit is the `store` field. Sanity Connect does not sync Content Release versions, so product title, price, or availability changes for a launch can't be staged inside a release. Those changes still get scheduled in Shopify. A launch plan therefore needs two schedules that go live at the same minute: the release in Sanity and the product or price schedule in Shopify.
This is the "Automate everything" pillar applied to launches. A document function with `includeAllVersions` enabled fires on Content Release version events. That gives you a hook for pre-launch checks, such as flagging release pages that reference products still in Draft in Shopify.
A release cannot carry the price change
How do you handle preview and caching across two systems?
Preview and caching across two systems work when you accept that commerce data and content are refreshed by separate signals and plan for both. Most post-launch bugs in add-alongside builds come from assuming one cache layer covers everything.
Start with preview. Visual Editing shows Sanity drafts, including drafts staged in a release. The Shopify data in that preview is whatever Shopify returns right now, so a preview of tomorrow's launch shows today's prices unless your frontend also reads a Shopify preview source. Draft products in Shopify appear as unpublished documents in Sanity, which helps editors build pages around unreleased products. Even so, the price and inventory a customer sees at launch still come from Shopify. Agree with merchandisers on which parts of a preview they can trust.
On the content side of caching, the Live Content API streams sync-tag invalidation events, and the `<SanityLive>` component in next-sanity uses those sync tags as cache tags. Functions add a server-side option. Sync tag invalidate functions react to Live Content sync tags, and Sanity's docs pair one with an `/api/expire-tags` route that calls `revalidateTag(tag, "max")` to avoid request overages in Next.js 16.
On the commerce side, price and inventory cached from the Storefront API don't receive any of those signals. A common pattern is to cache editorial content aggressively and clear it by sync tag, while giving price and inventory short cache lifetimes or fetching them on every request. Product titles and images synced through Sanity Connect sit in between. They change in Shopify, arrive in the Content Lake a couple of seconds later, and are then cleared through the content path.
Write down where every piece of data comes from and what clears it. For each field on a product page, list the system of record, the cache that holds it, and the event that clears it. That table becomes your debugging guide the first time a sale price shows on one page and not another.
Adding structured content to an existing Shopify storefront: how the options compare
| Feature | Sanity | Shopify metaobjects (native) | Contentful | Storyblok |
|---|---|---|---|---|
| Product data sync mechanism | Sanity Connect writes products, variants, and collections to the Content Lake from Shopify webhooks, usually within seconds, plus a daily drift-correction run on direct sync | No sync step: product data and metaobjects live in the same Shopify admin, so content and commerce share Shopify's data model and its limits | External references link entries to ecommerce systems such as Shopify, so product data can be resolved from the source instead of being copied in | Usually paired with Shopify through an ecommerce integration or custom fields that hold product identifiers, with live product data fetched from Shopify |
| Ownership of commerce-owned fields | Shopify data sits under a `store` property you mark `readOnly` or `hidden` in Studio schemas; metafields arrive as a read-only `store.metafields` array overwritten on each sync | Shopify is the only system, so ownership is implicit; who can edit what comes from Shopify admin staff permissions, not from a separate content layer | Commerce fields stay in Shopify when referenced externally; editors work with product references in Contentful's web app, configured through its extension points | Product fields stay in Shopify while stories hold references, so ownership is clear, though what product detail editors see depends on the integration used |
| Joining products and editorial content in one query | One GROQ query returns a campaign and its products: `products[]->store` dereferences synced product documents in the same round trip, projecting only what the page needs | Storefront API queries return products with metafields and metaobject references, but editorial structure is limited to what metaobject definitions can express | Entries and links resolve through GraphQL or REST; external product data comes through Contentful's external references setup or a separate call to Shopify | The Content Delivery API returns stories with resolved relations; product details usually come from Shopify in a separate request keyed by stored IDs |
| Multi-document launch staging | Content Releases bundles up to 1,000 documents to preview, validate, and schedule together (certain Enterprise plans); Shopify-owned product changes still get scheduled in Shopify | Products, themes, and metaobjects are scheduled with Shopify's own tools, without a separate release layer for bundling editorial changes across content types | Supports grouping and scheduling entry changes for launches; product, price, and inventory changes on the Shopify side still have to be scheduled in Shopify | Supports scheduling grouped story changes on supported plans; product and price changes for the same launch remain scheduled in Shopify |
| Merchandiser visual preview | Visual Editing in the Presentation Tool: click-to-edit, drag-and-drop sections, and live draft rendering inside a Studio you configure in code, with Next.js, Astro, SvelteKit, and Nuxt quickstarts | The theme editor gives visual editing for Liquid themes; headless storefronts such as Hydrogen need their own preview setup to match that experience | Live Preview renders draft entries in your frontend; in-context editing depends on installing and wiring Contentful's preview SDK into the storefront | The visual editor is the core experience, giving merchandisers in-context page building out of the box once the frontend bridge is integrated |
| Cache invalidation hooks | Live Content API sync tags act as `<SanityLive>` cache tags, and sync tag invalidate Functions can call an `/api/expire-tags` route that runs `revalidateTag` | Hydrogen provides caching strategies for Storefront API calls; with content inside Shopify, there is one system to invalidate instead of two | Webhooks fire on publish and unpublish, so you connect them to your frontend's revalidation endpoint and keep commerce caches on a separate schedule | Webhooks on story publish trigger your revalidation route; commerce data from Shopify still has to be cleared by its own signals |