July 10, 2019

Gatsby and Snipcart ecommerce tutorial

gatsbysnipcartecommercetutorialJAMstack

How I made a speedy e-commerce site using Gatsby and Snipcart emoji-cake.

I've been spending a lot of time playing around with Gatsby lately but one thing I hadn't tackled was making an e-commerce solution. I did a little reading around and came across Snipcart. It looked perfect for my needs and has a brilliant test environment so I decided to give it a go.

Once I got it working I decided to upload the site I made to the Gatsby starter library. At the time there were no Snipcart examples available and I thought it might help someone out. To see the code for the finished product have a look at it in the Gatsby starter library or on GitHub.

A simple shop design to use as a boilerplate...

I wanted the styling to be very simple so that people could design their own site around it, focusing instead on getting the shop functionality working properly. The demo website is available here.

Gwen's Cake Shop home page screenshot

Gwen's Cake Shop product page screenshot

What does this tutorial cover?

This won't be a complete step-by-step tutorial on making this website from scratch. Instead, I will focus on the parts of the code required to set up Snipcart. I'm therefore assuming that you understand how a basic Gatsby site is made.

There are three main things we will go through:

  1. Creating automatically-generated product pages for each item with a buy button, based on data stored in markdown files.

  2. Adding variations with different prices to your products via a dropdown list.

  3. A cart summary section in the product page header showing the number of items in the user's basket and providing a link to open the cart.

A brief introduction to Snipcart

Snipcart provides a really easy-to-use shopping cart and checkout that works really well with static, serverless sites. It doesn't direct users away from your website at any point and the amount of cart customisation is pretty much up to you. For this tutorial I have left the cart styling as it is but you can customise it endlessly using CSS!

I also think it has very reasonable pricing (the test environment is totally free to play with!).

Snipcart works using jQuery and I found that it has a few quirks when setting it up on a Gatsby/React site. Snipcart themselves have a Gatsby tutorial but I had a few issues when I followed it, perhaps things have changed slightly since it was written.

Okay, let us begin.

Get the project

The easiest way to follow this tutorial is to make a new project using the starter. You can automatically clone and deploy it to Netlify using the below button. Or you can make a new project using the starter by writing the following command.

Deploy to Netlify

gatsby new my-shop-starter https://github.com/gatsbyjs/gatsby-starter-blog

Alternatively you can just look at the code in GitHub and follow along.

The project structure

Products in markdown

To simplify matters this starter does not use a CMS for entering product data. Instead, the products are stored in markdown files in the /content/items folder at the root. Each product item is in a named folder with an index.md file and an 'image1.jpg' file. So the rainbow buttercream product looks like this for example:

<!-- content/items/rainbow/index.md-->
---
title: Rainbow buttercream
date: 2019-06-19
id: 5
price: 2.50
image: ./image1.jpg
description: A vanilla cake with rainbow buttercream icing.
customField: 
    name: Pack Size
    values: [{name: 'One Cake', priceChange: 0}, {name: 'Pack of 6', priceChange: 9.50}, {name: 'Pack of 12', priceChange: 20.00}]
---

This cake will match your pet unicorn nicely.

The product pages are generated using these files. Have a look at the gatsby-node.js file to see how that's done.

Getting the product information into the template

Each product page is generated using a template. This template can be found at src/templates/item.js. We grab the product information from the markdown file using a graphQL page query at the bottom of this template.

We need all this information to describe the product to the user but also to create a snipcart button that adds the correct product to the cart.

Basic Snipcart setup

Make a snipcart account.

The first thing to do is to make a snipcart account. You'll be asked to provide some basic details about your shop/business. There is a button for switching between the test and live environments. Make sure this is set to test.

You can find your API keys by going to your dashboard, clicking on your account info menu and then 'API Keys'. We need the public test API key.

Adding snipcart to your Gatsby site.

Installing Snipcart is very simple. All we need is a snipcart javascript file, a snipcart css file and jQuery.

