Type something to search...
Using Sanity With Shopify for E-commerce

Using Sanity With Shopify for E-commerce

To use Sanity with Shopify, install the Sanity Connect app in your Shopify admin and link it to a Sanity project and dataset. It syncs your products, variants and, optionally, collections into Sanity as documents, with Shopify's data stored in a read-only store object on each one. Editors then add the content Shopify can't hold well, such as rich descriptions, lookbooks, guides and landing pages, in Sanity Studio. Your storefront, built with Shopify Hydrogen or Next.js, fetches prices, inventory and cart data from Shopify's Storefront API, fetches content from Sanity with GROQ, and joins the two by product handle or ID.

You keep Shopify's checkout, payments and inventory, and gain a content model shaped around your brand. The hard part is deciding which system owns which data. This guide covers that split, installing Sanity Connect, the documents and fields it creates, setting up the Studio, enriching products with editorial content, building Hydrogen and Next.js storefronts that query both systems, syncing content back to Shopify, and keeping everything fresh.

Where Content and Commerce Data Should Live

The most common mistake in a Sanity and Shopify build is duplicating data, or reading data from the system that doesn't own it. Use this split:

DataOwnerRead it from
Price, compare-at priceShopifyStorefront API
Inventory and availabilityShopifyStorefront API
Variants, options, SKUsShopifyStorefront API (Sanity has a synced copy)
Cart, checkout, customers, ordersShopifyStorefront and Customer Account APIs
Product title and handleShopifyEither, the synced copy is fine for listings
Rich descriptions, size guides, care instructionsSanityGROQ
Lifestyle images, videos, lookbooksSanityGROQ
Landing pages, editorial articles, navigationSanityGROQ
SEO overrides, promotional bannersSanityGROQ

The rule is simple: anything that affects what a customer pays or whether they can buy comes from Shopify at request time. Sanity's synced store fields are a convenient copy for references, previews and listing cards, but they're not the source of truth for checkout. Everything editorial lives in Sanity, where it can reference products by their synced documents.

Install Sanity Connect

Sanity Connect is Sanity's official Shopify app:

  1. Install Sanity Connect from the Shopify App Store.
  2. Log in to Sanity from the app and choose a project and dataset.
  3. Choose a sync mode, then let the initial sync run.

Test against a non-production dataset first, so you can check the documents before they land in the dataset your site uses. Each product, variant and collection becomes one document and counts towards your plan's document limit, which matters for large catalogues. The datasets and environments guide explains how to set up a staging dataset.

Sync Modes

ModeWhat happensUse it when
AutomaticEvery product save in Shopify updates Sanity within secondsMost stores
ManualNothing syncs until you trigger it in the app settingsTight control over changes
Custom sync handlerShopify updates are sent to your own endpointYou want a different document shape

Collection sync is a separate option. It copies a collection's own properties, such as its title, handle, image and rules, but not which products belong to it. A daily reconciliation job also catches changes or deletions that a webhook missed, and the app's Logs tab shows recent runs.

What Sanity Connect Creates

Synced documents use predictable IDs and types:

Shopify resourceSanity _type_id format
ProductproductshopifyProduct-<id>
VariantproductVariantshopifyProductVariant-<id>
CollectioncollectionshopifyCollection-<id>

All Shopify data lives under store. A product document looks roughly like this, trimmed:

{
  "_id": "shopifyProduct-7696133062907",
  "_type": "product",
  "store": {
    "id": 7696133062907,
    "gid": "gid://shopify/Product/7696133062907",
    "title": "Linen Overshirt",
    "slug": { "_type": "slug", "current": "linen-overshirt" },
    "status": "active",
    "isDeleted": false,
    "previewImageUrl": "https://cdn.shopify.com/...",
    "productType": "Shirts",
    "vendor": "Northwind",
    "priceRange": { "minVariantPrice": 89, "maxVariantPrice": 95 },
    "options": [{ "_key": "Size", "_type": "option", "name": "Size", "values": ["S", "M", "L"] }],
    "variants": [{ "_key": "...", "_type": "reference", "_ref": "shopifyProductVariant-4321", "_weak": true }]
  }
}

