How to Build a Storefront with Next.js and BigCommerce
Storyblok is the first headless CMS that works for developers & marketers alike.
In this tutorial, you will build a Storefront connecting the Storyblok, BigCommerce and Next.js Commerce systems. Take a look at the end result demo: nextjs-bigcommerce-starter.vercel.app
If you are in a hurry, you can download the whole source code of the project at Github https://github.com/storyblok/nextjs-bigcommerce-starter or jump into one of the following chapters.

Requirements
Basic understanding of Next.js
The CLI and an account of Vercel to deploy
An account on Storyblok.com to manage content
Access to the Storyblok eCommerce integration plugin
A BigCommerce account for the product management
The project in this article was developed using the following versions of these technologies:
- Next.js v10.0.5
- Nodejs v12.3.1
- Npm v6.9.0
Keep in mind that these versions may be slightly behind the latest ones.
Storyblok Space
Sign up for a free Storyblok account. Once you're signed up you have two options: Option A: you clone our storefront template with the content structure already set up. Or Option B: you create it by yourself to learn how to set up different content structure.
A: Clone the Storefront Template
To duplicate the template we set up with this tutorial, click the following button:
B: Create Storefront from Scratch
Once you're logged in, create a new space {1}. Then, name your Storefront {2}, and click the create space {3} button.

Once you create a new space, follow our eCommerce Storefront documentation for setting up your storefront in Storyblok. After you finish it, you should have some basic components set up like in the image below {1}.

eCommerce Integration
Before you can move to Next.js and build your Storefront, you must set up your Integration plugins. Follow the BigCommerce guide on how to retrieve your endpoint credentials and then the integration plugin guide on how to setup the field-type plugin. Once your integration field-type plugin is working, you should be able to select some products or categories on your Storyblok components like in the image below {1}.

Storefront with Next.js Commerce
We prepared a complete Next.js starter template with the end results on Github to show you how to connect the eCommerce plugin with the eCommerce API. For this example, we will use the BigCommerce API as our eCommerce provider. You can take a look at the finished Demo before you get started to get a better understanding of the following tutorial.
Start by cloning the Next.js Commerce starter.
$ git clone https://github.com/vercel/commerce.git nextjs-bigcommerce-starter
$ cd nextjs-bigcommerce-starter
$ npm install
Connecting Your Storyblok Space with the Starter
To connect the storefront space that was created in the storefront tutorial, get your tokens from the Storyblok Space Settings {1} under API-Keys {2}.
Fill the Location (default environment)
{4} as well as the Preview URLs
{3} fields with your localhost URL http://localhost:3000/
. Don't forget the trailing slash /
. If you have a staging or production server, the default environment can also be set to a different staging URL {4}.

After installing all of your dependencies, it's time to connect the Next.js commerce starter to Storyblok.
Duplicate the .env.template
file to a .env.local
file and add your BigCommerce credentials as well as your Storyblok preview token:
.env.local file
BIGCOMMERCE_STOREFRONT_API_URL=https://my-demo-store.mybigcommerce.com/graphql
BIGCOMMERCE_STOREFRONT_API_TOKEN=eyJ0...Q
BIGCOMMERCE_STORE_API_URL=https://api.bigcommerce.com/stores/a21234xyz
BIGCOMMERCE_STORE_API_TOKEN=927...cn
BIGCOMMERCE_STORE_API_CLIENT_ID=ab12..e2
STORYBLOK_TOKEN=zT...tt
After adding your tokens start the development server:
npm run dev
You should already see the Next.js landing page with all our BigCommerce products on our localhost server.

Changing the Real Path Field
If you open the Home Page in Storyblok, you will see a page not found
error. Since Storyblok automatically creates a /home
path from the Home story, you want to redirect that to your base URL in the Next application /
.
To fix this routing issue, change the real path of your homepage. If you open the home entry, you should already see your running localhost on port 3000 {1}. Now navigate to the config tab {2} and change the real path field to /
{3}.
Reload the page and you should see your Next.js application in the preview space. Be sure you hit the Save button before reloading.

Now if all goes well, you should see a similar preview to the one below, which shows your localhost within Storyblok.

Create a Storefront in Storyblok
This step is only necessary if you haven't duplicated our example space. If you look at the Next.js commerce home page, you'll see the following components: Three featured products, a slider of products, a text feature, and a product grid. If you followed our Storefront Setup guide you should already have featured products and product slider set. You will need to add two more components to that: the text feature and the product grid.
Adding a Text Feature Component
Go to your Content Space and open the Home Story. Click on Add Block and add a new block called Text Feature.