The snipcart docs recommend adding these via a script in the head. However, I ran into some issues adding the scripts in this way and discovered that there is actually a gatsby plugin designed just for this purpose!

The plugin has been installed and added to the gatsby-config.js file as follows. Make sure you put your own test API key in to link it to your own snipcart account!

// gatsby-config.js file

module.exports = {
    plugins: [
        // ... other plugins
        {
            resolve: 'gatsby-plugin-snipcart',
            options: {
                //replace with own Snipcart API key
                apiKey: 'MjQ2MDY4MDctMDZkYi00ZTY0LWFlODItNzhlMmEzZDg1NTBiNjM2OTc2Nzk1NjcwMTU3MTkx',
                autopop: true,
            }
    },
        // ... other plugins
    ],
}

Adding a buy button.

To allow users to buy a product all we need to do is add a special buy button.

This is where we define the product for Snipcart. We don't need to enter any product info into the snipcart dashboard, it just grabs it from your website.

To define a buy button we just give a link/button the className "snipcart-add-item".

Have a look at the buy button used for this project. You will find it in the product page template item.js.

// src/templates/item.js

        <BuyButton
          className='snipcart-add-item'
          data-item-id={item.frontmatter.id}
          data-item-price={item.frontmatter.price}
          data-item-name={item.frontmatter.title}
          data-item-description={item.frontmatter.description}
          data-item-image={item.frontmatter.image.childImageSharp.fluid.src}
          data-item-url={"https://gatsby-snipcart-starter.netlify.com" + item.fields.slug} //REPLACE WITH OWN URL
          data-item-custom1-name={item.frontmatter.customField ? item.frontmatter.customField.name : null}
          data-item-custom1-options={this.createString(item.frontmatter.customField.values)}
          data-item-custom1-value={this.state.selected}>
          Add to basket
        </BuyButton>

This project uses styled-components and you may notice that the <BuyButton> component is simply a styled html button.

There are a number of attributes used here to define this product for snipcart.

  • data-item-id
    This is required and must be a unique ID. We set this in the markdown file so we grab it from there via graphQL.

  • data-item-price
    This is required and must be a decimal number. We set this in our markdown file and grab it from there.

  • data-item-name
    This is required and will be displayed during the checkout process. We set this in our markdown file and grab it from there.

  • data-item-description
    This isn't required but will will be displayed in the cart and during checkout so should be short. We set this in our markdown file and grab it from there.

  • data-item-image
    This acts as a small thumbnail in the cart and during checkout. If you are using gatsby-image in your website as this project does you need to make sure that you query and use the src property here! If you try to use the fluid snippet it won't work. I struggled with this for a while and there's no explanation of how to do it in the docs.

  • data-item-url
    This is required and must be the URL where the buy button is located. In this case the URL will be the individual product page. To get the correct URL I add the generated slug (grabbed via graphQL) to the website address.

The last three attributes relate to setting up custom fields and options. I will go through that in the next section.

This simple little button will allow users to click and add products to the basket. When they click the buy button the cart will open and show the added product. From here they can advance through the checkout process.

This in itself is enough to have a working e-commerce site! See, I told you it was simple.

However, I wanted to add a bit more useful functionality...

Adding custom fields and product variations.

Okay, so we have a working e-commerce site already. But what if we want to be able to offer variations of a product?

In this shop, I want to be able to offer the user the option to buy cakes individually, in packs of 6 or in packs of 12. And I want the price to change accordingly. We could do some clever things to change the attribute values on the buy button but snipcart actually has some built-in functionality to deal with this!

