If you’re a Gatsby + Shopify fan or have been keeping your ear close to the ground you may have stumbled upon Gatsby’s recent Shopify POC demo which showcases what’s possible with the upgraded Gatsby + Shopify integration.
Using gatsby-source-shopify you can pull products from a Shopify catalog into your Gatsby website with the greatest of ease and the POC certainly goes to the extremes to prove that point. Sourcing and then statically rendering 10k products with 30k variants is quite exemplary!
However, I was keen to explore how Gatsby + Shopify might be adopted in a more incremental way. To test the water in a way that might be of interest to those curious to adopt Gatsby as a headless Shopify solution and perhaps do a little more than your everyday ecommerce website.
Whenever I’m creating Shopify demos the first stumbling block is always the 14 day free trial which normally expires just as i’m about to push to production 🤦
This time around I’ve been a little more strategic and have collaborated with Gatsby’s marketing team and have been using their real Shopify account… and here’s what we came up with.
That’s right, I’ve worked directly with Gatsby’s marketing team to design and produce a limited edition water bottle, and you can win yours for FREE at https://500bottles.gatsbyjs.io
There’s only 500, so get yours now, right now, before they’re all gone!
Method to my madness
As with all my demos I try to push the boat out a little bit and do the things less common and 500 Bottles is no different.
How is it different I hear you ask? Well, for starters I’ve used Three.js to create a visually arresting synthwave inspired hero animation to differentiate it from run of the mill ecommerce websites.
Three.js isn’t something you’ll see that often on ecommerce sites, and perhaps for good reason.
Three.js weighs many KB’s but I have tried my damndest to keep it as lightweight and performant as possible, along with observing users prefers-reduced-motion preferences.
I felt the use of Three.js was justified for this demo since the remaining parts of the site are extremely lightweight.
The hero animation is “Shopify aware” which is a term I’ve just invented but what I mean by this is, the animated Three.js geometries are subtly used to reflect the current Shopify inventory levels. Solid Icosahedrons represent the amount of bottles available, while hollow Tori represent bottles that have already been won.
Here’s some screenshots to better demonstrate the effect 👇
When the Shopify inventory levels are at 500, the geometries are all Icosahedrons:
When the inventory levels are anywhere between 1 and 499 the geometries are mixed:
And finally, when the inventory levels hits 0 all the geometries are Tori:
I could have stopped there but, Gatsby as you know statically creates web pages. This gives your SEO a boost because you can populate your site’s pages with real and actual metadata.
A common example would be to include the product name in the HTML
I’ve followed the same SEO best practices but also made the logo, metadata and open-graph images “Shopify aware”
As each bottle is won and a checkout is completed, Shopify’s inventory level update event is fired which calls a defined webhook. I’ve used this webhook to notify Gatsby Cloud which in turn rebuilds the site so the next time the site loads the Three.js animation, the logo, the metadata and open-graph image will all reflect the current Shopfiy inventory levels for the water bottle product.
This can be seen in action when https://500bottles.gatsbyjs.io/ is shared on E.g Twitter where the social card summary and open-graph image should be almost in sync with the site.
(FYI This can sometimes be out of sync due to Twitter’s slightly aggressive image caching)
I won’t, for now go into how I created each of the open-graph images but stay tuned as i plan to write a follow up post about that, follow me on Twitter: @PaulieScanlon or bookmark my blog: https://paulie.dev/
Whilst I’m pretty happy with how this all turned out, none of the above is particularly “ground-breaking”. (Although I am quite pleased with the Three.js part 😊) After all, statically rendered web pages is what Gatsby does best.
The tricky part was actually sourcing the data, that may sound confusing since the plugin handles that right? — true but, there are in my opinion, a couple of shortcomings with Shopify’s API’s that made this particular project a slippery little blighter.
Let’s wind back the clock for a moment and discuss the legacy gatsby-source-shopify plugin.
In this version of the plugin Gatsby leveraged Shopify’s Storefront API:
“Storefront API gives you full creative control to add Shopify buying experiences anywhere your customers are, including websites, apps, and video games. Storefront API is useful when Shopify merchants have custom requirements not met by existing channels like online store or POS.”
However the main problem with this API is that it’s rate limited, blast!
“Storefront API uses a leaky bucket algorithm to enforce its rate limit”
Moreover I’ve experienced issues with this plugin on Gatsby Cloud where the webhook would send a notification that changes in the Shopify store had occurred but Gatsby Cloud didn’t pick up the changes 🤷♂️
The Storefront API works a little like this:
You can see from the above that if I’d used the legacy plugin I would have only sourced data for products assigned in the 500 Bottles Sales channel, in my case it’s just the one product and the rest of the products which are related to the Gatsby Swag store aren’t sourced.
This version of the plugin has been completely rebuilt to use Shopify’s Admin API:
“The Admin API is the primary way that apps and services interact with Shopify. It provides extensive access to data about individual Shopify stores, and allows you to add your own features to the Shopify user experience”
The Admin API isn’t rate limited like the Storefront API and any changes that occur in the Shopify store do correctly rebuild on Gatsby Cloud, but it needed a helping hand in identifying which set of products to query.
By default the plugin will query all products assigned to the Online store but via the use of the plugin options it can be configured to query Sales channels, E.g 500 Bottles.
With the plugin configured as above I was able to query the 500 Bottles Sales channel and could access the 500 Bottles product by using an
elemMatch on the
sku in a static GraphQL query. E.g:
In case it’s misunderstood, Gatsby’s Shopify source plugin only handles data sourcing, it doesn’t handle any of the Shopify checkout functionality which I needed to make this demo a fully functional headless experience, i’ll come back to this later, but let’s first talk about some of the trickier marketing requirements I faced when discussing how to handle a free swag giveaway.
This, again, is a term I’ve invented but what I mean is that this marketing campaign is a totally free giveaway, and the marketing team were keen to explore ways in which free bottles could or rather couldn’t be won.
Naturally, giving away large quantities of stock to a single user that’s cheated the system would’ve spoiled it for the rest of you lovely people, so here’s my absolute best effort to avoid such a scenario.
First up, spin the bottle, this little UI mechanic is a game based on chance, not every user who enters their details will automatically win a bottle, but to keep it fair, approximately every 6th person to submit their details will win a bottle… hooray!
I’m not sure this counts as magic but hidden deep in the Shopify API documentation there’s a method for creating one-time-use discount codes, the method is called discountCodeBasicCreate
This was to be the key to “tinker prevention” 🕺
To interact with the Shopify API i’ve used shopify-api-node which is a small wrapper that handles the authentication, the
password are the same as used by the upgraded source plugin since it uses the same Shopify API under the hood.
To communicate with the Shopify API I used Gatsby Functions to dynamically generate a discount code for any user that wins.
Here’s a code snippet of the GraphQL mutation that handles creating a discount code:
The key bits to note are
The actual discount code is a randomly generated alphanumeric code but I can’t reveal exactly how I did this as it’ll give the game away.
When the mutation successfully resolves I use it when creating a checkout.
This was a little confusing to me at first but a Shopify checkout is kind of like a moment in time with an instance of specific data. Once a checkout has been created it exists until either completed or abandoned.
To create a checkout I used a second Shopify API called shopify-buy which is confusingly called js-buy-sdk on GitHub 🤷♂️. This again is a thin wrapper that manages authentication, however because it uses the Storefront API (like the legacy plugin) it requires a different
password to that of the Admin API, it also requires a shop name which is the name of a private app that I created in Gatsby’s Shopify Admin UI.
To create a checkout there’s a few things that need to happen. Here’s a code snippet of how I create a checkout:
The params are as follows,
shopify is the Shopify instance created by shopify-buy,
storefrontId is actually a product id and
discountCode as discussed previously.
With all of this in place the Shopify instance will return a
webUrl, it’s this that you can use as an
href which takes users over to a Shopify checkout page. 🥵
As if tinker prevention with dynamic discounts was’t enough the marketing team were also keen to cap the competition submissions each day. This way we hope the giveaway will run for longer giving more folks an opportunity to win.
To develop “win capping” (which again is a term I’ve just made up) I used shopify-api-node to query the Shopify Admin API for all orders placed within a 24-hour period. I’ve again used the
sku to perform this query. If the amount of orders for any given day exceeds our cap we pause the activity, if it’s less, game on… go spin that bottle!
If the response contains an array of a given length I can use this to determine if we’re above or below the cap amount.
This is the final step in tinker prevention. It’s a small thing but a simple cookie drop is used to determine if a user has submitted their details in any 24hr period. If they have they’re prompted to try again another day, if it’s their first attempt that day then, go, go, go, spin that bottle!
We see you, we love you!
This was to be my final trick. I wanted an interesting way to surface locations of the winners in the UI but was aware that showing too much user information might not go down well with the marketing team.
The solution I came up with was to map the users latitude / longitude onto a Three.js interactive globe.
It’s possible with Three.js to map real geographical data points around a sphere, I achieved this using three-geojson-geometry which renders a lovely interactive globe and by sourcing the latitude and longitude data from each of the completed orders I was able to plot the approximate locations of each of the lucky winners onto the globe.
Getting at the lat / lng data wasn’t easy though. I mentioned above about how the plugin can be configured via plugin options, and it is possible to query data for orders along with products by adding “orders” to
However, this creates a new bulk query operation for all orders placed. Naturally this is likely to be a huge amount of data and unfortunately the plugin only returns one or two useful bits of information relating to orders, and not the
shippingAddress which contains the latitude and longitude for each order, which is what I needed! Blast again!
To solve this problem I wrote my own bulk query operation and to be honest that’s a blog post all on it’s own but i’ll briefly outline how bulks query operations work and if you’re interested, all the information I used can be found in the Shopify docs
Bulk queries work a bit like this.
You kick off a bulk query using a GraphQL mutation and can pass the params you’d like to query for and from where E.g.
You can see from my mutation that instead of querying all orders i’m only querying orders for a specific SKU
However, the response from a bulk query isn’t what you’d usually expect. Instead of returning useful data, bulk query operations return the following 😳
The main thing to notice here is the status, this can either be “CREATED”, “IN PROGRESS” or “COMPLETED”.
To find out the status of a bulk query you have continuously poll it. To create a continues poll I used the methods as outline in this article: Polling with async/await by Jakub Kočí
Once a bulk query’s status is “COMPLETED” Shopify will return a url for a JSONL file which can then be download using a second HTTP request.
Only once the second request completes could I loop over the data and pass it back on to Gatsby’s data layer using
Once I had the lat / lng for each order I was able to plot the locations around the globe.
This was no mean feat and actually it’s where using the upgraded plugin really shines. Having now written my own bulk query operation I can really appreciate how much effort must have gone into developing the upgrade plugin — Great work team!
Source plugin aside, I hope this demo serves as an example of how Gatsby can be so much more than static websites. The functionality I’ve added to this site rivals that of many straight up React applications I’ve worked on.
I’m so, so, so, so pleased with how this all worked out, marketing threw some tough challenges my way, some of which caused quite a bit of lost sleep and continue to cause night terrors but all in all it was a truly wonderful collaboration.
I’d like to thank Gatsby for the opportunity and I owe special thanks to one particular member of Gatsby’s marketing team (you know who you are)
I hope you enjoy this demo and if you’d like to discuss any of the methods I’ve used please come find me on Twitter and we’ll have a natter, oh and do let me know what you think of the bottle if you win one! @PaulieScanlon