Building a rich e-commerce experience on the Jamstack with TakeShape, Shopify, and Gatsby.js

Allan Lasser

UX Engineer, TakeShape

Using the TakeShape Mesh, we can combine our products and content to create a rich shopping experience that goes beyond your typical store templates.

I want you to picture an online store. What do you see? Maybe a hero image, a grid of products, or — sigh — a carousel. So many e-commerce sites look the same since they're using the same pre-made templates and solutions. They're easy to set up, but they also don't inspire. Let's change that!

In this guide we're going to break free of Shopify's limits and build a unique, content-driven e-commerce experience with Shopify, TakeShape and Gatsby. By connecting Shopify and TakeShape, we can extend our e-commerce backend with all different kinds of data. Then, by querying TakeShape from Gatsby, we can easily generate a static static from React components. Taken together, it's the best way to create a one-of-a-kind storefront on the Jamstack.

Now, what should we build?

Let's build a lookbook

Lookbooks are a powerful tool for e-commerce, since they help customers imagine our products in context. They're closer to a fashion magazine than a mail-order catalog, allowing our brand to tell a story and strike a mood. They can also show off multiple products at once, making them a powerful tool for increasing sales.

For example, Uniqlo's lookbook provides an alternate way to browse their different products based on what catches our eye. If we click on any photo, we'll be able to see the specific products the model is wearing.

This would be tricky to build just in Shopify, since the tool is really geared around rendering one product at a time. Connecting Shopify with TakeShape makes it easy to associate specific products with content, then query the combined data with GraphQL. By building our storefront with Gatsby and deploying to Netlify, we can quickly deploy a fast, inexpensive storefront that's built in React. In the end, we'll combine all these tools' strengths and end up with a one-of-a-kind Jamstack storefront. This is what it'll look like:

Let's get building!

Create a custom e-commerce API with TakeShape and Shopify

If you'd like to skip this section and start coding, you can simply use the Deploy to TakeShape button to instantly set up a project following our pattern.

First, we'll create the data we'll use to power our lookbook. We'll start by making a project in TakeShape, then connecting it to Shopify. Finally, we'll model some content.

If we don't already have a TakeShape account, it's free to sign up and create projects on the Developer plan!

We'll start by creating a new, blank project. Again, we can name this whatever we want, but here we'll call it "Shopify Lookbook". After creating our project we'll be taken to the setup screen.

Connect to Shopify

Since we want to use data from Shopify, let's connect our TakeShape project to our Shopify store. From the project menu, select "Services", then use the "Connect Service" button at the top of the list.

Pick the service we'd like to connect—in this case, Shopify. we'll name our new service and provide our Shopify store's URL. When we're done, click the Save button.

When we connect to Shopify for the first time, we'll be asked to authorize TakeShape as a Shopify App. To approve the installation, click the Install app button. 

After we install the app, we'll be taken back to our list of services in TakeShape. Now we'll see our shop is connected! Click on the connected service to see more about it.

Now when we're modeling our content, we can incorporate data from Shopify, too.

Build our lookbook API

Now that Shopify is connected, we'll now be able to use data from Shopify directly in our API. We'll be building a simple API that provides us with Looks: an object that joins an image with a set of Shopify products.

First, we'll create the Product object by clicking "Add new Content Type" to open the Form Studio. We should see our Shopify store as an available field in the palette. Go ahead and drag it into our Product. Select the Project type from the dropdown in the right sidebar, then save. 

Our Shopify products should be pulled right into our project. If they aren't, go ahead and create new Product entries in TakeShape for each Shopify item we'll feature in the lookbook.

Next, we'll create the Look object. We'll add three fields to this:

  1. An Asset field to store the photo of our look.
  2. An optional Paragraph text field that holds a description of the look.
  3. A Relationship field that connects to the Product shape we just made.

We'll save the new object when we're done, then start creating looks for our book! By the end, we'll have three looks, each of which is associated with two products.

Now that we have our TakeShape project created, connected to Shopify, and combined our Shopify products with data, we have an API to power our lookbook project.

Building the lookbook with Gatsby and TakeShape

If you want to skip this step, simply use our GitHub template, then either clone it to your local machine or deploy it straight to Netlify.

To build our lookbook, we'll quickly scaffold out a new Gatsby project, set up data fetching from TakeShape on our homepage, and then create some components to render out our data. Let's get going!

Scaffold our Gatsby project

We'll start by getting a bare-bones Gatsby project working. Before we begin, make sure you install the Gatsby CLI:

npm install -g gatsby-cli

Using the Gatsby CLI, create a new Gatsby project based on their "hello world" starter project:

gatsby new shape-shop-gatsby https://github.com/gatsbyjs/gatsby-starter-hello-world

After the project downloads and installs, cd into the new project and run the development server:

cd shape-shop-gatsby
gatsby develop

Open your browser and view the starter project at http://localhost:8000. We'll see the text "Hello World" on an otherwise blank canvas. Perfect!

Use our Gatsby plugin to query your TakeShape project

Install TakeShape's Gatsby plugin:

npm install --save gatsby-source-takeshape