The Text Feature {1} should have the following schema: a headline {2} for text type, a description {3} of Richtext type and a Link {4} for the link type .
Schema of the Text Feature:
-- headline (type: Text)
-- description (type: Richtext)
-- Link (type: Link)

Adding a Product Grid Component
Click on Add Block again and add a new block called Product Grid with the following schema. If you don't know yet how to set up the eCommerce integration plugins, take a look at our integration setup plugin guide.
Schema of the Product Grid:
-- categories (type: Plugin) {1}
-- products (type: Plugin) {2}

Adding the Content
Now that all of the components are set up it's time to mirror the components that are in the Next.js starter in your Storyblok Home Story. If you go through the different components in the Next.js commerce home page, you'll find the featured products with 3 products listed: a product slider, a hero text feature, and the product grid. You should have the same components in Storyblok as you do in Next.js, similar to the image below {1}.

And the plugin should have some categories and/or products selected similar to what you see on the product grid component in the image below {1}.

Take a look at your draft JSON from the home story by clicking the arrow icon on the top right {1} and then Draft JSON {2}. You should see the associated data that is returned from the eCommerce integration plugin.

If you set up your components and plugin field-types correctly you should see some data. Storyblok always returns a story object {1} with some content {2}. In this case, the content has a body {3} with an array of blocks. These blocks are your components and your first component has an eCommerce integration plugin set up {4}. The plugin has an array of product items {5}, where each item has id, sku, name, type (can be product
or category
), image, and a short description. You will mostly need the id to query the complete information like prices or variations from BigCommerce.