Useful details:

  • store.slug.current is the Shopify handle, which is what your storefront routes use.
  • store.gid is the Shopify global ID, which you can pass straight to the Storefront API.
  • store.variants holds weak references to the variant documents, which carry price, compareAtPrice, sku, option1 to option3 and inventory.isAvailable under their own store.
  • Status: active products are published in Sanity, draft products are created as unpublished, and archiving a product unpublishes its document. Deleted products are flagged with store.isDeleted.
  • Metafields: if you enable import, product and collection metafields appear in a read-only store.metafields array, overwritten on every sync.

Never edit store fields in Sanity. Shopify is the source of truth, and the next sync overwrites them.

Set Up the Studio

The quickest start is Sanity's Shopify Studio template, which includes schemas for the synced types, read-only store fields, product previews and editorial modules:

npm create sanity@latest -- --template shopify

To add Shopify types to an existing Studio, define the documents yourself. Keep store read-only and put your own fields alongside it:

// schemaTypes/product.ts
import { defineField, defineType } from 'sanity'

export const productType = defineType({
  name: 'product',
  title: 'Product',
  type: 'document',
  fields: [
    defineField({
      name: 'store',
      type: 'object',
      readOnly: true,
      fields: [
        defineField({ name: 'title', type: 'string' }),
        defineField({ name: 'slug', type: 'slug' }),
        defineField({ name: 'gid', type: 'string' }),
        defineField({ name: 'status', type: 'string' }),
        defineField({ name: 'isDeleted', type: 'boolean' }),
        defineField({ name: 'previewImageUrl', type: 'url' }),
      ],
    }),
    defineField({ name: 'body', title: 'Description', type: 'array', of: [{ type: 'block' }] }),
    defineField({ name: 'gallery', type: 'array', of: [{ type: 'image', options: { hotspot: true } }] }),
    defineField({ name: 'sizeGuide', type: 'reference', to: [{ type: 'sizeGuide' }] }),
    defineField({ name: 'seo', type: 'seo' }),
  ],
  preview: {
    select: { title: 'store.title', imageUrl: 'store.previewImageUrl', status: 'store.status' },
    prepare: ({ title, status }) => ({ title, subtitle: status }),
  },
})

You only need to declare the store fields you want editors to see; undeclared fields stay in the document regardless. Because Shopify creates and deletes these documents, stop editors from doing either in the Studio:

// sanity.config.ts
const SHOPIFY_TYPES = ['product', 'productVariant', 'collection']

export default defineConfig({
  // ...
  document: {
    newDocumentOptions: (prev) =>
      prev.filter((item) => !SHOPIFY_TYPES.includes(item.templateId)),
    actions: (prev, { schemaType }) =>
      SHOPIFY_TYPES.includes(schemaType)
        ? prev.filter(({ action }) => !['duplicate', 'delete', 'unpublish'].includes(action ?? ''))
        : prev,
  },
})

The document actions guide explains this API in more detail. With the Structure Builder, you can also group products, variants and collections under a single "Shop" item, separate from editorial content.

Enrich Products With Editorial Content

Once products exist in Sanity, any document can reference them. This is where the integration pays off: a guide, lookbook or landing page can link to real products, and the reference stays valid when a price changes.

// schemaTypes/lookbook.ts
defineField({
  name: 'looks',
  type: 'array',
  of: [
    defineArrayMember({
      type: 'object',
      name: 'look',
      fields: [
        defineField({ name: 'image', type: 'image', options: { hotspot: true } }),
        defineField({
          name: 'products',
          type: 'array',
          of: [{ type: 'reference', to: [{ type: 'product' }], options: { filter: '!store.isDeleted && store.status == "active"' } }],
        }),
      ],
    }),
  ],
})

The filter option keeps archived and deleted products out of the reference picker. In GROQ, resolve the references and take only what the page needs:

*[_type == "lookbook" && slug.current == $slug][0]{
  title,
  looks[]{
    _key,
    image,
    "products": products[@->store.isDeleted != true]->{
      "handle": store.slug.current,
      "gid": store.gid,
      "title": store.title,
      "image": store.previewImageUrl
    }
  }
}

