Structured Content 101: Find out how to make your content work harder (without your team working harder) →

Sanity Connect for Shopify

Setting up and configuring Sanity Connect for Shopify

You can install Sanity Connect on the Shopify app store to set up synchronization of your product inventory to your content lake. This will let you add custom metadata to product information, as well as reference and use product information in your other content.

Installation

How to install Sanity Connect in your Shopify store and connect it to a project:

  • Find Sanity Connect on the Shopify app store and push the Add App button
  • If you have multiple Shopify accounts, you need to choose the one that contains the store you want to add the app to
  • After choosing the store, Shopify will show you the permission Sanity Connect needs to work and its data policies. You can push the Install app button to continue.
  • The app will ask you to connect to your Sanity account. If you don't have one, you can choose to Create new account.
  • When you're logged in, you will need to connect your shop with a project on Sanity. You can choose between existing projects or create a new one (for free).
  • Select organization to list out projects under it, and then the project and dataset you want to sync to.
  • You are now ready to configure the app.

Gotcha

Once you chose Start synchronizing now, the app will add product documents to your content lake. It can be wise to test it against a non-production dataset if you haven't tried it before.

Settings

You can configure how and when Sanity Connect should synchronize products to your content lake. You can change these options at any time.

The Sanity Connect configuration screen

Direct sync

This will synchronize all products and variants as documents to your content lake. You can check the reference below to preview the data model for these documents.

Custom sync handler

This option will let you enter an endpoint that receives updates from Shopify and returns the documents you want to sync to your content lake. Typically that will be a serverless function handler where you can reshape the data and do other business logic before it's added to your content lake. You can find the shape of the payload your handler will receive below in the reference section, as well as the expected response.

Custom sync will send this payload on every update from Shopify as a POST request. You can write your custom business logic in it and update your content lake accordingly. Your handler also needs to return a response that contains information about the documents in the content lake that has been created, updated, or deleted by the custom handler.

Gotcha

The request has a 10s timeout and your handler needs to reply before that.

Gotcha

This operation will in batched when manually syncing, especially when dealing with larger catalogs.

Gotcha

Changes in product inventory (through sales) will also trigger updates to your custom handler.

Make sure to tailor your custom handler to account for how our API CDN invalidates cache on writes to non-draft documents, especially if operating on a high traffic stores with fast moving content.

Example custom function

Below is an example of a barebones custom function that will:

  • Create/update/delete products (including drafts) in the Content Lake on Shopify product operations
  • Only deal with products (variants are included as objects within products)
  • Manual sync will create and update products on your dataset, but will not delete products that have since been removed.
import client from "@sanity/client";

// Document type for all incoming synced Shopify products
const SHOPIFY_PRODUCT_DOCUMENT_TYPE = "shopify.product";

// Prefix added to all Sanity product document ids
const SHOPIFY_PRODUCT_DOCUMENT_ID_PREFIX = "product-";

// Enter your Sanity studio details here.
// You will also need to provide an API token with write access in order for this
// handler to be able to create documents on your behalf.
// Read more on auth, tokens and securing them: https://www.sanity.io/docs/http-auth
const sanityClient = client({
  apiVersion: "2021-10-21",
  dataset: process.env.SANITY_DATASET,
  projectId: process.env.SANITY_PROJECT_ID,
  token: process.env.SANITY_ADMIN_AUTH_TOKEN,
  useCdn: false,
});

/**
 * Sanity Connect sends POST requests and expects both:
 * - a 200 status code
 * - a response header with `content-type: application/json`
 * 
 * Remember that this may be run in batches when manually syncing.
 */
export default async function handler(req, res) {
  // Next.js will automatically parse `req.body` with requests of `content-type: application/json`,
  // so manually parsing with `JSON.parse` is unnecessary.
  const { body, method } = req;

  // Ignore non-POST requests
  if (method !== "POST") {
    return res.status(405).json({ error: "Method not allowed" });
  }

  try {
    const transaction = sanityClient.transaction();
    switch (body.action) {
      case "create":
      case "update":
      case "sync":
        await createOrUpdateProducts(transaction, body.products);
        break;
      case "delete":
        const documentIds = body.productIds.map((id) =>
          getDocumentProductId(id)
        );
        await deleteProducts(transaction, documentIds);
        break;
    }
    await transaction.commit();
  } catch (err) {
    console.error("Transaction failed: ", err.message);
  }

  res.status(200).json({ message: "OK" });
}