Add Storyblok to Next.js Starter
Finally, it's time to connect Storyblok to the Next.js Starter. You can follow all these changes, by looking at the commits of the example repository. First, install the storyblok-react
and storyblok-js-client
packages.
Install the Storyblok Client and React Module
The Storyblok client allows you to request content from Storyblok's API, and the storyblok-react
module gives you a wrapper component that creates editable blocks in the live visual editor/composer of Storyblok.
$ npm install storyblok-js-client storyblok-react --save
Create a Storyblok File
Create the lib/storyblok.ts
file in the folder lib
. Copy and paste the code below. This loads your Storyblok client and sets it up with the preview token. Your Storyblok preview token is automatically read from the .env.local
file.
lib/storyblok.ts
import StoryblokClient from 'storyblok-js-client'
const Storyblok = new StoryblokClient({
accessToken: process.env.STORYBLOK_TOKEN,
cache: {
clear: 'auto',
type: 'memory'
}
})
export default Storyblok
Adding the Storyblok Bridge and Passing the Cache Version
To add the Storyblok Bridge, we will add a custom hook to thelib/storyblok.ts file. The useStoryblok
hook adds the necessary <script>
tag and loads the event listeners. Read more about this in our 5 minutes Next.js tutorial.
components/core/Head.tsx
import StoryblokClient, { StoryblokComponent, StoryData } from 'storyblok-js-client'
import { useEffect, useState } from 'react'
import { merge, debounce } from 'lodash'
const Storyblok = new StoryblokClient({
accessToken: process.env.STORYBLOK_TOKEN,
cache: {
clear: 'auto',
type: 'memory'
}
})
function addBridge(callback: any) {
const existingScript = document.getElementById("storyblokBridge");
if (!existingScript) {
const script = document.createElement("script");
script.src = `https://app.storyblok.com/f/storyblok-latest.js`;
script.id = "storyblokBridge";
document.body.appendChild(script);
script.onload = () => {
// once the scrip is loaded, init the event listeners
callback();
};
} else {
callback();
}
}
function initEventListeners(story: StoryData, setStory: any) {
if (window.storyblok) {
window.storyblok.init({
accessToken: process.env.STORYBLOK_TOKEN
});
window.storyblok.on(["change", "published"], () => location.reload(true));
const inputFunction = (event: any, s: any) => {
const content = s.content ? s.content : s
if (event && event.story.content._uid === content._uid) {
event.story.content = window.storyblok.addComments(event.story.content, event.story.id)
if(event.story.content.body) {
const mergedBody: any[] = event.story.content.body?.map((item: StoryblokComponent<string>) => {
let oldItem = content.body.find((itemWithData: StoryblokComponent<string>) => itemWithData._uid === item._uid)
return merge(oldItem, item)
}).filter(Boolean)
event.story.content.body = mergedBody
}
setStory(event.story)
}
}
// we will debounce the funcction since we're doing some data processing inside
window.storyblok.on('input', debounce((e) => inputFunction(e, story), 300))
}
}
export function useStoryblok(originalStory: StoryData) {
let [story, setStory] = useState(originalStory);
useEffect(() => {
// first load the bridge, then initialize the event listeners
addBridge(() => initEventListeners(story, setStory));
});
return story;
}
export default Storyblok
The event listeners in the initEventListeners function above make use of React Hooks to return the content that was changed during live editing. The function for live input is slightly adapted to what is normally used in Next.js projects. In this case it's necessary to hook into this function because you're loading additional data from BigCommerce and want to keep that data independent from what has changed in Storyblok. So after a live edit happens in Storyblok you're merging this already existent data with Storyblok's new data.
Another possibility is reloading the data from BigCommerce, but that would result in a lot more requests on every live edit and depends on your use case.
You can check these changes in the commit Add Storyblok
Add a Dynamic Component Loader
Next, you want to dynamically load the right component dependent on which components you have in Storyblok. Add a new folder components/common/DynamicComponent
with two new files: a DynamicComponent.tsx
and a index.tsx
file.
components/common/DynamicComponent/DynamicComponent.tsx
import React, { FC } from 'react'
import { Grid, Marquee, Hero } from '@components/ui'
import HomeAllProductsGrid from '@components/common/HomeAllProductsGrid'
import SbEditable, { SbEditableContent } from "storyblok-react";
import dashify from 'dashify'
interface IComponents {
[key: string]: React.ElementType,
}
interface Props {
blok?: SbEditableContent
}
const Components: IComponents = {
'product-feature': Grid,
'product-slider': Marquee,
'hero': Hero,
'product-grid': HomeAllProductsGrid,
}
const DynamicComponent: FC<Props> = ({ blok }) => {
if (blok) {
const componentName = dashify(blok.component)
if(typeof Components[componentName] !== 'undefined') {
const FoundComponent = Components[componentName]
return (<SbEditable content={blok} key={blok._uid}><FoundComponent blok={blok} /></SbEditable>)
} else {
return (<p>{componentName} is not yet defined.</p>)
}
}
return null
}
export default DynamicComponent
This component resolves the components you defined in Storyblok to components defined within the Next.js commerce starter. Next, add a index.tsx
file:
components/core/DynamicComponent/index.tsx
export { default } from './DynamicComponent'
To be able to load the component you also need to add it to the core component file. Add the DynamicComponent
to components/common/index.ts
:
...
export { default as DynamicComponent } from './DynamicComponent'
You can check these changes in the commit add Storyblok & dynamic components
Adapting the Components to Storyblok
Since you want to add the Storyblok content and have access to a live preview, you will need to adapt the four components you have in Storyblok as well as the Next.js template.
Grid.tsx
Start with the Grid component. Add a blok
prop and use the Storyblok content if it's available because it was passed into the component through the DynamicComponent
. The blok property will have all of your products, which are passed to the component.
components/ui/Grid/Grid.tsx
import cn from 'classnames'
import { FC, ReactNode, Component } from 'react'
import { SbEditableContent } from "storyblok-react"
import s from './Grid.module.css'
import { ProductCard } from '@components/product'
interface Props {
className?: string
children?: ReactNode[] | Component[] | any[]
layout?: 'A' | 'B' | 'C' | 'D' | 'normal'
products?: ReactNode[] | Component[] | any[]
variant?: 'default' | 'filled'
blok?: SbEditableContent
}
const Grid: FC<Props> = ({
className,
layout = 'A',
children,
products = [],
blok,
variant = 'default',
}) => {
const activeLayout = blok?.layout ? blok.layout : layout
const rootClassName = cn(
s.root,
{
[s.layoutA]: activeLayout === 'A',
[s.layoutB]: activeLayout === 'B',
[s.layoutC]: activeLayout === 'C',
[s.layoutD]: activeLayout === 'D',
[s.layoutNormal]: activeLayout === 'normal',
[s.default]: variant === 'default',
[s.filled]: variant === 'filled',
},
className
)
const activeProducts = blok?.product ?
blok.product?.items?.map((sbProduct: any) =>
blok.product?.fetchedItems?.find(({node}:any) =>
node.entityId === sbProduct.id)).filter(Boolean)
: products;
return <div className={rootClassName}>
{ children }
{activeProducts ? activeProducts?.map(({ node }: any) => (
<ProductCard
key={node.path}
product={node}
variant="simple"
// The first & last image are the largest one in the grid
imgWidth={1600}
imgHeight={1600}
/>
)) : null }
</div>
}
export default Grid
If you have a layout {3} in Storyblok you enter the Grid component. You can also enter the order and products selected in your eCommerce plugin so you can reorder the products in the live preview {2}.

