Overview

Learn about Shopstory core concepts, features and architecture.

What is Shopstory?

Shopstory is a visual (or no-code) page builder. It allows non-technical folks (designers, marketers, etc) to build new pages or sections in websites without involvement of developers. So it's kind of like Squarespace or Wix, but:

  1. It's for websites with custom front-end, using modern framework like next.js, Gatsby, Remix, etc.

  2. It works inside of modern CMSes, like Sanity or Contentful, where you probably already manage content. We extend CMSes with a new field type (a visual content field) that can be added anywhere within your content model. You can think of Shopstory as super-advanced rich text field. Shopstory uses CMS native media pickers and localisation strategy.

  3. It's focused only on "content blocks" (hero banners, product grids, blog articles, popups etc) - the stuff that marketing team wants to modify most often. You can't build menu or checkout with Shopstory - we believe that these complex components should be built and maintained by developers.

  4. It's built around simplicity and constraints to make sure that editors don't accidentally break the website or use wrong design tokens (fonts, colors, buttons, etc).

Shopstory is ultra-compatible with custom-code

With Shopstory developers are still in a driver's seat. Our goal is to add visual building capabilities without compromising any advantages of custom front-end architecture:

  1. It's developers who decide where content built visually can be rendered (for example in landing page). The rest is handled as usual with code (checkout, menu, collection filtering, account etc).

  2. It's fast and low-footprint. We know how important Web Vitals are.

  3. It can be easily constrained with your design system. Fonts, colors, spacings, icons or even page containers and image aspect ratios are defined in code. Editors can only pick from a predefined set of values so they can never go "off-brand".

  4. You can use your custom components in Shopstory (think of custom buttons, product cards or already coded sections). It is an extremely powerful feature because sometimes components are too complex for marketers and should be built by developers. But marketing team should still be able to access them in a visual builder.

  5. It allows to connect 3rd party data very easily. You can connect any 3rd party source (like a products from e-commerce platform) in literally a couple of minutes. Developers have full control over how this data is displayed and searched in Shopstory UI and then how it is fetched in a build phase of your framework (like in getStaticProps from next.js). All the configuration stays in your project's code.

  6. Other CMS data (entries / assets) can be very easily linked in Shopstory content. How they're fetched is customizable.

  7. You can use code to build custom actions (like "open newsletter modal") and then make them available in Shopstory.

  8. You can customise link behaviour. Connecting custom router (like next/link) is very easy.

  9. It's possible to use custom image component (like next/image).

Below in How it works? section we explain each of these points in more detail.

Why?

In a traditional content architecture when marketer or designer comes up with a new idea then it usually must go through the following process:

  1. Developer builds new content blocks with code.

  2. Developer adds new content types in a headless CMS.

  3. Developer writes a code that fetches the data from the CMS and renders the components in the front-end.

It simply takes too long. For many businesses that don't have their own resources and rely on agencies it is a painful process requiring scoping, invoicing etc. And let's be honest, how many developers like to be bothered all the time with these small marketing requests?

With Shopstory this process can be completely avoided. Marketer or designer can build new blocks without code and push to production with no developer involvement.

How it works?

CMS

Shopstory provides the app (or plugin / extension depending on the naming convention) for the CMS of your choice. We currently support Contentful and Sanity. The purpose of the app is to extend your CMS with a new field type - Shopstory field. This field can be added anywhere within your content model. It displays a button that opens Shostory editor where you can build the content visually. Let's see how a simple hero section can be built with Shopstory in Sanity:

When you close the editor all the content is saved back to the CMS field.

As you can see in the video, Shopstory uses native media picker from the CMS. The media source is overrideable.

Shopstory is fully compatible with native CMS localisation strategy.

The architecture where visual content is represented by a CMS field is very powerful. It allows to add visual building capabilities without losing any features of your CMS and its ecosystem:

  1. Publishing, scheduling, previewing or reviewing CMS workflows are preserved.

  2. You can keep using apps from the marketplace. For example, personalisation apps from Contentful marketplace can be used easily alongside Shopstory visual content.

  3. Flexibility in content modeling is the same. Shopstory doesn't enforce any specific content types structure.

Displaying content in your front-end

Currently Shopstory supports any React framework like next, Gatsby or Remix. To render Shopstory visual content all you need to do is fetch the data from CMS and pass it through Shopstory SDK:

// in the server (for example getStaticProps):

const entry = await fetchDataFromCMS(...)
const shopstoryClient = new ShosptoryClient(
    shopstoryConfig /* shopstory configuration object */
)
const renderableContent = shopstoryClient.add(entry.shopstoryField)
const meta = await shopstoryClient.build()

// somewhere in page component

<ShopstoryMetadataProvider meta={meta}>
  <Shopstory content={renderableContent} />
</ShopstoryMetadataProvider>

Each Shopstory field in the CMS represents a piece of visual content and it's up to the developers where the content is rendered.

Is it compatible with SSR and static sites?

Yes. Shopstory is built with the static sites in mind. All the data fetching and heavy lifting happens in the server (like in next.js getStaticProps) exactly as in the traditional architecture. ShopstoryClient is not supposed to work in the browser. There is no layout shift that is so common in many visual building solutions.