All snipcart needs in order to offer a set of options to the user are the three following attributes:

  • data-item-custom1-name
    This describes what the options are variations of e.g. colour, size or in our case 'Pack Size'. We set this in the markdown file and grab it from there.

  • data-item-custom1-options
    This requires a string in a specific format. The options are listed with a pipe separator and no white space. In our case we want a string that looks like this:
    "One Cake|Pack of 6|Pack of 12".
    Snipcart also allows you to set different prices for each option. To set this we simply add "[+3.00]" after the name to add £3 to the base price or "[-3.00]" to take away £3 from the base price. So a suitable string with price changes might look like this:
    "One Cake[+0]|Pack of 6[+9.50]|Pack of 12[+20.00]".
    See the next section for how we can create this string.

  • data-item-custom1-value
    This attribute will store the name of the option as a string that the user has chosen to buy. So if the user selected to buy a pack of 6 cakes it would be 'Pack of 6'.

Generating the 'data-item-custom1-options' string

So how do we generate appropriate values for the data-item-custom1-options attribute?

Lets have a look at where we set the custom fields in the markdown file:

<!-- content/items/rainbow/index.md-->
---
title: Rainbow buttercream
date: 2019-06-19
id: 5
price: 2.50
image: ./image1.jpg
description: A vanilla cake with rainbow buttercream icing.
customField:     name: Pack Size    values: [{name: 'One Cake', priceChange: 0}, {name: 'Pack of 6', priceChange: 9.50}, {name: 'Pack of 12', priceChange: 20.00}]---

This cake will match your pet unicorn nicely.

We could have just written the options out as a string here e.g. "One Cake[+0]|Pack of 6[+9.50]|Pack of 12[+20.00]". But that isn't very flexible!

Instead, we have a name for the variation type ('Pack Size') and an array of objects for the values. These objects contain a name and a priceChange.

If you take a look at the BuyButton component you can see that the value set for data-item-custom1-options is calling a function and passing in the values array.

data-item-custom1-options={this.createString(item.frontmatter.customField.values)}

Let's have a look at that function.

// src/templates/item.js

createString = (values) => {
  return values.map(option => {
    const price = option.priceChange >= 0 ? `[+${option.priceChange}]` : `[${option.priceChange}]`
    return `${option.name}${price}`
  }).join('|')
}

This function takes the array of objects, adds a '+' or '-' in front of the priceChange value depending on whether we're adding to the price or taking away, and then puts it all together with the name and separates each option with a pipe.

Voila! We have a string in the correct format that snipcart can understand!

Selecting the correct option.

The next step is to provide some way for the user to select the option they want and for us to get that option name into the data-item-custom1-value attribute.

For this, we can utilise React state.

state = {
    selected: this.props.data.markdownRemark.frontmatter.customField.values[0].name
}

The initial state is set to be the first option in the list so that it defaults to that if the user doesn't change anything. In this case 'One Cake'.

I decided to use a dropdown for selecting the appropriate option. This is just a styled < select > component.

<Dropdown
    id={item.frontmatter.customField.name}
    onChange={(e) => this.setSelected(e.target.value)}
    value={this.state.selected}>
    {item.frontmatter.customField.values.map((option) => (<DropdownOption key={option.name}>{option.name}</DropdownOption>))}
</Dropdown>

When the user selects an option a function that updates the state with the selected option is called.

setSelected = (value) => {
    this.setState({ selected: value })
}  

We can then use the this.state.selected as the value for data-item-custom1-value.

The correct option will now be added to the cart based on the selected dropdown option. The customer even has the ability to change their choice in the cart itself!

the cart modal

Updating the price on the product page

Finally, we should really update the price displayed on the product page when the user selects a variation using the dropdown. Otherwise, they have no idea how much this variation costs until they add it their basket. Which isn't a great user experience!

To do this, the displayed price calls the following function:

updatePrice = (basePrice, values) => {
    const selectedOption = values.find(option => option.name === this.state.selected)
    return (basePrice + selectedOption.priceChange).toFixed(2) 
}

The string stored in the state reflects the selected option but only the name. We need the price. So the function finds the matching object in the original values array, grabs the price and returns it. Hooray!

A cart summary in the header.

A valuable addition to any e-commerce site is an obvious link back to the cart and some information on what the cart contains.

Cart summary in the header