Marquee.tsx
Similarly, add Storyblok content to the Slider component.
components/ui/Marquee/Marquee.tsx
import cn from 'classnames'
import s from './Marquee.module.css'
import { FC, ReactNode, Component } from 'react'
import Ticker from 'react-ticker'
import { ProductCard } from '@components/product'
import { SbEditableContent } from "storyblok-react"
interface Props {
className?: string
children?: ReactNode[] | Component[] | any[]
variant?: 'primary' | 'secondary'
blok?: SbEditableContent
products?: ReactNode[] | Component[] | any[]
}
const Marquee: FC<Props> = ({
className = '',
children,
products,
variant = 'primary',
blok
}) => {
const activeVariant = blok?.variant ? blok.variant : variant
const rootClassName = cn(
s.root,
{
[s.primary]: activeVariant === 'primary',
[s.secondary]: activeVariant === 'secondary',
},
className
)
const activeProducts = blok?.products ?
blok.products.items.map((sbProduct: any) =>
blok.products?.fetchedItems?.find(({node}:any) =>
node.entityId === sbProduct.id)).filter(Boolean)
: products;
return (
<div className={rootClassName}>
<Ticker offset={80}>
{({ index }) => (<div className={s.container}>
{ children }
{ activeProducts &&
activeProducts.map(({ node }: any) => (
<ProductCard
key={node.path}
product={node}
variant="slim"
imgWidth={320}
imgHeight={320}
/>
))
}
</div>)}
</Ticker>
</div>
)
}
export default Marquee
Similarly to the Grid component, you pass on the variant {3}, and the products {2}, and wrap it with your DynamicSbEditable
to make it editable.

Hero.tsx
In the Hero component, render your Richtext field with the Storyblok client and add the component fields.
components/ui/Hero/Hero.tsx
import React, { FC } from 'react'
import Storyblok from '@lib/storyblok'
import { SbEditableContent } from 'storyblok-react'
import { Container } from '@components/ui'
import { RightArrow } from '@components/icons'
import s from './Hero.module.css'
import Link from 'next/link'
interface Props {
className?: string
headline: string
description: string
blok?: SbEditableContent
}
const Hero: FC<Props> = ({ headline, description, blok }) => {
const renderedDescription = (blok && Storyblok.richTextResolver) ? Storyblok.richTextResolver.render(blok.description) : description
const bg = blok?.color ? blok.color.color : '#000'
return (
<div style={{ backgroundColor: bg }}>
<Container>
<div className={s.root}>
<h2 className="text-4xl leading-10 font-extrabold text-white sm:text-5xl sm:leading-none sm:tracking-tight lg:text-6xl">
{blok?.headline || headline}
</h2>
<div className="flex flex-col justify-between">
<div className="mt-5 text-xl leading-7 text-accent-2 text-white" dangerouslySetInnerHTML={
{__html: renderedDescription }
} />
<Link href="/blog">
<a className="text-white pt-3 font-bold hover:underline flex flex-row cursor-pointer w-max-content">
Read it here
<RightArrow width="20" heigh="20" className="ml-1" />
</a>
</Link>
</div>
</div>
</Container>
</div>
)
}
export default Hero