Design system

In Shopstory editors can use only a predefined set of design tokens: fonts, colors, icons, spacings, page containers, etc. The tokens are set in the code in the Shopstory configuration object (that is later passed to the ShopstoryClient). Example color configuration:

export const shopstoryConfig = {
  //...
  colors: [
    {
      id: "green",
      label: "Green",
      value: "#83d1c4",
    },
    {
      id: "purple",
      label: "Purple",
      value: "#78517c",
    }
  ]
}

To learn more, read Design Tokens guide.

Custom components

You can easily extend Shopstory with your custom components. Imagine a very simple React component with border and background color that can be set via props:

import React from "react";

export type CustomComponentProps = {
  color: "white" | "purple" | "green",
  noBorder: boolean
}

export const CustomComponent : React.FC<CustomComponentProps> = (props) => {
  return <div style={{
    border: props.noBorder ? "none" : "2px solid black",
    background: props.color === "white" ? "white" : props.color === "green" ? "#83d1c4" : "#78517c",
    padding: 24,
    display: "flex",
    justifyContent: "center",
    alignItems: "center",
    textAlign: "center"
  }}>
    My custom component
  </div>
}

To make it available in Shopstory all you need to do is register this component with its schema in Shopstory configuration object and add the instance of the component to Shopstory Provider:

// shopstory/config.ts

export const shopstoryConfig: Config = {
    // ...
    components: [
      {
        id: "CustomComponent",
        label: "Custom component",
        schema: [
          {
            prop: "color",
            label: "Color",
            type: "select",
            options: [
              "white", "purple", "green"
            ]
          },
          {
            prop: "noBorder",
            label: "Disable border",
            type: "boolean"
          },
        ]
      }
    ]
}

// shopstory/provider.tsx

export const DemoShopstoryProvider : React.FC = ({ children }) => {
  return <ShopstoryProvider
    components={{
      // ...
      CustomComponent
    }}
  >
    { children }
  </ShopstoryProvider>
}

The result looks like this:

You can add your custom cards, sections, buttons, literally anything you can imagine. Most commonly added components are custom buttons or, in e-commerce, product cards.

How does Shopstory render custom components?

When setting up Shopstory in your front-end (described in Getting Started) we ask you to create an empty page called "Shopstory Canvas". The URL for this page is usually yourdomain.com/shopstory-canvas (it can be any URL you want though). While using Shopstory editor the actual content rendering takes place in an iframe that uses the Canvas Page. Thanks to this all your custom components are rendered in the context of your own website. It means that even if the component uses React Context, window, etc., it will still work perfectly without any nasty hacks. It also means that whatever you see in Shopstory preview, it's gonna look exactly the same for your visitors.

A bit of philosophy - hybrid no-code / code

Shopstory makes it easy to build things without code but at the same time makes it easy to interleave content build visually with the content built traditionally with code. It's by design.

The idea behind Shopstory is to provide the easiest possible page building experience for end-users. This assumption implies that not every block can be built with Shopstory. If Shopstory allowed for building every possible block it would mean that the tool would have to become complex and intimidating for editors (like Webflow where you need a Webflow expert or know HTML/CSS). It would also greatly increase the risk of site getting broken by a non-professional and big mature brands can't afford it.

Shopstory follows Pareto principle. We noticed that most of the content blocks in websites are simple (80% from Pareto). We want to make these 80% doable in a super simple and unbreakable way by non-technical people. Simplicity and foolproofness are the most important features for us. For the remaining 20% you should stick to the custom code. When things are complex and custom then no visual tool can take away inherent complexity from the task. We believe that complex tasks are handled best by people who are trained for years to deal with such complexity - developers.

One of our e-commerce customers recently had a challenge of building a component for checking a gift card balance. It's a small component with input, button, validation, call to the API and displaying a result in a custom modal. We think this kind of component is too complex to be built by marketers. Developer built this component with code, made it available in Shopstory as a custom component and from that point he didn't have to worry about where and how this component is displayed in the website. Marketing team could add this component in different parts of the pages, sections and layouts, without limitations.

When editor adds a button in Shopstory it must be possible to decide what happens when user clicks it. It can be done with the mechanism called actions and links. Actions are simply JS functions (like openNewsletterSignupModal) that are run on button click (button HTML element is used). Links are responsible for page transitions (a element). Similar to components, each action or link can have its own fields (schema).

In the example below we assign link and action respectively to left and right buttons:

Actions and links can be also applied to text selection. Editors can fine-tune text styling easily:

Custom routing behaviour

The powerful feature of Shopstory is that it's fully customisable in terms of routing behaviour. Whether you use next/link, react-router or any other router, it can be easily applied to link actions. All you need to do is define the Link Wrapper component:

import Link from "next/link"

const NextLink : ShopstoryLink = ({ Component, componentProps, values }) => {
  return (
    <Link href={values.pagePath} passHref={true}>
      <Component {...componentProps} />
    </Link>
  )
}