Adding a show cart button.

Snipcart provide a way to add a show cart button. All you need to do is add the className 'snipcart-checkout' to any link contained within a html element with the 'snipcart-summary' className. See the docs.

You can see this in use in the < HeaderMinor > component. This component provides a header for the product pages and differs from the home page header.

// src/components/Headers/HeaderMinor.js

<CartSummary className="snipcart-summary">
    <a href="#" className="snipcart-checkout"> <ShoppingCart size='40px' /></a>    <p>{this.state.items} yummy items</p>
</CartSummary>

We have a shopping cart icon that acts as a link to open up the cart modal provided by snipcart.

Tracking and displaying the number of items in the cart.

Snipcart also provide a similar className for showing the current total price and number of items in the cart. I could not get this to work properly with a multi-page website.

Instead, I decided to use the javascript API provided by snipcart to display the number of items in the cart.

Note: This took a fair bit of wrangling to get to work. There may be a better way of doing this, let me know in the comments if you find one! However, this method does seem to work well enough.

I decided to store the number of items in the state and create a simple function for updating this item quantity.

state = {
    items: 0
}

updateItemTotal = (qty) => {
    this.setState({ items: qty })
}

I identified two events where I would need to update this total:

  • When the page first loads up.
  • When the cart is changed through adding an item, removing an item, changing the quantity etc.

The snipcart public API can be accessed via window.snipcart.api and contains all the current cart information. See the docs for more info on what is available.

As the window is a browser global we need to call it in componentDidMount to prevent it from running unless it's in the browser. This is a bit of a gatsby gotcha.

I added the following code to access the number of cart items and call the `updateItemTotal()' function. Notice that I first check to see if the snipcart script has loaded up.

// src/components/Headers/HeaderMinor.js

componentDidMount() {
    if (window.Snipcart) {
        var count = window.Snipcart.api.items.count();
        this.updateItemTotal(count)
    }
}

This works nicely to make sure the item total is calculated every time the component renders (e.g. switching pages).

I discovered however that this wasn't updating properly if the page refreshed. It instead showed 0 items (the original state value). This seemed to be because the cart was not ready when componentDidMount ran after a refresh.

Snipcart have a number of events that you can subscribe to, putting code to be executed in a callback.

I simply subscribed to the cart.ready event to make sure that the code to calculate the total was not executed until the cart was loaded up.

// src/components/Headers/HeaderMinor.js

componentDidMount() {
    if (window.Snipcart) {
        var count = window.Snipcart.api.items.count();
        this.updateItemTotal(count)

        window.Snipcart.subscribe('cart.ready', (data) => {            var count = window.Snipcart.api.items.count();            this.updateItemTotal(count)        })    }
}

You need both of these. I think you need both because when you switch pages the cart remains loaded so the callback isn't reached. But I may be wrong on that.

So now the cart summary is looking good when we switch pages and when we refresh the page. But what about when we change the cart contents?

The easiest way to capture this is to use the cart.closed event. When a user adds an item to the basket the cart modal pops up and they must close it to continue. They can only delete/change items in the cart via the modal. So if we run our item counting code every time the cart modal is closed we will be sure to capture any cart changes.

// src/components/Headers/HeaderMinor.js

componentDidMount() {
    if (window.Snipcart) {
        var count = window.Snipcart.api.items.count();
        this.updateItemTotal(count)

        window.Snipcart.subscribe('cart.closed', () => {            var count = window.Snipcart.api.items.count();            this.updateItemTotal(count)        });
        window.Snipcart.subscribe('cart.ready', (data) => {
            var count = window.Snipcart.api.items.count();
            this.updateItemTotal(count)
        })
    }
}

All done! Now make yourself a cuppa emoji-coffee.

That was a fairly long post. But remember, you don't have to add loads of extra functionality. A working shop could be as simple as a single product on a one-page website with a basic buy button!

Hopefully, this guide will help you experiment more with Snipcart and Gatsby.