HomeAllProductsGrid.tsx
Finally, add your Storyblok content to the big grid component on the home page.
components/common/HomeAllProductsGrid/HomeAllProductsGrid.tsx
import { FC } from 'react'
import Link from 'next/link'
import { Grid } from '@components/ui'
import { SbEditableContent } from "storyblok-react"
import { ProductCard } from '@components/product'
import s from './HomeAllProductsGrid.module.css'
import { getCategoryPath, getDesignerPath } from '@lib/search'
interface Props {
categories?: any
brands?: any
newestProducts?: any
blok?: SbEditableContent
}
const Head: FC<Props> = ({ categories, brands, newestProducts, blok }) => {
const activeProducts = blok?.products ?
blok?.products?.items.map((sbProduct: any) =>
blok?.products?.fetchedItems?.find(({node}:any) =>
node.entityId === sbProduct.id)).filter(Boolean)
: newestProducts;
const activeCategories = blok?.products ?
blok?.categories?.items.map((sbCategory: any) =>
blok?.categories?.fetchedItems?.find((category:any) =>
category.entityId === sbCategory.id)).filter(Boolean)
: categories;
return (
<div className={s.root}>
<div className={s.asideWrapper}>
<div className={s.aside}>
<ul className="mb-10">
<li className="py-1 text-base font-bold tracking-wide">
<Link href={getCategoryPath('')}>
<a>All Categories</a>
</Link>
</li>
{activeCategories.map((cat: any) => (
<li key={cat.path} className="py-1 text-accents-8 text-base">
<Link href={getCategoryPath(cat.path)}>
<a>{cat.name}</a>
</Link>
</li>
))}
</ul>
{(brands && brands.length) ? (<ul className="">
<li className="py-1 text-base font-bold tracking-wide">
<Link href={getDesignerPath('')}>
<a>All Designers</a>
</Link>
</li>
{brands.flatMap(({ node }: any) => (
<li key={node.path} className="py-1 text-accents-8">
<Link href={getDesignerPath(node.path)}>
<a>{node.name}</a>
</Link>
</li>
))}
</ul>) : null }
</div>
</div>
<div className="flex-1">
<Grid layout="normal">
{activeProducts.map(({ node }: any) => (
<ProductCard
key={node.path}
product={node}
variant="simple"
imgWidth={480}
imgHeight={480}
/>
))}
</Grid>
</div>
</div>
)
}
export default Head
In the big grid component, you can select from a few categories {1} as well as a selection of products {2}, which you pass on to the component in Next.js. The nice thing here is that you can freely arrange the order of the categories or products from within Storyblok.

The final missing piece is to load everything with the Storyblok client. Open the Index Page file and copy the following code. First, you're loading your Home Story from Storyblok, then you will find all of the components that have a plugin defined and load the correct products or categories from the BigCommerce API and pass it along with the Storyblok data.
index.tsx
pages/index.tsx
import { Layout, DynamicComponent } from '@components/common'
import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
import { SbEditableContent } from "storyblok-react"
import Storyblok, { useStoryblok } from '@lib/storyblok'
import getDetailsFromStory from '@lib/storyblokBigCommerce'
import { getConfig } from '@framework/api'
export async function getStaticProps({
preview,
locale,
}: GetStaticPropsContext) {
const config = getConfig({ locale })
const sbParams = {
version: "draft"
}
const { data: { story }} = await Storyblok.get('cdn/stories/home', sbParams)
const copyOfStory = Object.assign({}, story)
const fullProducts = await getDetailsFromStory({ story, config, preview })
copyOfStory.content = fullProducts
return {
props: {
story: copyOfStory,
},
revalidate: 14400,
}
}
const nonNullable = (v: any) => v
export default function Home({
story,
}: InferGetStaticPropsType<typeof getStaticProps>) {
const liveStory = useStoryblok(story);
const components = liveStory.content.body.map((blok: SbEditableContent) => {
return (<DynamicComponent blok={blok} key={blok._uid} />)
})
return (
<div>
{ components }
</div>
)
}
Home.Layout = Layout
Combining API Calls from Storyblok and BigCommerce
Since the basic data of which products to display is stored in Storyblok, but the details for all the products are stored in BigCommerce, you must fetch the data for each product from BigCommerce. To do that, create a new file in the lib folder lib/storyblokBigCommerce.ts
to handle the combined logic. Add the fetched data from BigCommerce as an additional fetchedItems
array to each plugin component.
lib/storyblokBigCommerce.ts
import getAllProducts from '@framework/api/operations/get-all-products'
import getSiteInfo from '@framework/api/operations/get-site-info'
import { BigcommerceConfig } from '@framework/api'
import { SbEditableContent } from "storyblok-react"
import { StoryData } from 'storyblok-js-client'
type detailsContext = {
story?: StoryData
preview?: boolean
config?: BigcommerceConfig
}
export default async function getDetailsFromStory({ story, config, preview }: detailsContext) {
let storyContent = Object.assign({}, story?.content)
const { categories } = await getSiteInfo({ config, preview })
let finalPromises = await storyContent.body.map(async (blok: SbEditableContent) => {
const promises = await Object.keys(blok).map(async key => {
// check all the entries for plugins and fetch the items
if(blok[key].hasOwnProperty('plugin') && blok[key].plugin === 'sb-bigcommerce') {
const type = blok[key].items[0].type
const itemIds = blok[key].items.map((i: { id: any }) => i.id)
if(type === 'product' ) {
const fetchedItems = await getAllProducts({
variables: { entityIds: itemIds! },
config,
preview,
})
blok[key].fetchedItems = fetchedItems.products
return fetchedItems
} else if(type === 'category' ) {
blok[key].fetchedItems = categories.filter((c: { entityId: any; }) => itemIds.includes(c.entityId))
}
}
})
return await Promise.all(promises)
})
await Promise.all(finalPromises)
return storyContent
}
This function then is called in your getStaticProps
function in the index.tsx
page.
export async function getStaticProps({
preview,
locale,
}: GetStaticPropsContext) {
...
const fullProducts = await getDetailsFromStory({ story, config, preview })
...
}
Activating the Storyblok Live Preview Editor
Inside the pages/index.tsx
you can make use of our custom Storyblok hook in lib/storyblok.ts
to set the story object whenever you're changing something inside Storyblok, like when typing a heading.
pages/index.tsx
export default function Home({
story,
}: InferGetStaticPropsType<typeof getStaticProps>) {
const liveStory = useStoryblok(story);
const components = liveStory.content.body.map((blok: SbEditableContent) => {
return (<DynamicComponent blok={blok} key={blok._uid} />)
})
return (
<div>
{ components }
</div>
)
}
Whenever an input
event happens, you get the content with Storyblok's addComments
functions, which adds the _editable
properties to the content. Then you can go through the previously fetched data and overwrite it with the changes in Storyblok. Finally, setting it with the setContent
callback will result in a live update in the Visual Editor.
lib/storyblok.ts
function initEventListeners(story: StoryData, setStory: any) {
if (window.storyblok) {
// this initiliazies the bridge
window.storyblok.init({
accessToken: process.env.STORYBLOK_TOKEN
});
// this reloads the page when we hit save
window.storyblok.on(["change", "published"], () => location.reload(true));
// this function is called whenever an input happens in Storyblok
const inputFunction = (event: any, s: any) => {
const content = s.content ? s.content : s
if (event && event.story.content._uid === content._uid) {
event.story.content = window.storyblok.addComments(event.story.content, event.story.id)
if(event.story.content.body) {
const mergedBody: any[] = event.story.content.body?.map((item: StoryblokComponent<string>) => {
let oldItem = content.body.find((itemWithData: StoryblokComponent<string>) => itemWithData._uid === item._uid)
return merge(oldItem, item)
}).filter(Boolean)
event.story.content.body = mergedBody
}
setStory(event.story)
}
}
// we will debounce the funcction since we're doing some data processing inside
window.storyblok.on('input', debounce((e) => inputFunction(e, story), 300))
}
}
You can check out these changes in this commit: add Storyblok & dynamic components
If everything worked out you should be able to click {1} and live edit {2} the hero component and select products or categories on the other components. It should also be possible to reorder the components and products with a live preview.

