Sanity

Introduction

In this guide you will learn how to integrate Shopstory visual builder with Sanity CMS. All the code snippets are taken from our example project which you can find here: https://github.com/shopstory-app/shopstory-examples (examples/sanity-next-13). We highly recommend installing the example project on an empty Sanity project to play around.

Important! Our examples repository is a monorepo that works correctly only with pnpm. Please read README.md before starting.

Getting started

Installation

The first step is installing Shopstory - Sanity integration package in your project.

npm install @shopstory/sanity

Add Shopstory plugin

Shopstory is just a field in your schema. You can think of Shopstory as something similar to rich text - just way more advanced. You can add Shopstory anywhere within your content model.

It all starts with adding shopstory plugin to the sanity.config.ts:

import { defineConfig } from "sanity";
import { shopstory } from "@shopstory/sanity";
import { mediaAssetSource } from "sanity-plugin-media";

const SITE_URL = "http://localhost:3000" // root URL of your site

export default defineConfig({
  // ...

  plugins: [
    // ...
    shopstory({
      accessToken: "your-shopstory-access-token",
      canvasUrl: `${SITE_URL}/shopstory-canvas`,
      locales: [
        {
          code: "en",
          isDefault: true,
        },
        {
          code: "de",
          fallback: "en",
        },
      ],
      assetSource: mediaAssetSource,
    }),
  ],
});

Let's look at shopstory parameters:

  1. accessToken - Shopstory access token required for authentication.

  2. canvasUrl - the URL to the Shopstory canvas page you created in previous chapter. In the example code above we're hardcoding SITE_URL constant value to http://localhost:3000. In real-world scenario you will probably want it to come from environment variable (local and production URLs might differ).

  3. locales - the list of locales accepted by Shopstory. The fallback parameter tells which locale should be displayed if there is no translation for the current one. In the example above if de is missing the translation then user will see en. isDefault determines the root locale which is applied when fallback can't be found. Only one locale can be the default locale.

  4. assetSource - this properly tells Shopstory which Sanity asset source should be used when selecting media from Shopstory. In this example we use mediaAssetSource from the one and only https://www.sanity.io/plugins/sanity-plugin-media

Add Shopstory field to your schema

Let's create a new document type that represents a piece of Shopstory content. We'll name it shopstoryBlock and it's available in src/schemas/shopstory-block.ts:

import { defineType, defineField } from "sanity";

export default defineType({
  name: "shopstoryBlock",
  title: "Shopstory Block",
  type: "document",
  fields: [
    defineField({
      name: "title",
      title: "Title",
      type: "string",
    }),
    defineField({
      name: "content",
      title: "Content",
      type: "shopstory",
    }),
  ],
});

That's all we need - just add shopstory field to your content model.

Add Sanity plugin to the Shopstory config

We already added Shopstory plugin to Sanity, but we must also add Sanity plugin to Shopstory. Let's edit shopstory/config.ts:

import type { Config } from "@shopstory/core";
import { sanityPlugin } from "@shopstory/sanity";
import sanityConfig from "../sanity.config";

export const shopstoryConfig: Config = {
  // ...
  plugins: [
    sanityPlugin({
      dataset: sanityConfig.dataset,
      projectId: sanityConfig.projectId,
      token: process.env.NEXT_PUBLIC_SANITY_API_TOKEN,
    }),
  ],
};

Just populate dataset, projectId and token with your Sanity credentials.

Let's build some content

It's time to build something visually. Let's create a new shopstoryBlock document and add some sections visually:

Displaying content

The last step is to render Shopstory content in your project. For the demo purpose we'll create a page that renders Shopstory Block entry by id - pages/shopstory-block/[entryId].tsx:

import { Metadata, RenderableContent, ShopstoryClient } from "@shopstory/core";
import { Shopstory, ShopstoryMetadataProvider } from "@shopstory/react";
import type { GetStaticPaths, GetStaticProps, NextPage } from "next";
import { createClient } from "next-sanity";
import sanityConfig from "../../sanity/sanity.config";
import { shopstoryConfig } from "../../shopstory/config";
import { DemoShopstoryProvider } from "../../shopstory/provider";

type ShopstoryBlockPageProps = {
  renderableContent: RenderableContent;
  meta: Metadata;
};

const ShopstoryBlockPage: NextPage<ShopstoryBlockPageProps> = (props) => {
  return (
    <DemoShopstoryProvider>
      <ShopstoryMetadataProvider meta={props.meta}>
        <Shopstory content={props.renderableContent} />
      </ShopstoryMetadataProvider>
    </DemoShopstoryProvider>
  );
};

export const getStaticPaths: GetStaticPaths = () => {
  return { paths: [], fallback: "blocking" };
};

export const getStaticProps: GetStaticProps<
  ShopstoryBlockPageProps,
  { entryId: string }
> = async (context) => {
  let { params, preview, locale = "en" } = context;

  if (!params) {
    return { notFound: true };
  }

  const rawContent = await fetchShopstoryContentJSONFromCMS(
    params.entryId,
    locale,
    !!preview
  );

  const shopstoryClient = new ShopstoryClient(shopstoryConfig, {
    locale,
    sanity: { preview },
  });
  const renderableContent = shopstoryClient.add(rawContent);
  const meta = await shopstoryClient.build();

  return {
    props: { renderableContent, meta },
    revalidate: 10,
  };
};

async function fetchShopstoryContentJSONFromCMS(
  entryId: string,
  locale: string,
  preview: boolean
): Promise<any> {
  const sanityClient = createClient({
    apiVersion: "2023-03-30",
    dataset: sanityConfig.dataset,
    projectId: sanityConfig.projectId,
    useCdn: false,
    token: process.env.NEXT_PUBLIC_SANITY_API_TOKEN,
  });

  const entryIdQuery =
    preview && !entryId.startsWith("drafts.") ? `drafts.${entryId}` : entryId;

  const documents = await sanityClient.fetch(
    `*[_id == "${entryIdQuery}"]{"content": content.${locale}}`
  );

  return documents[0].content;
}

export default ShopstoryBlockPage;

Most of the code is CMS-agnostic and relates to how Shopstory SDK works (ShopstoryClient, ShopstoryMetadataProvider, Shopstory, etc). This will be explained in the next chapter: Displaying Content.

However, there are 2 Sanity-specific pieces of code here.

First one is the body of the fetchShopstoryContentJSONFromCMS function. As you can see it's a standard GROQ call to the Sanity API. In this example we fetch the field named shopstory from our newly created shopstoryBlock document.

The second Sanity-specific piece of code is here:

 const shopstoryClient = new ShopstoryClient(shopstoryConfig, {
    locale,
    sanity: { preview }, // Sanity-specific code
  });

Here we tell the Shopstory-Sanity Plugin if the Sanity resources linked in Shopstory content (media, documents or files) should be fetched as drafts (preview: true) or as published documents (preview: false).

getStaticPaths implementation is dummy just for the sake of demo simplicity.

Further reading

Now it's time to read Displaying Content chapter and understand how Shopstory SDK works.

Last updated