The same pattern works for inline product cards inside Portable Text, using a custom block type that references a product. See Portable Text custom blocks for rendering those, and Sanity references for modelling relationships.

Build a Hydrogen Storefront

Hydrogen is Shopify's React Router-based framework, and hydrogen-sanity is Sanity's toolkit for it. It adds a Sanity client to Hydrogen's request context, caches queries with Hydrogen's cache and supports Visual Editing:

npm install hydrogen-sanity @sanity/client

Check its @shopify/hydrogen peer range before upgrading Hydrogen, because hydrogen-sanity declares support for specific Hydrogen releases.

Register the Vite plugin, then create the Sanity context in app/lib/context.ts and pass it to createHydrogenContext:

// app/lib/context.ts (excerpt)
import { createSanityContext, type SanityContext } from 'hydrogen-sanity'

const sanity = await createSanityContext({
  request,
  cache,
  waitUntil,
  client: {
    projectId: env.SANITY_PROJECT_ID,
    dataset: env.SANITY_DATASET || 'production',
    apiVersion: '2025-02-19',
    useCdn: process.env.NODE_ENV === 'production',
  },
})

A product route then queries both systems in parallel. Shopify supplies the commercial data, Sanity the editorial content, both keyed by the handle:

// app/routes/products.$handle.tsx
import { defineQuery } from 'groq'
import type { Route } from './+types/products.$handle'

const SHOPIFY_PRODUCT_QUERY = `#graphql
  query Product($handle: String!) {
    product(handle: $handle) {
      id
      title
      description
      selectedOrFirstAvailableVariant {
        id
        availableForSale
        price { amount currencyCode }
      }
    }
  }
`

const SANITY_PRODUCT_QUERY = defineQuery(`
  *[_type == "product" && store.slug.current == $handle][0]{
    body,
    gallery,
    sizeGuide->{ title, rows }
  }
`)

export async function loader({ params, context }: Route.LoaderArgs) {
  const handle = params.handle!
  const [{ product }, content] = await Promise.all([
    context.storefront.query(SHOPIFY_PRODUCT_QUERY, { variables: { handle } }),
    context.sanity.fetch(SANITY_PRODUCT_QUERY, { handle }),
  ])

  if (!product) throw new Response('Not found', { status: 404 })
  return { product, content }
}

The component renders the price and add-to-cart button from product, and the description, gallery and size guide from content. If the Sanity query returns null, for example for a brand-new product nobody has enriched yet, fall back to Shopify's own description. hydrogen-sanity also offers query with a <Query> component for Visual Editing, and useImageUrl for Sanity images. Since Hydrogen is built on React Router, the loader patterns in Sanity with Remix and React Router apply here too.

Build a Next.js Storefront

In Next.js, call the Storefront API with fetch and Sanity with next-sanity. A small helper keeps the Shopify side tidy:

// src/lib/shopify.ts
const endpoint = `https://${process.env.SHOPIFY_STORE_DOMAIN}/api/2026-10/graphql.json`

export async function shopifyFetch<T>(query: string, variables: Record<string, unknown>, tags: string[] = []) {
  const res = await fetch(endpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Shopify-Storefront-Access-Token': process.env.SHOPIFY_STOREFRONT_TOKEN!,
    },
    body: JSON.stringify({ query, variables }),
    next: { tags, revalidate: 60 },
  })
  const json = await res.json()
  if (json.errors) throw new Error(json.errors[0].message)
  return json.data as T
}
// src/app/products/[handle]/page.tsx
import { notFound } from 'next/navigation'
import { PortableText } from 'next-sanity'
import { sanityFetch } from '@/sanity/lib/live'
import { shopifyFetch } from '@/lib/shopify'
import { SANITY_PRODUCT_QUERY, SHOPIFY_PRODUCT_QUERY } from './queries'