Create a TakeShape API Key and then create a .env file in your project root:

TAKESHAPE_TOKEN=<api key>
TAKESHAPE_PROJECT=<project id>

Add the TakeShape to your list of Gatsby plugins, providing the secrets in your .env file, in gatsby-config.js:

require('dotenv').config()

module.exports = {
  siteMetadata: {
    title: 'Shape Shop Lookbook'
  },
  plugins: [
    {
      resolve: 'gatsby-source-takeshape',
      options: {
        apiKey: process.env.TAKESHAPE_TOKEN,
        projectId: process.env.TAKESHAPE_PROJECT,
      },
    },
  ],
}

To make sure our configuration is correct, we'll start with updating the homepage to use a simple query on our project. We'll update our src/pages/index.js file to look like this:

import React from "react"
import {graphql} from 'gatsby'

export const query = graphql`
  query {
    takeshape {
      looks: getLookList {
        total
      }
    }
  }
`

const IndexPage = ({data}) => <>{data.takeshape.looks.total} looks</>

export default IndexPage

We'll save and start our Gatsby site with npm run develop. Now at http://localhost:8000 we'll see a simple homepage that tells us how many looks we have in our project.

Looking good

Now that everything's working, we can really take our project to the next level. We'll render out styled list of looks with a list of products alongside them.

First, we need to install the gatsby-image module:

npm install --save gatsby-image

TakeShape's plugin works great with this plugin, provided that we follow one simple rule: we need to either include a fluid or a fixed property in our query underneath the path property. That fluid or fixed property is then provided as an argument to Gatsby Image's Img component. We explain this in more detail on the gatsby-source-takeshape plugin page.

Next, we'll update our query to include all the data we want to fetch from TakeShape—including data coming from our connected Shopify account! Our query in src/pages/index.js should look like this:

export const query = graphql`
  fragment image on TS_Asset {
    title
    description
    path
  }

  fragment product on TS_Product {
    _id
    name
    image {
      ...image
      fluid(maxWidth: 400, maxHeight: 400) {
        ...GatsbyTakeShapeImageFluid
      }
    }
    shopifyProductId: takeshapeIoShopId
    shopifyProduct: takeshapeIoShop {
      title
      variants(first: 1) {
        edges {
          node {
            price
          }
        }
      }
    }
  }

  query HomepageQuery {
    takeshape {
      looks: getLookList {
        items {
          _id
          name
          text
          photo {
            ...image
            fluid(maxWidth: 900, maxHeight: 1200) {
              ...GatsbyTakeShapeImageFluid
            }
          }
          products {
            ...product
          }
        }
      }
    }
  }
`

Then, we'll update our components in src/pages/index.js to comprehensively render out the returned data:

import Img from 'gatsby-image'
import styles from '../styles/index.module.css'

export function getProductPrice(product) {
  const { variants } = product;
  const firstNode = variants?.edges[0]?.node;
  const price = firstNode?.price;
  return price;
}

const Product = ({ _id, name, image, shopifyProductId, shopifyProduct }) => {
  const { title } = shopifyProduct;
  const price = getProductPrice(shopifyProduct);
  return (
    <div className={styles.product}>
      {image && (
        <Img className={styles.productImage} fluid={image.fluid} />
      )}
      <div className={styles.productLabel}>
        <p className={styles.productTitle}>{title}</p>
        {price && <p className={styles.productPrice}>${price}</p>}
      </div>
    </div>
  );
};

const Look = ({ photo, text, products }) => {
  return (
    <div className={styles.look}>
      <div className={styles.photo}>
        <Img fluid={photo.fluid} />
      </div>
      <div className={styles.details}>
        <p classname={styles.text}>{text}</p>
        <div>
          {products.map((product) => (
            <Product {...product} key={product._id} />
          ))}
        </div>
      </div>
    </div>
  );
};

const IndexPage = ({data}) => (
  <div className={styles.container}>
    {data.takeshape.looks.items.map((look) => (
      <Look key={look._id} {...look} />
    ))}
  </div>
)

We'll also need to create the CSS module file we imported (Gatsby comes with out-of-the-box support for CSS modules). In a new folder src/styles, we'll create a file named index.module.css that looks like this:

.container {
  min-height: 100vh;
  padding: 0 0.5rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  scroll-snap-type: y proximity;
}

.container h1 {
  margin: 2em;
}

.look {
  width: 100%;
  max-width: 80em;
  margin: 1em auto;
  padding: 1em;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  scroll-snap-align: start;
}

.look:nth-of-type(even) {
  flex-direction: row-reverse;
}

.photo {
  flex: 1 1 28em;
  max-width: 64em;
  margin: 1em;
}

.photo img {
  display: block;
  width: 100%;
  border-radius: 4px;
}