Adding Next.js Preview Mode
In order to optimize the pages, you'll want to pre-render the pages at build time. This is great for performance, but not ideal for previews in Storyblok. For this reason, Next.js created a Preview-Mode feature. To add a preview mode, add these two files: pages/api/preview.ts
and pages/api/exit-preview.ts
. For the preview route, you will need to set up a MY_SECRET_TOKEN
environment variable to protect it from being previewed by anyone.
pages/api/preview.ts
import { NextApiRequest, NextApiResponse } from "next";
export default async function preview(req: NextApiRequest, res: NextApiResponse) {
// Check the secret and next parameters
if (
req.query.secret !== process.env.MY_SECRET_TOKEN ||
!req.query.slug
) {
return res.status(401).json({ message: 'Invalid token' })
}
// Enable Preview Mode by setting the cookies
res.setPreviewData({})
// Set cookie to None, so it can be read in the Storyblok iframe
const cookies = res.getHeader('Set-Cookie')
if(cookies instanceof Array) {
res.setHeader('Set-Cookie', cookies?.map((cookie: string) => cookie.replace('SameSite=Lax', 'SameSite=None')))
}
// Redirect to the entry location
let slug = req.query.slug
// Handle home slug
if(slug === 'home') {
slug = ''
}
// Redirect to the path from entry
res.redirect(`/${slug}`)
}
This preview path sets the preview cookie and rerenders the page. If you deploy your example and open it {1}, you can see that the component is showing the deployed content {2} and not the changed content in Storyblok {3}. In the request object, enter the Storyblok parameters that are automatically attached when clicking the preview button {4}.

Add a preview route to Storyblok by adding the api/preview
path to the Settings {1} under Preview URLs {2} and click Save {3}.

If you open this route {1} from within Storyblok now and pass a parameter slug and the secret token: https://my-example.vercel.app/api/preview?slug=home&token=MY_SECRET_TOKEN
you shoul be in the preview mode.
Once this route is called with the correct token, it should redirect to the page with the changed content. Now when you reload the page in Storyblok, the Next.js preview cookies should be set and you'll be in preview mode. So the current version is saved, but deployed content will not yet appear.