/**
 * Creates (or updates if already existing) Sanity documents of type `shopify.product`.
 * Patches existing drafts too, if present.
 *
 * All products will be created with a deterministic _id in the format `product-${SHOPIFY_ID}`
 */
async function createOrUpdateProducts(transaction, products) {
  // Extract draft document IDs from current update
  const draftDocumentIds = products.map((product) => {
    const productId = extractIdFromGid(product.id);
    return `drafts.${getDocumentProductId(productId)}`;
  });

  // Determine if drafts exist for any updated products
  const existingDrafts = await sanityClient.fetch(`*[_id in $ids]._id`, {
    ids: draftDocumentIds,
  });

  products.forEach((product) => {
    // Build Sanity product document
    const document = buildProductDocument(product);
    const draftId = `drafts.${document._id}`;

    // Create (or update) existing published document
    transaction
      .createIfNotExists(document)
      .patch(document._id, (patch) => patch.set(document));

    // Check if this product has a corresponding draft and if so, update that too.
    if (existingDrafts.includes(draftId)) {
      transaction.patch(draftId, (patch) =>
        patch.set({
          ...document,
          _id: draftId,
        })
      );
    }
  });
}

/**
 * Delete corresponding Sanity documents of type `shopify.product`.
 * Published and draft documents will be deleted.
 */
async function deleteProducts(transaction, documentIds) {
  documentIds.forEach((id) => {
    transaction.delete(id).delete(`drafts.${id}`);
  });
}

/**
 * Build Sanity document from product payload
 */
function buildProductDocument(product) {
  const {
    featuredImage,
    id,
    options,
    productType,
    priceRange,
    status,
    title,
    variants,
  } = product;
  const productId = extractIdFromGid(id);
  return {
    _id: getDocumentProductId(productId),
    _type: SHOPIFY_PRODUCT_DOCUMENT_TYPE,
    image: featuredImage?.src,
    options: options?.map((option, index) => ({
      _key: String(index),
      name: option.name,
      position: option.position,
      values: option.values,
    })),
    priceRange,
    productType,
    status,
    title,
    variants: variants?.map((variant, index) => {
      const variantId = extractIdFromGid(variant.id);
      return {
        _key: String(index),
        compareAtPrice: Number(variant.compareAtPrice || 0),
        id: variantId,
        inStock: !!variant.inventoryManagement
          ? variant.inventoryPolicy === "continue" ||
            variant.inventoryQuantity > 0
          : true,
        inventoryManagement: variant.inventoryManagement,
        inventoryPolicy: variant.inventoryPolicy,
        option1: variant?.selectedOptions?.[0]?.value,
        option2: variant?.selectedOptions?.[1]?.value,
        option3: variant?.selectedOptions?.[2]?.value,
        price: Number(variant.price || 0),
        sku: variant.sku,
        title: variant.title,
      };
    }),
  };
}

/**
 * Extract ID from Shopify GID string (all values after the last slash)
 * e.g. gid://shopify/Product/12345 => 12345
 */
function extractIdFromGid(gid) {
  return gid?.match(/[^\/]+$/i)[0];
}

/**
 * Map Shopify product ID number to a corresponding Sanity document ID string
 * e.g. 12345 => product-12345
 */
function getDocumentProductId(productId) {
  return `${SHOPIFY_PRODUCT_DOCUMENT_ID_PREFIX}${productId}`;
}

When to synchronize

Sync data automatically: Automatically sync whenever you save products. Note: The sync will update the Shopify information for both published and draft documents. An update is typically available in your content lake after a couple of seconds.

Sync manually: There will no automatic sync, and you'll have to go into the Sanity Connect settings to trigger a synchronization manually.

Gotcha

Sanity Connect will do an initial synchronization once you choose one of these options.

Sync collections

The Sanity Connect app can also sync collections data. It's turned off by default, but you can enable it with the checkbox in the advanced settings.

Reference Studio for Shopify

You can install a production-ready reference studio that's set up with a great editor experience by running this command in your local shell. Replace PROJECT_ID and DATASET_NAME with those from the project your Shopify store is connected to:

npx @sanity/cli@shopify init --template shopify --project PROJECT_ID --dataset DATASET_NAME

You'll find comprehensive documentation for this studio in its README.md.

The Shopify reference studio

Integrating with an existing studio

If you already have set up a studio and want to surface the product and variant documents, then you can go to the reference studio's source code and copy the schema files from there.

Using Sanity Connect with Hydrogen

Hydrogen is Shopify's React-based framework that comes with a lot of building blocks that help you build custom storefronts with cart handling, checkout, and more.

The best place to start exploring what you can do with Hydrogen and Sanity is by checking out the demo starter. Its README.md contains an introduction and documentation of how it works. Inside this starter, you'll find a production-grade example of how to combine data from Shopify and Sanity.

Reference

You will find all data from Shopify under the store property. Typically, you want to set these fields as readOnly or hidden in your Sanity Studio schemas.

Product document

This is an example of a product document. Note the array of references to variant documents.

{
  "_createdAt": "2022-05-18T07:45:26Z",
  "_id": "shopifyProduct-7696133062907",
  "_rev": "sERZ3ZJ9MtNiP4BmT5zftt",
  "_type": "product",
  "_updatedAt": "2022-08-31T21:41:10Z",
  "body": [],
  "store": {
    "createdAt": "2022-05-12T17:39:51+01:00",
    "descriptionHtml": "",
    "gid": "gid://shopify/Product/7696133062907",
    "id": 7696133062907,
    "isDeleted": false,
    "options": [
      {
        "_key": "Color",
        "_type": "option",
        "name": "Color",
        "values": [
          "Blue",
          "Ecru",
          "Pink"
        ]
      }
    ],
    "previewImageUrl": "https://cdn.shopify.com/s/files/1/0639/3285/8619/products/Green_1.jpg?v=1655598944",
    "priceRange": {
      "maxVariantPrice": 25.5,
      "minVariantPrice": 25
    },
    "productType": "",
    "slug": {
      "_type": "slug",
      "current": "soap-dish"
    },
    "status": "active",
    "tags": "",
    "title": "AUTOGRAF Soap Dish",
    "variants": [
      {
        "_key": "c8b492e1-3c24-527d-bffd-accc634177c7",
        "_ref": "shopifyProductVariant-43068621422843",
        "_type": "reference",
        "_weak": true
      },
      {
        "_key": "9128c62c-f887-594c-b9b8-ddaaf850ce84",
        "_ref": "shopifyProductVariant-43068621455611",
        "_type": "reference",
        "_weak": true
      },
      {
        "_key": "5d861cdf-bcfe-5781-81dd-d62db159442b",
        "_ref": "shopifyProductVariant-43068621488379",
        "_type": "reference",
        "_weak": true
      }
    ],
    "vendor": "Lucy Holdberg"
  }
}

Variant document

This is an example of a variant document.

{
  "_createdAt": "2022-05-27T08:49:54Z",
  "_id": "shopifyProductVariant-43068621422843",
  "_rev": "sERZ3ZJ9MtNiP4BmT5zftt",
  "_type": "productVariant",
  "_updatedAt": "2022-08-31T21:32:01Z",
  "store": {
    "compareAtPrice": 35,
    "createdAt": "2022-05-27T09:49:52+01:00",
    "gid": "gid://shopify/ProductVariant/43068621422843",
    "id": 43068621422843,
    "inventory": {
      "isAvailable": true,
      "management": "SHOPIFY",
      "policy": "CONTINUE"
    },
    "isDeleted": false,
    "option1": "Blue",
    "option2": "",
    "option3": "",
    "previewImageUrl": "https://cdn.shopify.com/s/files/1/0639/3285/8619/products/Blue_1.jpg?v=1655598950",
    "price": 25.5,
    "productGid": "gid://shopify/Product/7696133062907",
    "productId": 7696133062907,
    "sku": "AGSD_BLUE",
    "status": "active",
    "title": "Blue"
  }
}

Collection document

This is an example of a collection document:

{
  "_createdAt": "2022-06-07T10:00:11Z",
  "_id": "shopifyCollection-396461834491",
  "_rev": "0penztPZlC32Cv2tesREk7",
  "_type": "collection",
  "_updatedAt": "2022-08-26T15:07:57Z",
  "store": {
    "createdAt": "2022-08-26T15:07:56.895Z",
    "descriptionHtml": "",
    "disjunctive": false,
    "gid": "gid://shopify/Collection/396461834491",
    "id": 396461834491,
    "imageUrl": "https://cdn.shopify.com/s/files/1/0639/3285/8619/collections/BLOMST_print.jpg?v=1655599663",
    "isDeleted": false,
    "rules": [
      {
        "_key": "7803ad21-682e-56b6-ae2a-4d380d0d120c",
        "_type": "object",
        "column": "TYPE",
        "condition": "Poster",
        "relation": "CONTAINS"
      }
    ],
    "slug": {
      "_type": "slug",
      "current": "prints"
    },
    "sortOrder": "BEST_SELLING",
    "title": "Prints"
  }
}

Below are the data types for the properties of a collection document:

export type ShopifyDocumentCollection = {
  _id: `shopifyCollection-${string}` // Shopify product ID
  _type: 'collection'
  store: {
    id: number
    gid: `gid://shopify/Collection/${string}`
    createdAt: string
    isDeleted: boolean
    descriptionHtml: string
    imageUrl?: string
    rules?: {
      _key: string
      _type: 'object'
      column: Uppercase<string>
      condition: string
      relation: Uppercase<string>
    }[]
    disjunctive?: boolean
    slug: {
      _type: 'slug'
      current: string
    }
    sortOrder: string
    title: string
    updatedAt: string
  }
}

Custom webhook sync payload

If you use the custom webhook sync, your handler will receive the shape described Product (and Collection if enabled) below. You can still use JavaScript or any other programming language in your custom handler even though we describe the payload using TypeScript syntax.

export type Product = {
  id: `gid://shopify/ProductVariant/${string}`
  title: string
  description: string
  descriptionHtml: string
  featuredImage?: ProductImage
  handle: string
  images: ProductImage[]
  options: ProductOption[]
  priceRange: ProductPriceRange
  productType: string
  tags: string[]
  variants: ProductVariant[]
  vendor: string
  status: 'active' | 'archived' | 'draft' | 'unknown'
  publishedAt: string
  createdAt: string
  updatedAt: string
}
export type ProductImage = {
  id: `gid://shopify/ProductImage/${string}`
  altText?: string
  height?: number
  width?: number
  src: string
}
export type ProductOption = {
  id: `gid://shopify/ProductOption/${string}`
  name: string
  position: number
  values: string[]
}
export type ProductPriceRange = {
  minVariantPrice?: number
  maxVariantPrice?: number
}
export type ProductVariant = {
  id: `gid://shopify/ProductVariant/${string}`
  title: string
  compareAtPrice?: number
  barcode?: string
  inventoryPolicy: string
  inventoryQuantity: number
  inventoryManagement: string
  position: number
  requiresShipping: boolean
  sku: string
  taxable: boolean
  weight: number
  weightUnit: string
  price: string
  createdAt: string
  updatedAt: string
  image?: ProductImage
  product: {
    id: `gid://shopify/Product/${string}`
    status: 'active' | 'archived' | 'draft' | 'unknown'
  }
  selectedOptions: {
    name: string
    values: string[]
  }[]  
}
export type Collection = {
  id: `gid://shopify/Collection/${string}`
  createdAt: string
  handle: string
  descriptionHtml: string
  image?: CollectionImage
  rules?: {
    column: string
    condition: string
    relation: string
  }[]
  disjunctive?: boolean
  sortOrder: string
  title: string
  updatedAt: string
}
export type CollectionImage = {
  altText: string
  height?: number
  width?: number
  src: string
}

// When products are created, updated or manually synced
export type payloadProductsSync = {
  action: 'create' | 'update' | 'sync'
  products: Product[]
}

// When products are deleted
export type payloadProductsDelete = {
  action: 'delete'
  productIds: number[]
}

// When collections are created, updated or manually synced
export type payloadCollectionsSync = {
  action: 'create' | 'update' | 'sync'
  collections: Collection[]
}

// When collections are deleted
export type payloadCollectionsDelete = {
  action: 'delete'
  collectionIds: number[]
}

export type requestPayload = payloadProductsDelete | payloadProductsSync | payloadCollectionsDelete | payloadCollectionsSync

Was this article helpful?