.details {
  flex: 1 1 24em;
  margin: 3em;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.text {
  font-family: "Georgia", "serif";
  font-size: 1.2em;
  line-height: 1.6;
}

.product {
  display: flex;
  flex-direction: row;
  border: 1px solid rgba(0, 0, 0, 0.15);
  border-radius: 4px;
  overflow: hidden;
  align-items: flex-start;
  padding: 0 1em 0 0;
  margin: 1em 0;
}

.productImage {
  flex: 0 0 auto;
  width: 100%;
  max-width: 4em;
  display: block;
  height: auto;
  margin: 1em;
  border-radius: 4px;
}

.productLabel {
  flex: 1 1 auto;
  padding: 1em;
  line-height: 1.6;
}

.productTitle {
  font-weight: bold;
  margin: 0;
}

.productPrice {
  margin: 0;
}

When we save, our development site should reload. Now we'll have a lovely, laid out set of looks and their associated products!

Standalone product pages

Next, we'll create some standalone pages for the products listed and link to them from our looks.

First, stop the Gatsby development server if it's running, since we'll be updating some configuration files.

We'll add a file named gatsby-node.js to our project root to tell Gatsby to generate static pages for each of our products:

const path = require(`path`)
const routes = require(`./src/routes`)

exports.createPages = async ({ actions, graphql }) => {
  const { data } = await graphql(`
    query {
      takeshape {
        products: getProductList {
          items {
            _id
            name
          }
        }
      }
    }
  `)

  data.takeshape.products.items.forEach(({ _id, name }) => {
    actions.createPage({
      path: routes.products(name),
      component: path.resolve(`./src/components/Product.js`),
      context: {
        productId: _id,
      },
    })
  })
}

Next, we'll add the referenced file src/routes.js to provide our route generation function:

const slugify = require(`slugify`)

exports.products = function (name) {
  const slug = slugify(name.toLowerCase())
  return `/products/${slug}/`
}

Note that we're using the slugify module to consistently transform the product's name into a URL-friendly string. Makes sure to install it!

npm install --save slugify

Now we'll add the file for the React component we designate in the createPagefunction above. In a new folder src/components, we create a file Product.js:

import React from 'react'
import {graphql} from 'gatsby'
import Img from 'gatsby-image'
import styles from '../styles/product.module.css'
import {getProductPrice} from '../pages/index'

const Product = ({ image, product, productId }) => {
  const { title } = product;
  const price = getProductPrice(product);
  return (
    <div className={styles.container}>
      {image && (
        <div className={styles.image}>
          <Img fluid={image.fluid} />
        </div>
      )}
      <div className={styles.text}>
        <h2>{title}</h2>
        <p>${price}</p>
        <div
          className={styles.description}
          dangerouslySetInnerHTML={{ __html: product.descriptionHtml }}
        />
      </div>
    </div>
  );
};

export default ({data}) => {
  const product = data.takeshape.product
  return (
    <>
      <Product {...product} />
    </>
  )
}

export const query = graphql`
  query($productId: ID!) {
    takeshape {
      product: getProduct(_id: $productId) {
        image {
          path
          title
          description
          fluid(maxWidth: 800, maxHeight: 1200) {
            ...GatsbyTakeShapeImageFluid
          }
        }
        productId: takeshapeIoShopId
        product: takeshapeIoShop {
          title
          descriptionHtml
          variants(first: 1) {
            edges {
              node {
                price
              }
            }
          }
        }
      }
    }
  }
`

We'll also want to style it a bit, so we add a CSS module src/styles/product.module.css:

.container {
  display: flex;
  max-width: 64em;
  margin: 2em auto;
}

.image {
  flex: 1 1 16em;
  padding: 1em;
}

.image img {
  display: block;
  width: 100%;
  border-radius: 4px;
}

.text {
  flex: 1 1 16em;
  padding: 1em;
}

.description {
  line-height: 1.4;
  opacity: 0.8;
  margin: 2em 0;
}

Finally, we'll use Gatsby's Link component to connect the products on our homepage to their unique product pages. Back in src/pages/index.js, we'll update our imports and our Product component like so:

import {Link, graphql} from 'gatsby'
import routes from '../routes'

const Product = ({ _id, name, image, shopifyProductId, shopifyProduct }) => {
  const { title } = shopifyProduct;
  const price = getProductPrice(shopifyProduct);
  return (
    <div className={styles.product}>
      {image && (
        <Link className={styles.productImage} to={routes.products(name)}><Img fluid={image.fluid} /></Link>
      )}
      <div className={styles.productLabel}>
        <p className={styles.productTitle}><Link to={routes.products(name)}>{title}</Link></p>
        {price && <p className={styles.productPrice}>${price}</p>}
      </div>
    </div>
  );
};

Now, we can save our files and restart the Gatsby development server. When we do, we'll see that our products now have their images and titles linked. When we click on the links, we're taken to dedicated pages for each product.

Pretty cool!

Conclusion

We won't go further into the logic for managing a cart or checking out in this guide. We'll explore that functionality and create a checkout with TakeShape and Shopify in a follow-up.

In the meantime, keep playing around with and exploring TakeShape's Shopify integration. This is only one example of how powerful it is to combine Shopify's data with content and even other APIs!

If you have any feedback, please go ahead and open an issue on the sample project. You can also email us at contact@takeshape.io or send us a message using the chat tool below.