Skip to main content

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

· 13 min read
Allan Lasser

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 in an API Mesh, 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's API Mesh and Shopify

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.

Follow Along

Follow along with this guide by checking out our starter project repo. Our starters let you instantly deploy a pre-configured TakeShape project.

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.

The "Shopify Import" dialog will open. Select all the checkboxes. This will proactively create new shapes, queries and mutations for our API and do an initial import of all of our related Shopify data.

Next, navigate to the "Schema" page and create a Look shape. We'll add three fields to this new shape:

  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 shape 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

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!

Follow Along

Follow along with this guide by checking out our starter project repo. Our starters let you instantly deploy a pre-configured TakeShape project.

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:

.env
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:

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:

src/pages/index.js
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:

src/pages/index.js
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:

src/pages/index.js
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:

gatsby-node.js
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:

src/routes.ts
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:

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:

src/pages/index.js
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.