You also need to add an exit-preview route to leave the preview mode so that you can see the deployed content again. Just call the api/exit-preview
route to leave preview mode.
pages/api/exit-preview.ts
import { NextApiRequest, NextApiResponse } from "next";
export default async function exit(_: NextApiRequest, res: NextApiResponse) {
// Exit the current user from "Preview Mode". This function accepts no args.
res.clearPreviewData()
// set the cookies to None
const cookies = res.getHeader('Set-Cookie')
if(cookies instanceof Array) {
res.setHeader('Set-Cookie', cookies?.map((cookie: string) => cookie.replace('SameSite=Lax', 'SameSite=None')))
}
// Redirect the user back to the index page.
res.redirect('/')
}
You can also add it to your Dev Previews in the settings {1} for quick access to exiting the preview mode {2}.

For a more detailed explanation on how to load different versions from Storyblok while in Preview mode, read this tutorial.
You can check these changes in the commit Add Preview Mode
Custom Product Detail Page
You may want to create custom pages for specific products that have some additional content like on the Featured Product in the Demo example, where you add a custom heading and description.
To do this, first set up a folder called product
{1} to allow for custom Product Detail Pages. Then create a new entry {2} with the matching slug {4} and a Product content type {5} as described in the storefront setup tutorial.

If you open this entry now and the product with this slug also exists in your eCommerce API, you should see a product preview page. The visible content is loaded via the slug parameter in the URL {1}. Now you can add some additional content in Storyblok like a headline {2} and a description {3}.

