Skip to main content

Getting Started with Next.js Commerce and Storyblok

Contents
    Try Storyblok

    Storyblok is the first headless CMS that works for developers & marketers alike.

    In this tutorial, we will implement Storyblok into a Next.js commerce site. Next.js Commerce is a front-end starter kit for your e-commerce site. For this tutorial, we will use BigCommerce as our eCommerce platform.

    Starting point

    We are going to follow this guide from BigCommerce. It takes you to the step-by-step guild on creating and deploying the site to Vercel. This will be our starting point and we will have a fully working e-commerce site with dummy data.

    Note:

    If you already have an existing Next.js commerce site you can still follow this guide and learn how you can implement Storyblok in your site.

    Now we can just clone the project from our own GitHub repo to our local machine to work on it.

    Hint:

    You can quickly get all the env variables from Vercel to your local machine by using the Vercel CLI: vercel env pull

    Once the project is cloned locally do the following to run the project.

    pnpm install & pnpm build # run these commands in the root folder of the mono repo
    pnpm dev # run this command in the site folder

    Create a Storyblok project

    Sign up for a free Storyblok account. Once you're signed up you can create a new space.

    https://app.storyblok.com/
    Storyblok create new space

    Storyblok create new space

    Once the project is created, we can connect our Storyblok space to our project. You can also follow our Next.js starter guide to connect this Storyblok space with our project.

    npm install @storyblok/react # run this command in the site folder
    npm install local-ssl-proxy -D # run this command in the site folder

    After installing the following packages, add this script to your package.json file in the site folder.

    "proxy": "mkcert -install && mkcert localhost && local-ssl-proxy --source 3010 --target 3000 --cert localhost.pem --key localhost-key.pem"

    The above script will proxy our dev site to run on https this is required to load Storyblok data.

    Furthermore, we have to add the Storyblok access token to the env.local file.

    NEXT_PUBLIC_STORYBLOK_ACESSTOKEN=YOUR_ACESS_KEY

    Now you can open two terminals and run two commands npm run dev and npm run proxy. Now if you go to https://localhost:3010/ you will see the site is working as if nothing changed.

    Now let's add the following code in _app.tsx file in your pages folder.

    import { storyblokInit, apiPlugin } from '@storyblok/react'
    
    storyblokInit({
      accessToken: process.env.NEXT_PUBLIC_STORYBLOK_ACESSTOKEN,
      use: [apiPlugin],
      apiOptions: { https: true },
      components: {}
    })

    We will return to this file shortly and add all of our components but for now, the empty object{} is fine.

    Breakdown of the Next.js Commerce Home Page

    Before we design our components in Storyblok let's first quickly take a look at how our current Home page is structured.

    export default function Home({
      products
    }: InferGetStaticPropsType<typeof getStaticProps>) {
      return (
        <>
          <Grid variant="filled">
            {products.slice(0, 3).map((product: any, i: number) => (
              <ProductCard
                key={product.id}
                product={product}
                imgProps={{
                  alt: product.name,
                  width: i === 0 ? 1080 : 540,
                  height: i === 0 ? 1080 : 540,
                  priority: true,
                }}
              />
            ))}
          </Grid>
          <Marquee variant="secondary">
            {products.slice(0, 3).map((product: any, i: number) => (
              <ProductCard key={product.id} product={product} variant="slim" />
            ))}
          </Marquee>
          <Hero
            headline="Dessert dragée halvah croissant."
            description="Cupcake ipsum dolor sit amet lemon drops pastry cotton candy. Sweet carrot cake macaroon bonbon croissant fruitcake jujubes macaroon oat cake. Soufflé bonbon caramels jelly beans. Tiramisu sweet roll cheesecake pie carrot cake. "
          />
          <Grid layout="B" variant="filled">
            {products.slice(0, 3).map((product: any, i: number) => (
              <ProductCard
                key={product.id}
                product={product}
                imgProps={{
                  alt: product.name,
                  width: i === 1 ? 1080 : 540,
                  height: i === 1 ? 1080 : 540,
                }}
              />
            ))}
          </Grid>
          <Marquee>
            {products.slice(3).map((product: any, i: number) => (
              <ProductCard key={product.id} product={product} variant="slim" />
            ))}
          </Marquee>
        </>
      )
    }

    If we breakdown the above code we can see we have

    1. Grid component and this component take two optional props layout and variant and within this, we have three ProductCard components.

    2. Next, we have a Marquee component and this component takes an optional prop variant and within this, similar to the above Grid it also has three ProductCard components.

    3. Lastly, we have a Hero component that just takes two props headline and description.

    Based on this now let's design our components in Storyblok.

    Create components in Storyblok

    To create new components let’s go to the Block library section in the Storyblok dashboard.

    https://app.storyblok.com/
    Storyblok Block Library

    Storyblok Block Library

    By default, four blocks come with an empty Storyblok project. We will delete all except the page block and then create new blocks to match our eCommerce homepage layout.

    Create our first component "HeroSection"

    https://app.storyblok.com/
    Storyblok create new block

    Storyblok create new block

    This will represent the existing Hero component of our project. Based on this we will have two Text field headline and description. You can match the below screenshot.

    https://app.storyblok.com/
    Create HeroSection block

    Create HeroSection block

    Second component "ProductGrid"

    Next, we are going to create a ProductGrid component. This will represent the Grid component and looped ProductCard within this. Let's create a new Nestable Block like the one above and name it ProductGrid.

    After the block is created we are going to add a new field to this Block named variant and this will be a Single-Option field

    https://app.storyblok.com/
    Storyblok editing capabilities

    Now we can edit this field and add two options. This will let us select between these two values.

    We can learn about all the props our Grid component takes in the following path site/components/ui/Grid.

    interface GridProps {
      className?: string
      children?: ReactNode
      layout?: 'A' | 'B' | 'C' | 'D' | 'normal'
      variant?: 'default' | 'filled'
    }

    Seeing that we can add the above two values as options in our variant Single-Option Field.

    https://app.storyblok.com/
    Storyblok editing capabilities

    Next, we will repeat the above step and create a new field named layout with the following options. layout?: 'A' | 'B' | 'C' | 'D' | 'normal'

    Note:

    These options and field names are not limited to what we are writing. This is just to match our current code base. You are free to add new fields and more options as you like.

    The last field for this component will be products and for this, we are going to select a new field called Plugin.

    https://app.storyblok.com/
    Storyblok editing capabilities

    Now we need to edit this field and configure it as seen in the following screenshot:

    https://app.storyblok.com/
    Storyblok editing capabilities

    Note:

    You can find the endpoint and access_token in your BigCommerce dashboard. If you are not sure you can follow this guide on how you can generate the token.

    Final component "MarqueeSlider"

    https://app.storyblok.com/
    Storyblok editing capabilities

    This one will be pretty similar to the above ProductGrid component. It will have two fields

    1. variant this will be a Single-Option Field with the following option variant: 'primary' | 'secondary'

    2. products this will be a Plugin Field, with the same option as the above ProductGrid

    Add the following components to our Home story

    Lastly, we must add our newly created components to our Home story. For this tutorial, we are going to match the components to match our code base but with Stoyblok we are free to add new components if needed.

    https://app.storyblok.com/
    Storyblok editing capabilities

    Create Storyblok components in our project

    Next, we must create the following components in our Next.js project and reference these components in the _app.txs file. Let's create a new folder named storyblok inside the site folder and create the following files.

    site/storyblok/Page.tsx
    import { storyblokEditable, StoryblokComponent, SbBlokData } from '@storyblok/react'
    interface PageProps {
      blok: SbBlokData
    }
    export default function Page({ blok }: PageProps) {
      return (
        <div {...storyblokEditable(blok)} key={blok._uid} data-test="page">
          {blok.body
            ? (blok.body as SbBlokData[]).map((nestedBlok) => (
                <div key={nestedBlok._uid}>
                  <StoryblokComponent blok={nestedBlok} />
                </div>
              ))
            : null}
        </div>
      )
    }

    site/storyblok/HeroSection.tsx
    import { Hero } from '@components/ui'
    import { storyblokEditable } from '@storyblok/react'
    interface Props {
      blok: {
        _uid: string
        headline: string
        description: string
      }
    }
    export default function HeroSection({ blok }: Props) {
      return (
        <div
          {...storyblokEditable(blok)}
          key={blok._uid}
          data-test="storyblok-hero-section"
          className="storyblok-hero-section"
        >
          <Hero headline={blok.headline} description={blok.description} />
        </div>
      )
    }

    site/storyblok/ProductGrid.tsx
    import { storyblokEditable } from '@storyblok/react'
    import SingleProduct from './SingleProduct'
    import { Grid } from '@components/ui'
    interface Props {
      blok: {
        _uid: string
        products: {
          items: PItem[]
        }
        variant: 'default' | 'filled'
        layout: 'A' | 'B' | 'C' | 'D' | 'normal'
      }
    }
    interface PItem {
      id: number
    }
    export default function ProductGrid({ blok }: Props) {
      let { variant, layout, products } = blok
      return (
        <div
          {...storyblokEditable(blok)}
          key={blok._uid}
          data-test="storyblok-product-grid"
          className="storyblok-product-grid"
        >
          <Grid variant={variant} layout={layout}>
            {products?.items?.map((item, index) => (
              <SingleProduct key={item.id} index={index} productID={item.id} />
            ))}
          </Grid>
        </div>
      )
    }

    In the above component, you can see that we are looping through the products. Instead of passing the ProductCard we are passing a component called SingleProduct and this component takes only productID.

    The reason for doing this is following

    1. Incoming product details are very limited, and there is no way to get more info for each product from Storyblok (this is by design).

    2. The data coming from Storyblok is static/cached in Storyblok CDN but for an eCommerce site always updated information is more desirable.

    This is why we are going to only take the productID from Storyblok and then we will validate the data on our end and also get all the required info for ProductCard component.

    First, create an API endpoint that gets the product info by ID

    site/pages/api/get-product/[pid].ts
    import { normalizeProduct } from '@framework/lib/normalize'
    import type { NextApiRequest, NextApiResponse } from 'next'
    export default async function handler(
      req: NextApiRequest,
      res: NextApiResponse
    ) {
      const { pid } = req.query
      if (!pid || typeof parseInt(pid as string) !== 'number') {
        res.status(400).json({
          error: true,
          message: 'Not a valid product ID',
        })
      }
      const product = await fetchClient({
        query,
        variables: {
          productId: parseInt(pid as string),
        },
      })
      let haveProduct = product?.site?.product
      if (!haveProduct) {
        res.status(500).json({ error: true, message: 'No product found' })
      }
      let parsedProduct = normalizeProduct(haveProduct)
      res.status(200).json({ product: parsedProduct })
    }
    let fetchClient = async ({ query = '', variables = {} }) => {
      let url = process.env.BIGCOMMERCE_STOREFRONT_API_URL!
      try {
        const response = await fetch(url, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            Authorization: `Bearer ${process.env
              .BIGCOMMERCE_STOREFRONT_API_TOKEN!}`,
          },
          body: JSON.stringify({
            query,
            variables,
          }),
        })
        const { data } = await response.json()
        return data
      } catch (error) {
        throw new Error('Failed to fetch API')
      }
    }
    let query = `
    query productById($productId: Int!) {
      site {
        product(entityId: $productId) {
          entityId
          name
          path
          description
          prices {
            price {
              value
              currencyCode
            }
          }
          images {
            edges {
              node {
                urlOriginal
                altText
                isDefault
              }
            }
          }
          variants(first: 250) {
            edges {
              node {
                entityId
                defaultImage {
                  urlOriginal
                  altText
                  isDefault
                }
              }
            }
          }
          productOptions {
            edges {
              node {
                __typename
                entityId
                displayName
                ...multipleChoiceOption
              }
            }
          }
        }
      }
    }
    fragment multipleChoiceOption on MultipleChoiceOption {
      values {
        edges {
          node {
            label
            ...swatchOption
          }
        }
      }
    }
    fragment swatchOption on SwatchOptionValue {
      isDefault
      hexColors
    }`

    This will give us the formatted product information that will match what we need to pass as props in ProductCard component. Next, we can create our SingleProduct component as follows.

    site/storyblok/SingleProduct.tsx
    import useSWR from 'swr'
    import { ProductCard } from '@components/product'
    export default function SingleProduct({
      index,
      productID,
    }: {
      index: number
      productID: number
    }) {
      const { data }: any = useSWR(
        `/api/get-product/${productID}`,
        (apiURL: string) => fetch(apiURL).then((res) => res.json())
      )
      if (!data || data?.error)
        return <p style={{ backgroundColor: 'black' }}>Loading...</p>
      let product = data.product
      return (
        <ProductCard
          key={product.id}
          product={product}
          imgProps={{
            alt: product.name,
            width: index === 0 ? 1080 : 540,
            height: index === 0 ? 1080 : 540,
            priority: true,
          }}
        />
      )
    }

    Lastly, we have one component left MarqueeSlider. let’s create it.

    site/storyblok/MarqueeSlider.tsx
    import { storyblokEditable } from '@storyblok/react'
    import SingleProduct from './SingleProduct'
    import { Marquee } from '@components/ui'
    interface Props {
      blok: {
        _uid: string
        products: {
          items: PItem[]
        }
        variant: 'primary' | 'secondary'
      }
    }
    interface PItem {
      id: number
    }
    export default function MarqueeSlider({ blok }: Props) {
      let { variant, products } = blok
      return (
        <div
          {...storyblokEditable(blok)}
          key={blok._uid}
          data-test="storyblok-marquee-slider"
          className="storyblok-marquee-slider"
        >
          <Marquee variant={variant}>
            {products?.items?.map((item, index) => (
              <SingleProduct key={item.id} index={index} productID={item.id} />
            ))}
          </Marquee>
        </div>
      )
    }

    Now, we have created all the components that we defined in Storyblok let's pass these into _app.tsx file.

    import { storyblokInit, apiPlugin } from '@storyblok/react'
    import Page from '../storyblok/Page'
    import ProductGrid from '../storyblok/ProductGrid'
    import HeroSection from '../storyblok/HeroSection'
    import MarqueeSlider from '../storyblok/MarqueeSlider'
    storyblokInit({
      accessToken: process.env.NEXT_PUBLIC_STORYBLOK_ACESSTOKEN,
      use: [apiPlugin],
      apiOptions: { https: true },
      components: {
        page:Page,
        ProductGrid,
        HeroSection,
        MarqueeSlider
      },
    })

    Next, go to our index.tsx page and replace this file with the following code.

    site/pages/index.tsx
    import { Layout } from '@components/common'
    import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
    import {
      useStoryblokState,
      getStoryblokApi,
      StoryblokComponent,
    } from '@storyblok/react'
    export async function getStaticProps({
      preview,
      locale,
      locales,
    }: GetStaticPropsContext) {
      let slug = 'home'
      const storyblokApi = getStoryblokApi()
      let { data } = await storyblokApi.get(`cdn/stories/${slug}`, {
        version: 'draft',
      })
      return {
        props: {
          story: data ? data.story : false,
        },
        revalidate: 60,
      }
    }
    export default function Home({
      story: initialStory,
    }: InferGetStaticPropsType<typeof getStaticProps>) {
      const story = useStoryblokState(initialStory)
      if (story?.content === (undefined || null)) {
        return <div>Loading...</div>
      }
      return <StoryblokComponent blok={story.content} />
    }
    Home.Layout = Layout

    We can already see how minimal our homepage code looks compared to before. Now let's visit https://localhost:3010/ and make sure our site works as expected.

    We now don't have hard-coded products on our home page. We can easily change the products for each section and reorder components directly from the Storyblok Visual Editor and see the live preview. Once we publish it will reflect automatically on our site without touching the code.

    https://app.storyblok.com/
    Storyblok editing capabilities
    https://app.storyblok.com/
    Storyblok editing capabilities
    https://app.storyblok.com/
    Storyblok editing capabilities

    Finally, by implementing Storyblok with our existing codebase we not only reduced the number of codes in our project but also added a lot of new possibilities. Now we can create new components and make all the parts of our site fully dynamic. We can reuse these components throughout our site to reduce development time and give the content creators much more flexibility.