export default async function ProductPage({ params }: { params: Promise<{ handle: string }> }) {
  const { handle } = await params
  const [{ product }, { data: content }] = await Promise.all([
    shopifyFetch<{ product: ShopifyProduct | null }>(SHOPIFY_PRODUCT_QUERY, { handle }, [`product:${handle}`]),
    sanityFetch({ query: SANITY_PRODUCT_QUERY, params: { handle } }),
  ])

  if (!product) notFound()

  return (
    <main>
      <h1>{product.title}</h1>
      <Price variant={product.selectedOrFirstAvailableVariant} />
      <AddToCart variantId={product.selectedOrFirstAvailableVariant?.id} />
      {content?.body ? <PortableText value={content.body} /> : <p>{product.description}</p>}
    </main>
  )
}

Keep the Storefront API revalidation short, or use Shopify webhooks to revalidate the product tag, because prices and stock change outside Sanity. The Sanity side refreshes through defineLive, covered in the next-sanity guide. Cart actions should always call Shopify directly, never use cached data.

For product listing pages, a single GROQ query against synced products is often enough, using store.title, store.previewImageUrl and store.priceRange for the cards. Fetch live prices only on the product page and in the cart.

Sync Content Back to Shopify

Sanity Connect can also sync in the other direction. When enabled, custom fields on products, variants and collections are written to Shopify as metafields, and custom document types become metaobjects. That's useful if part of your store still runs on a Liquid theme. Configure it in the app's Metafields tab.

Keep Everything Fresh

Because Sanity Connect writes product changes into your dataset, Shopify edits trigger the same Sanity events as editorial changes. A single revalidation path handles both: <SanityLive /> in Next.js, or a webhook that purges caches or rebuilds a static site, as described in how to use Sanity webhooks. Prices and stock read from the Storefront API need their own short cache or Shopify webhook, as above.


FAQ: Sanity and Shopify

No. Shopify still runs products, inventory, pricing, checkout, payments and orders. Sanity adds a flexible content layer for everything around the products, such as rich descriptions, editorial pages and campaigns, and your storefront combines the two.

No. Synced Shopify data lives in read-only store fields, and every sync overwrites them. Prices, inventory and variants are managed in the Shopify admin and flow into Sanity automatically.

Only where a small delay is acceptable, such as listing cards. On product pages, in the cart and at checkout, read prices and availability from the Storefront API so customers always see current values.

No. Hydrogen with hydrogen-sanity is a good fit if you want Shopify's hosting on Oxygen and its built-in commerce components, but Next.js, Astro, Nuxt or any framework that can call two APIs works just as well.

Archiving unpublishes the Sanity document, and deletion flags it with store.isDeleted. Filter on both in GROQ and in reference pickers, so editorial content never links to products customers can't buy.

Yes. Each product, variant and collection becomes a document. For large catalogues with many variants, check your plan's limits before the initial sync, or use a custom sync handler to store variants as objects on the product instead.


Conclusion

Sanity and Shopify work best when each does what it's good at. Sanity Connect keeps products, variants and collections in sync as read-only documents, editors enrich them and build pages around them in the Studio, and your storefront fetches commerce data from the Storefront API and content from GROQ, joined by handle or GID.

Decide the data split up front: anything that affects what a customer pays comes from Shopify at request time, and everything editorial comes from Sanity. Start with the Shopify Studio template, lock down the synced types, filter out archived and deleted products, and pick Hydrogen or Next.js based on where you want to host.

Tags :
Share :

Related Posts

Building a Static Site With Astro and Sanity

Building a Static Site With Astro and Sanity

To build a static site with Astro and Sanity, keep Astro's default output: 'static', configure @sanity/astro with useCdn: false, and fetch

Dive Deeper
Common Sanity Mistakes and How to Fix Them

Common Sanity Mistakes and How to Fix Them

The most common Sanity mistakes are using the CDN where you need fresh data, fetching whole documents with ..., leaving slugs unvalidated, ship

Dive Deeper
How to Create a Custom Sanity Plugin

How to Create a Custom Sanity Plugin

To create a custom Sanity plugin, wrap a piece of Studio configuration in definePlugin from the sanity package and add it to the plugins ar

Dive Deeper