In order for this Storyblok content to show up on your product pages, you need to adapt your Product pages pages/product/[slug].tsx
. First load the Storyblok Content if there is any in your getStaticProps
function. If a product exists in Storyblok, you will pass the story content to the ProductView
component.
pages/product/[slug].tsx
import type {
GetStaticPathsContext,
GetStaticPropsContext,
InferGetStaticPropsType,
} from 'next'
import { useRouter } from 'next/router'
import { Layout } from '@components/common'
import { ProductView } from '@components/product'
// Data
import { getConfig } from '@framework/api'
import getProduct from '@framework/api/operations/get-product'
import getAllPages from '@framework/api/operations/get-all-pages'
import getAllProductPaths from '@framework/api/operations/get-all-product-paths'
import Storyblok, { useStoryblok } from '@lib/storyblok'
import SbEditable from "storyblok-react"
export async function getStaticProps({
params,
locale,
preview,
}: GetStaticPropsContext<{ slug: string }>) {
const config = getConfig({ locale })
let story = {}
const { pages } = await getAllPages({ config, preview })
const { product } = await getProduct({
variables: { slug: params!.slug },
config,
preview,
})
const sbParams = {
version: "draft"
}
try {
const { data } = await Storyblok.get(`cdn/stories/product/${params!.slug}`, sbParams)
if(data.story) story = data.story
} catch(e) {
console.error(`Product ${params!.slug} doesn't exist in Storyblok`)
}
if (!product) {
throw new Error(`Product with slug '${params!.slug}' not found`)
}
return {
props: { pages, product, story },
revalidate: 200,
}
}
export async function getStaticPaths({ locales }: GetStaticPathsContext) {
const { products } = await getAllProductPaths()
return {
paths: locales
? locales.reduce<string[]>((arr, locale) => {
// Add a product path for every locale
products.forEach((product) => {
arr.push(`/${locale}/product${product.node.path}`)
})
return arr
}, [])
: products.map((product) => `/product${product.node.path}`),
fallback: 'blocking',
}
}
export default function Slug({
product,
story
}: InferGetStaticPropsType<typeof getStaticProps>) {
// @ts-ignore
const liveStory = useStoryblok(story)
const router = useRouter()
const hasStory = typeof liveStory.content !== 'undefined'
if(router.isFallback) {
return (<h1>Loading...</h1>)
} else if (hasStory) {
return (
<SbEditable content={liveStory.content} key={liveStory.content._uid}>
<ProductView product={product} story={liveStory.content} />
</SbEditable>
)
}
return (<ProductView product={product} />)
}
Slug.Layout = Layout
Then you also need to adapt the ProductView component to load the Story data if any exists, otherwise it will load the data from BigCommerce. You will also add the SbEditable
to enable the live-editing.
components/product/ProductView/ProductView.tsx
import { FC, useState } from 'react'
import cn from 'classnames'
import Image from 'next/image'
import { NextSeo } from 'next-seo'
import s from './ProductView.module.css'
import { useUI } from '@components/ui/context'
import { Swatch, ProductSlider } from '@components/product'
import { Button, Container, Text } from '@components/ui'
import usePrice from '@framework/use-price'
import useAddItem from '@framework/cart/use-add-item'
import type { ProductNode } from '@framework/api/operations/get-product'
import {
getCurrentVariant,
getProductOptions,
SelectedOptions,
} from '../helpers'
import WishlistButton from '@components/wishlist/WishlistButton'
import Storyblok from '@lib/storyblok'
interface Props {
className?: string
children?: any
product: ProductNode
story?: any
}
const ProductView: FC<Props> = ({ product, story }) => {
const addItem = useAddItem()
const { price } = usePrice({
amount: product.prices?.price?.value,
baseAmount: product.prices?.retailPrice?.value,
currencyCode: product.prices?.price?.currencyCode!,
})
const { openSidebar } = useUI()
const options = getProductOptions(product)
const [loading, setLoading] = useState(false)
const [choices, setChoices] = useState<SelectedOptions>({
size: null,
color: null,
})
const variant = getCurrentVariant(product, choices)
const addToCart = async () => {
setLoading(true)
try {
await addItem({
productId: product.entityId,
variantId: variant?.node.entityId!,
})
openSidebar()
setLoading(false)
} catch (err) {
setLoading(false)
}
}
return (
<Container className="max-w-none w-full" clean>
<NextSeo
title={product.name}
description={product.description}
openGraph={{
type: 'website',
title: product.name,
description: product.description,
images: [
{
url: product.images.edges?.[0]?.node.urlOriginal!,
width: 800,
height: 600,
alt: product.name,
},
],
}}
/>
<div className={cn(s.root, 'fit')}>
<div className={cn(s.productDisplay, 'fit')}>
<div className={s.nameBox}>
<h1 className={s.name}>{story && story?.name?.length ? story.name : product.name }</h1>
<div className={s.price}>
{price}
{` `}
{product.prices?.price.currencyCode}
</div>
</div>
<div className={s.sliderContainer}>
<ProductSlider key={product.entityId}>
{product.images.edges?.map((image, i) => (
<div key={image?.node.urlOriginal} className={s.imageContainer}>
<Image
className={s.img}
src={image?.node.urlOriginal!}
alt={image?.node.altText || 'Product Image'}
width={1050}
height={1050}
priority={i === 0}
quality="85"
/>
</div>
))}
</ProductSlider>
</div>
</div>
<div className={s.sidebar}>
<section>
{options?.map((opt: any) => (
<div className="pb-4" key={opt.displayName}>
<h2 className="uppercase font-medium">{opt.displayName}</h2>
<div className="flex flex-row py-4">
{opt.values.map((v: any, i: number) => {
const active = (choices as any)[opt.displayName]
return (
<Swatch
key={`${v.entityId}-${i}`}
active={v.label === active}
variant={opt.displayName}
color={v.hexColors ? v.hexColors[0] : ''}
label={v.label}
onClick={() => {
setChoices((choices) => {
return {
...choices,
[opt.displayName]: v.label,
}
})
}}
/>
)
})}
</div>
</div>
))}
<div className="pb-14 break-words w-full max-w-xl">
{ story && story.description && story.description.content[0].content
? (<Text html={Storyblok.richTextResolver.render(story.description)}/>)
: (<Text html={product.description} />)
}
</div>
</section>
<div>
<Button
aria-label="Add to Cart"
type="button"
className={s.button}
onClick={addToCart}
loading={loading}
disabled={!variant}
>
Add to Cart
</Button>
</div>
</div>
<WishlistButton
className={s.wishlistButton}
productId={product.entityId}
variant={variant!}
/>
</div>
</Container>
)
}
export default ProductView
With this setup, you should be able to add custom content to any product you want. This is great for adding content that you don't want or need in the eCommerce system but do need for the Storefront. A good example of this would be layout options or the order of how products appear.
You can check these changes in the commit create custom product page
By connecting two headless systems you gain a very flexible structure and can reorder any content with Storyblok’s powerful visual editor. It also allows you to decouple the visual logic from your eCommerce system.

Conclusion
Connecting two headless based systems like BigCommerce and Storyblok can greatly enhance your workflow and the user experience. With Storyblok's Visual Editor you get a great editing experience, while still keeping all your store and product information in your eCommerce system. The other big advantage is in terms of performance. With a JAMstack based storefront, you can decrease page loading times and hopefully increase purchases. If you want to learn more about why Storyblok is a great choice as a CMS for your eCommerce experiences, we recommend reading our article CMS for eCommerce.