// shopstory/provider.tsx

export const DemoShopstoryProvider : React.FC = ({ children }) => {
  return <ShopstoryProvider
    // ...
    links={{
      NextLink
    }}
  >
    { children }
  </ShopstoryProvider>

Read actions and links guides to learn more.

It's a common pattern to represent website URLs by a separate CMS content type. With Shopstory it's very easy to connect your custom links or actions to CMS entries of specific type (described below).

Loading data from external sources

Imagine custom ProductCard component with one of the props being product. product is an object with data coming from e-commerce platform. Shopstory allows developers to connect any 3rd party data source and customise every aspect of it. It can be achieved by using the concept called "resources".

Adding a resource field to a custom component, link or action schema is very simple:

export const shopstoryConfig: Config = {
    // ...
  components: [
    {
      id: "ProductCard",
      label: "Product Card",
      type: "card",
      schema: [
        // ...
        {
          prop: "product",
          label: "Product",
          type: "resource", // resource field
          resourceType: "product" // resource type
        }
      ]
    }
  ],
}

At that point Shopstory doesn't know anything about product. It's a custom name for which additional behaviour must be defined. The next step is to define how editors can browse and pick products. In this example we'll use built-in item-picker widget. It displays a list of items with a search input at the top:

To connect the widget to your data source you must define widget property for your newly created resource type product:

export const shopstoryConfig: Config = {
    // ...
  resourceTypes: {
    product: {
      widget: {
        type: "item-picker",
        getItems: async (query) => {
          const products = await fetchProductsByQuery(query);
          return products.map(product => ({
            id: product.id,
            title: product.title,
            thumbnail: product.image
          }))
        },
        getItemById: async (id) => {
          const product = await fetchProductById(id);
          if (!product) {
            throw new Error("can't find product");
          }

          return {
            id: product.id,
            title: product.title,
            thumbnail: product.image
          }
        }
      }
    }
  },
}

Voilà! From now on editors can browse and select products from your e-commerce platform and assign them to custom ProductCard components.

Now the important thing: the only data stored in Shopstory data is product id! Shopstory doesn't store any resource data internally. Thanks to this there are no problems with data synchronisation and privacy. But this brings up another question - how and when the product data is fetched? The answer is that you need to define another function called fetch where you can define the custom fetching behaviour:

export const shopstoryConfig: Config = {
    // ...
  resourceTypes: {
    product: {
      fetch: async (resources) => {
        const ids = resources.map(resource => resource.id);
        const products = await getFullProductDataByIds(ids);

        return resources.map(resource => ({
          ...resource,
          value: products.find(product => product.id === resource.id)
        }))
      },
      widget: {
        // ...widget definition
      }
    }
  },
}

It's an extremely powerful mechanism, because developers are in full control over how data is fetched to their front-end. We know that there are bazzilion of different ways how you might want to fetch your data (from e-commerce platform, from cache, from CMS, from PIM, from a mix of any, with different optimisations, etc), so we don't enforce anything. You just pick your own way.

Another great thing is that Shopstory batches the fetch calls. If marketer built a page with 10 sections and each section has 2 product cards with different products, then only one fetch call with 20 products will be invoked. It allows you to fetch all these resources in one API call without overloading the server and unnecessarily increasing build time.

The fetching is done when you call await shopstoryClient.build() and it should obviously run on the server.

Connecting CMS data

Shopstory allows for linking other CMS data (entries, images, files) to your custom components, actions or links. Each Shopstory CMS plugin provides special built-in resource types for this purpose:

  1. Entries / Documents (contentful.entry, sanity.document)

  2. Assets / Media (contentful.asset, sanity.image)

  3. Files (sanity.file)

Below we're showing the example with custom coded section connected to CMS entry (Sanity document):

How the data from CMS is fetched for these built-in resource types? Out of the box Shopstory provides a default fetch function. For example the contentful.entry is fetched with Contentful JS client with include: 5 parameter. For sanity.document, because of how GROQ is designed, we fetch all field values of the requested document without any deeper references. Obviously in many cases you might need to override this behaviour. It's very easy to do with Shopstory, either by providing custom fetch function or by using transform property (both described in-depth in Resources guide).

Can I use my existing sections with Shopstory?

Yes, totally. There are multiple ways of doing this, all described in Content modeling with Shopstory.

Custom Image component

By default Shopstory renders images with <img /> tag, but we know displaying images is often more sophisticated than that. Shopstory allows to override built-in image component with the one of your own choosing. Example for next/image:

import { ShopstoryProvider, ImageProps } from "@shopstory/react";
import NextImage from "next/image";

const Image : React.FC<ImageProps> = (props) => {
  return <NextImage src={props.src} alt={props.alt} layout={"fill"} />
}

export const DemoShopstoryProvider : React.FC = ({ children }) => {
  return <ShopstoryProvider
    Image={Image}
  >
    { children }
  </ShopstoryProvider>
}

Summary

The purpose of this chapter was to show you how Shopstory works in a nutshell. Obviously we just scratched the surface here, so if you want to dive deeper, continue with Getting Started guide.

Last updated