Building The SSG I’ve Always Wanted: An 11ty, Vite And JAM Sandwich

I don’t know about you, but I’ve been overwhelmed by all the web development tools we have these days. Whether you like Markdown, plain HTML, React, Vue, Svelte, Pug templates, Handlebars, Vibranium — you can probably mix it up with some CMS data and get a nice static site cocktail.

I’m not going to tell you which UI development tools to reach for because they’re all great — depending on the needs of your project. This post is about finding the perfect static site generator for any occasion; something that lets us use JS-less templates like markdown to start, and bring in “islands” of component-driven interactivity as needed.

I’m distilling a year’s worth of learnings into a single post here. Not only are we gonna talk code (aka duct-taping 11ty and Vite together), but we’re also going to explore why this approach is so universal to Jamstackian problems. We’ll touch on:

  • Two approaches to static site generation, and why we should bridge the gap;
  • Where templating languages like Pug and Nunjucks still prove useful;
  • When component frameworks like React or Svelte should come into play;
  • How the new, hot-reloading world of Vite helps us bring JS interactivity to our HTML with almost zero configs;
  • How this complements 11ty’s data cascade, bringing CMS data to any component framework or HTML template you could want.

So without further ado, here’s my tale of terrible build scripts, bundler breakthroughs, and spaghetti-code-duct-tape that (eventually) gave me the SSG I always wanted: an 11ty, Vite and Jam sandwich called Slinkity!

A Great Divide In Static Site Generation

Before diving in, I want to discuss what I’ll call two “camps” in static site generation.

In the first camp, we have the “simple” static site generator. These tools don’t bring JavaScript bundles, single-page apps, and any other buzzwords we’ve come to expect. They just nail the Jamstack fundamentals: pull in data from whichever JSON blob of CMS you prefer, and slide that data into plain HTML templates + CSS. Tools like Jekyll, Hugo, and 11ty dominate this camp, letting you turn a directory of markdown and liquid files into a fully-functional website. Key benefits:

  • Shallow learning curve
    If you know HTML, you’re good to go!
  • Fast build times
    We’re not processing anything complex, so each route builds in a snap.
  • Instant time to interactive
    There’s no (or very little) JavaScript to parse on the client.

Now in the second camp, we have the “dynamic” static site generator. These introduce component frameworks like React, Vue, and Svelte to bring interactivity to your Jamstack. These fulfill the same core promise of combining CMS data with your site’s routes at build time. Key benefits:

  • Built for interactivity
    Need an animated image carousel? Multi-step form? Just add a componentized nugget of HTML, CSS, and JS.
  • State management
    Something like React Context of Svelte stores allow seamless data sharing between routes. For instance, the cart on your e-commerce site.

There are distinct pros to either approach. But what if you choose an SSG from the first camp like Jekyll, only to realize six months into your project that you need some component-y interactivity? Or you choose something like NextJS for those powerful components, only to struggle with the learning curve of React, or needless KB of JavaScript on a static blog post?

Few projects squarely fit into one camp or the other in my opinion. They exist on a spectrum, constantly favoring new feature sets as a project’s need evolve. So how do we find a solution that lets us start with the simple tools of the first camp, and gradually add features from the second when we need them?

Well, let’s walk through my learning journey for a bit.

Note: If you’re already sold on static templating with 11ty to build your static sites, feel free to hop down to the juicy code walkthrough. 😉

Going From Components To Templates And Web APIs

Back in January 2020, I set out to do what just about every web developer does each year: rebuild my personal site. But this time was gonna be different. I challenged myself to build a site with my hands tied behind my back, no frameworks or build pipelines allowed!

This was no simple task as a React devotee. But with my head held high, I set out to build my own build pipeline from absolute ground zero. There’s a lot of poorly-written code I could share from v1 of my personal site… but I’ll let you click this README if you’re so brave. 😉 Instead, I want to focus on the higher-level takeaways I learned starving myself of my JS guilty pleasures.

Templates Go A Lot Further Than You Might Think

I came at this project a recovering JavaScript junky. There are a few static-site-related needs I loved using component-based frameworks to fill:

  1. We want to break down my site into reusable UI components that can accept JS objects as parameters (aka “props”).
  2. We need to fetch some information at build time to slap into a production site.
  3. We need to generate a bunch of URL routes from either a directory of files or a fat JSON object of content.

List taken from this post on my personal blog.

But you may have noticed… none of these really need clientside JavaScript. Component frameworks like React are mainly built to handle state management concerns, like the Facebook web app inspiring React in the first place. If you’re just breaking down your site into bite-sized components or design system elements, templates like Pug work pretty well too!

Take this navigation bar for instance. In Pug, we can define a “mixin” that receives data as props:

// nav-mixins.pug
mixin NavBar(links) // pug's version of a for loop each link in links a(href=link.href) link.text

Then, we can apply that mixin anywhere on our site.

// index.pug
// kinda like an ESM "import"
include nav-mixins.pug
html body +NavBar(navLinksPassedByJS) main h1 Welcome to my pug playground 🐶

If we “render” this file with some data, we’ll get a beautiful index.html to serve up to our users.

const html = pug.render('/index.pug', { navLinksPassedByJS: [ { href: '/', text: 'Home' }, { href: '/adopt', text: 'Adopt a Pug' }
] })
// use the NodeJS filesystem helpers to write a file to our build
await writeFile('build/index.html', html)

Sure, this doesn’t give niceties like scoped CSS for your mixins, or stateful JavaScript where you want it. But it has some very powerful benefits over something like React:

  1. We don’t need fancy bundlers we don’t understand.
    We just wrote that pug.render call by hand, and we already have the first route of a site ready-to-deploy.
  2. We don’t ship any JavaScript to the end-user.
    Using React often means sending a big ole runtime for people’s browsers to run. By calling a function like pug.render at build time, we keep all the JS on our side while sending a clean .html file at the end.

This is why I think templates are a great “base” for static sites. Still, being able to reach for component frameworks where we really benefit from them would be nice. More on that later. 🙃

Recommended Reading: How To Create Better Angular Templates With Pug by Zara Cooper

You Don’t Need A Framework To Build Single Page Apps

While I was at it, I also wanted some sexy page transitions on my site. But how do we pull off something like this without a framework?

Crossfade with vertical wipe transition. (Large preview)

Well, we can’t do this if every page is its own .html file. The whole browser refreshes when we jump from one HTML file to the other, so we can’t have that nice cross-fade effect (since we’d briefly show both pages on top of each other).

We need a way to “fetch” the HTML and CSS for wherever we’re navigating to, and animate it into view using JavaScript. This sounds like a job for single-page apps!
I used a simple browser API medley for this:

  1. Intercept all your link clicks using an event listener.
  2. fetch API: Fetch all the resources for whatever page you want to visit, and grab the bit I want to animate into view: the content outside the navbar (which I want to remain stationary during the animation).
  3. web animations API: Animate the new content into view as a keyframe.
  4. history API: Change the route displaying in your browser’s URL bar using window.history.pushState({}, 'new-route'). Otherwise, it looks like you never left the previous page!

For clarity, here’s a visual illustration of that single page app concept using a simple find-and-replace (source article):

Step-by-step clientside routing process: 1. Medium rare hamburger is returned, 2. We request a well done burger using the fetch API, 3. We massage the response, 4. We pluck out the ‘patty’ element and apply it to our current page. (Large preview)

Note: You can also visit the source code from my personal site.

Sure, some pairing of React et al and your animation library of choice can do this. But for a use case as simple as a fade transition… web APIs are pretty dang powerful on their own. And if you want more robust page transitions on static templates like Pug or plain HTML, libraries like Swup will serve you well.

What 11ty Brought To The Table

I was feeling pretty good about my little SSG at this point. Sure it couldn’t fetch any CMS data at build-time, and didn’t support different layouts by page or by directory, and didn’t optimize my images, and didn’t have incremental builds.

Okay, I might need some help.

Given all my learnings from v1, I thought I earned my right to drop the “no third-party build pipelines” rule and reach for existing tools. Turns out, 11ty has a treasure trove of features I need!

If you’ve tried out bare-bones SSGs like Jekyll or Hugo, you should have a pretty good idea of how 11ty works. Only difference? 11ty uses JavaScript through-and-through.

11ty supports basically every template library out there, so it was happy to render all my Pug pages to .html routes. It’s layout chaining option helped with my foe-single-page-app setup too. I just needed a single script for all my routes, and a “global” layout to import that script:

// _includes/base-layout.html
<body> <!--load every page's content between some body tags--> {{ content }} <!--and apply the script tag just below this--> <script src="main.js"></script>
</html> // random-blog-post.pug
layout: base-layout
--- article h2 Welcome to my blog p Have you heard the story of Darth Plagueis the Wise?

As long as that main.js does all that link intercepting we explored, we have page transitions!

Oh, And The Data Cascade

So 11ty helped clean up all my spaghetti code from v1. But it brought another important piece: a clean API to load data into my layouts. This is the bread and butter of the Jamstack approach. Instead of fetching data in the browser with JavaScript + DOM manipulation, you can:

  1. Fetch data at build-time using Node.
    This could be a call to some external API, a local JSON or YAML import, or even the content of other routes on your site (imagine updating a table-of-contents whenever new routes are added 🙃).
  2. Slot that data into your routes. Recall that .render function we wrote earlier:
const html = pug.render('/index.pug', { navLinksPassedByJS: [ { href: '/', text: 'Home' }, { href: '/adopt', text: 'Adopt a Pug' }
] })

…but instead of calling pug.render with our data every time, we let 11ty do this behind-the-scenes.

Sure, I didn’t have a lot of data for my personal site. But it felt great to whip up a .yaml file for all my personal projects:

# _data/works.yaml
- title: Bits of Good Homepage hash: bog-homepage links: - href: text: Explore the live site - href: text: Scour the Svelt-ified codebase timeframe: May 2019 - present tags: - JAMstack - SvelteJS
- title: Dolphin Audio Visualizer

And access that data across any template:

// home.pug
.project-carousel each work in works h3 #{title} p #{timeframe} each tag in tags ...

Coming from the world of “clientside rendering” with create-react-app, this was a pretty big revelation. No more sending API keys or big JSON blobs to the browser. 😁

I also added some goodies for JavaScript fetching and animation improvements over version 1 of my site. If you’re curious, here’s where my README stood at this point.

I Was Happy At This Point But Something Was Missing

I went surprisingly far by abandoning JS-based components and embracing templates (with animated page transitions to boot). But I know this won’t satisfy my needs forever. Remember that great divide I kicked us off with? Well, there’s clearly still that ravine between my build setup (firmly in camp #1) and the haven of JS-ified interactivity (the Next, SvelteKit, and more of camp #2). Say I want to add:

  • a pop-up modal with an open/close toggle,
  • a component-based design system like Material UI, complete with scoped styling,
  • a complex multi-step form, maybe driven by a state machine.

If you’re a plain-JS-purist, you probably have framework-less answers to all those use cases. 😉 But there’s a reason JQuery isn’t the norm anymore! There’s something appealing about creating discrete, easy-to-read components of HTML, scoped styles, and pieces of JavaScript “state” variables. React, Vue, Svelte, etc. offer so many niceties for debugging and testing that straight DOM manipulation can’t quite match.

So here’s my million dollar question:

Can we use straight HTML templates to start, and gradually add React/Vue/Svelte components where we want them?

The answer is yes. Let’s try it.

11ty + Vite: A Match Made In Heaven ❤️

Here’s the dream that I’m imagining here. Wherever I want to insert something interactive, I want to leave a little flag in my template to “put X React component here.” This could be the shortcode syntax that 11ty supports:

# Super interesting programming tutorial Writing paragraphs has been fun, but that's no way to learn. Time for an interactive code example! {% react './components/FancyLiveDemo.jsx' %}

But remember, the one-piece 11ty (purposely) avoids: a way to bundle all your JavaScript. Coming from the OG guild of bundling, your brain probably jumps to building Webpack, Rollup, or Babel processes here. Build a big ole entry point file, and output some beautiful optimized code right?

Well yes, but this can get pretty involved. If we’re using React components, for instance, we’ll probably need some loaders for JSX, a fancy Babel process to transform everything, an interpreter for SASS and CSS module imports, something to help with live reloading, and so on.

If only there were a tool that could just see our .jsx files and know exactly what to do with them.

Enter: Vite

Vite’s been the talk of the town as of late. It’s meant to be the all-in-one tool for building just about anything in JavaScript. Here’s an example for you to try at home. Let’s make an empty directory somewhere on our machine and install some dependencies:

npm init -y # Make a new package.json with defaults set
npm i vite react react-dom # Grab Vite + some dependencies to use React

Now, we can make an index.html file to serve as our app’s “entry point.” We’ll keep it pretty simple:

<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title>
<body> <h1>Hello Vite! (wait is it pronounced "veet" or "vight"...)</h1> <div id="root"></div>

The only interesting bit is that div id="root" in the middle. This will be the root of our React component in a moment!

If you want, you can fire up the Vite server to see our plain HTML file in your browser. Just run vite (or npx vite if the command didn’t get configured in your terminal), and you’ll see this helpful output:

vite vX.X.X dev server running at: > Local: http://localhost:3000/
> Network: use `--host` to expose ready in Xms.

Much like Browsersync or other popular dev servers, the name of each .html file corresponds to a route on our server. So if we renamed index.html to about.html, we would visit http://localhost:3000/about/ (yes, you’ll need a trailing slash!)

Now let’s do something interesting. Alongside that index.html file, add a basic React component of some sort. We’ll use React’s useState here to demonstrate interactivity:

// TimesWeMispronouncedVite.jsx
import React from 'react' export default function TimesWeMispronouncedVite() { const [count, setCount] = React.useState(0) return ( <div> <p>I've said Vite wrong {count} times today</p> <button onClick={() => setCount(count + 1)}>Add one</button> </div> )

Now, let’s load that component onto our page. This is all we have to add to our index.html:

<!DOCTYPE html>
<body> <h1>Hello Vite! (wait is it pronounced "veet" or "vight"...)</h1> <div id="root"></div> <!--Don't forget type="module"! This lets us use ES import syntax in the browser--> <script type="module"> // path to our component. Note we still use .jsx here! import Component from './TimesWeMispronouncedVite.jsx'; import React from 'react'; import ReactDOM from 'react-dom'; const componentRoot = document.getElementById('root'); ReactDOM.render(React.createElement(Component), componentRoot); </script>

Yep, that’s it. No need to transform our .jsx file to a browser-ready .js file ourselves! Wherever Vite sees a .jsx import, it’ll auto-convert that file to something browsers can understand. There isn’t even a dist or build folder when working in development; Vite processes everything on the fly — complete with hot module reloading every time we save our changes. 🤯

Okay, so we have an incredibly capable build tool. How can we bring this to our 11ty templates?

Running Vite Alongside 11ty

Before we jump into the good stuff, let’s discuss running 11ty and Vite side-by-side. Go ahead and install 11ty as a dev dependency into the same project directory from last section:

npm i -D @11ty/eleventy # yes, it really is 11ty twice

Now let’s do a little pre-flight check to see if 11ty’s working. To avoid any confusion, I’d suggest you:

  1. Delete that index.html file from earlier;
  2. Move that TimesWeMispronouncedVite.jsx inside a new directory. Say, components/;
  3. Create a src folder for our website to live in;
  4. Add a template to that src directory for 11ty to process.

For example, a file with the following contents:

# Hello world! It’s markdown here

Your project structure should look something like this:

components/ TimesWeMispronouncedVite.jsx

Now, run 11ty from your terminal like so:

npx eleventy --input=src

If all goes well, you should see an build output like this:

_site/ blog-post/ index.html

Where _site is our default output directory, and blog-post/index.html is our markdown file beautifully converted for browsing.

Normally, we’d run npx eleventy --serve to spin up a dev server and visit that /blog-post page. But we’re using Vite for our dev server now! The goal here is to:

  1. Have eleventy build our markdown, Pug, nunjucks, and more to the _site directory.
  2. Point Vite at that same _site directory so it can process the React components, fancy style imports, and other things that 11ty didn’t pick up.

So a two-step build process, with 11ty handing off the Vite. Here’s the CLI command you’ll need to start 11ty and Vite in “watch” mode simultaneously:

(npx eleventy --input=src --watch) & npx vite _site

You can also run these commands in two separate terminals for easier debugging. 😄

With any luck, you should be able to visit http://localhost:3000/blog-post/ (again, don’t forget the trailing slash!) to see that processed Markdown file.

Partial Hydration With Shortcodes

Let’s do a brief rundown on shortcodes. Time to revisit that syntax from earlier:

{% react '/components/TimesWeMispronouncedVite.jsx' %}

For those unfamiliar with shortcodes: they’re about the same as a function call, where the function returns a string of HTML to slide into your page. The “anatomy” of our shortcode is:

  • {% … %}
    Wrapper denoting the start and end of the shortcode.
  • react
    The name of our shortcode function we’ll configure in a moment.
  • '/components/TimesWeMispronouncedVite.jsx'
    The first (and only) argument to our shortcode function. You can have as many arguments as you’d like.

Let’s wire up our first shortcode! Add a .eleventy.js file to the base of your project, and add this config entry for our react shortcode:

// .eleventy.js, at the base of the project
module.exports = function(eleventyConfig) { eleventyConfig.addShortcode('react', function(componentPath) { // return any valid HTML to insert return `<div id="root">This is where we'll import ${componentPath}</div>` }) return { dir: { // so we don't have to write `--input=src` in our terminal every time! input: 'src', } }

Now, let’s spice up our with our new shortcode. Paste this content into our markdown file:

# Super interesting programming tutorial Writing paragraphs has been fun, but that's no way to learn. Time for an interactive code example! {% react '/components/TimesWeMispronouncedVite.jsx' %}

And if you run a quick npx eleventy, you should see this output in your _site directory under /blog-post/index.html:

<h1>Super interesting programming tutorial</h1> <p>Writing paragraphs has been fun, but that's no way to learn. Time for an interactive code example!</p> <div id="root">This is where we'll import /components/TimesWeMispronouncedVite.jsx</div>

Writing Our Component Shortcode

Now let’s do something useful with that shortcode. Remember that script tag we wrote while trying out Vite? Well, we can do the same thing in our shortcode! This time we’ll use the componentPath argument to generate the import, but keep the rest pretty much the same:

// .eleventy.js
module.exports = function(eleventyConfig) { let idCounter = 0; // copy all our /components to the output directory // so Vite can find them. Very important step! eleventyConfig.addPassthroughCopy('components') eleventyConfig.addShortcode('react', function (componentPath) { // we'll use idCounter to generate unique IDs for each "root" div // this lets us use multiple components / shortcodes on the same page 👍 idCounter += 1; const componentRootId = `component-root-${idCounter}` return ` <div id="${componentRootId}"></div> <script type="module"> // use JSON.stringify to // 1) wrap our componentPath in quotes // 2) strip any invalid characters. Probably a non-issue, but good to be cautious! import Component from ${JSON.stringify(componentPath)}; import React from 'react'; import ReactDOM from 'react-dom'; const componentRoot = document.getElementById('${componentRootId}'); ReactDOM.render(React.createElement(Component), componentRoot); </script> ` }) eleventyConfig.on('beforeBuild', function () { // reset the counter for each new build // otherwise, it'll count up higher and higher on every live reload idCounter = 0; }) return { dir: { input: 'src', } }

Now, a call to our shortcode (ex. {% react '/components/TimesWeMispronouncedVite.jsx' %}) should output something like this:

<div id="component-root-1"></div>
<script type="module"> import Component from './components/FancyLiveDemo.jsx'; import React from 'react'; import ReactDOM from 'react-dom'; const componentRoot = document.getElementById('component-root-1'); ReactDOM.render(React.createElement(Component), componentRoot);

Visiting our dev server using (npx eleventy --watch) & vite _site, we should find a beautifully clickable counter element. ✨

Buzzword Alert — Partial Hydration And Islands Architecture

We just demonstrated “islands architecture” in its simplest form. This is the idea that our interactive component trees don’t have to consume the entire website. Instead, we can spin up mini-trees, or “islands,” throughout our app depending on where we actually need that interactivity. Have a basic landing page of links without any state to manage? Great! No need for interactive components. But do you have a multi-step form that could benefit from X React library? No problem. Use techniques like that react shortcode to spin up a Form.jsx island.

This goes hand-in-hand with the idea of “partial hydration.” You’ve likely heard the term “hydration” if you work with component-y SSGs like NextJS or Gatsby. In short, it’s a way to:

  1. Render your components to static HTML first.
    This gives the user something to view when they initially visit your website.
  2. “Hydrate” this HTML with interactivity.
    This is where we hook up our state hooks and renderers to, well, make button clicks actually trigger something.

This 1-2 punch makes JS-driven frameworks viable for static sites. As long as the user has something to view before your JavaScript is done parsing, you’ll get a decent score on those lighthouse metrics.

Well, until you don’t. 😢 It can be expensive to “hydrate” an entire website since you’ll need a JavaScript bundle ready to process every last DOM element. But our scrappy shortcode technique doesn’t cover the entire page! Instead, we “partially” hydrate the content that’s there, inserting components only where necessary.

Don’t Worry, There’s A Plugin For All This: Slinkity

Let’s recap what we discovered here:

  1. Vite is an incredibly capable bundler that can process most file types (jsx, vue, and svelte to name a few) without extra config.
  2. Shortcodes are an easy way to insert chunks of HTML into our templates, component-style.
  3. We can use shortcodes to render dynamic, interactive JS bundles wherever we want using partial hydration.

So what about optimized production builds? Properly loading scoped styles? Heck, using .jsx to create entire pages? Well, I’ve bundled all of this (and a whole lot more!) into a project called Slinkity. I’m excited to see the warm community reception to the project, and I’d love for you, dear reader, to give it a spin yourself!

🚀 Try the quick start guide

Astro’s Pretty Great Too

Readers with their eyes on cutting-edge tech probably thought about Astro at least once by now. 😉 And I can’t blame you! It’s built with a pretty similar goal in mind: start with plain HTML, and insert stateful components wherever you need them. Heck, they’ll even let you start writing React components inside Vue or Svelte components inside HTML template files! It’s like MDX Xtreme edition. 🤯

There’s one pretty major cost to their approach though: you need to rewrite your app from scratch. This means a new template format based on JSX (which you might not be comfortable with), a whole new data pipeline that’s missing a couple of niceties right now, and general bugginess as they work out the kinks.

But spinning up an 11ty + Vite cocktail with a tool like Slinkity? Well, if you already have an 11ty site, Vite should bolt into place without any rewrites, and shortcodes should cover many of the same use cases as .astro files. I’ll admit it’s far from perfect right now. But hey, it’s been useful so far, and I think it’s a pretty strong alternative if you want to avoid site-wide rewrites!

Wrapping Up

This Slinkity experiment has served my needs pretty well so far (and a few of y’all’s too!). Feel free to use whatever stack works for your JAM. I’m just excited to share the results of my year of build tool debauchery, and I’m so pumped to see how we can bridge the great Jamstack divide.

Further Reading

Want to dive deeper into partial hydration, or ESM, or SSGs in general? Check these out:

  • Islands Architecture
    This blog post from Jason Format really kicked off a discussion of “islands” and “partial hydration” in web development. It’s chock-full of useful diagrams and the philosophy behind the idea.
  • Simplify your static with a custom-made static site generator
    Another SmashingMag article that walks you through crafting Node-based website builders from scratch. It was a huge inspiration to me!
  • How ES Modules have redefined web development
    A personal post on how ES Modules have changed the web development game. This dives a little further into the “then and now” of import syntax on the web.
  • An introduction to web components
    An excellent walkthrough on what web components are, how the shadow DOM works, and where web components prove useful. Used this guide to apply custom components to my own framework!

My 3 Rules for Social Media Success

Roughly 4.2 billion people worldwide use social media. In 2021, all ecommerce businesses should attempt to connect with prospects on those platforms.

This is my first post to help merchants utilize social media for brand awareness and sales. I’ll start by sharing my experiences as an artist. For years I’ve used social media as the primary sales tool for my paintings.

3 Social Media Rules for Ecommerce

Chose the right platforms. Rule number one is to focus on the networks where your audience gathers. Don’t try to force your business onto the most popular. Instead, choose the platforms that offer the best chances of success.

I focus heavily on TikTok and Instagram because the visual medium lends itself well to my creativity, such as shooting videos, while also selling paintings. When one of my videos goes viral — 500,000 to 25 million views — I sell a lot of paintings. That wouldn’t be possible on Twitter or Facebook.

But that doesn’t mean posting branded content on large Facebook groups won’t net a similar result with your business. It just means you have to look at what you’re selling and find the platform that will produce the best results for your time.

Carolyn Mara's TikTok pageCarolyn Mara's TikTok page

TikTok’s visual medium works well for artists, such as the author.

Connect, connect, connect. Next, take the time to respond to user comments, questions, and reviews. You want an engaged and happy community if you’re going to sell your products to the participants. You never know what opportunity might blossom from thanking someone for her comment or agreeing to repost your work.

Here’s an example.

A few months ago, a fan asked if he could repost my work on his Instagram profile. I agreed and then forgot about it until he posted one of my performance art mop painting videos a few weeks later. I didn’t realize this person’s account was very popular in Spain. That repost of my work was viewed millions of times, which led to an increase in orders and, more importantly, exposure to a new market. All I did was say, “Thank you for asking. Of course you can repost my work!”

The principle is the same for any online business. Find a way to connect with your audience because you never know which connection will lead to the next breakthrough.

Always remember that every single repost or share on someone’s social media profile can create extra exposure. Always interact no matter how big or small their accounts.

Use videos and photos. Engage your followers with videos and photos, but do it in a way that’s more like a conversation versus traditional advertising. People don’t like being sold to when browsing Instagram or Reddit. The most impactful posts are those that your audience shares with friends, family, and colleagues. The best way to do that is with content that shows your product, how it’s used, and how it works. And, if applicable, show how it looks in a realistic setting.

A life-like setting has been a game-changer for me. My online art business first gained traction when I posted images of my paintings in clients’ homes. It helped my audience see the finished product in the proper context and visualize how it fits into their lives.

Building An API With Gatsby Functions

You’ve probably heard about Serverless Functions, but if you haven’t, Serverless Functions provide functionality typically associated with server-side technologies that can be implemented alongside front-end code without getting caught up in server-side infrastructures.

With server-side and client-side code coexisting in the same code base, front-end developers like myself can extend the reach of what’s possible using the tools they already know and love.


Coexistence is great but there are at least two scenarios I’ve encountered where using Serverless Functions in this way weren’t quite the right fit for the task at hand. They are as follows:

  1. The front end couldn’t support Serverless Functions.
  2. The same functionality was required by more than one front end.

To help provide some context here’s one example of both points 1 and 2 named above. I maintain an Open-source project called MDX Embed, you’ll see from the docs site that it’s not a Gatsby website. It’s been built using Storybook and Storybook on its own provides no Serverless Function capabilities. I wanted to implement “Pay what you want” contributions to help fund this project and I wanted to use Stripe to enable secure payments but without a secure “backend” This would not have been possible.

By abstracting this functionality away into an API built with Gatsby Functions I was able to achieve what I wanted with MDX Embed and also re-use the same functionality and enable “Pay what you want” functionality for my blog.

You can read more about how I did that here: Monetize Open-Source Software With Gatsby Functions And Stripe.

It’s at this point that using Gatsby Functions can act as a kind of Back end for front end or BFF 😊 and developing in this way is more akin to developing an API (Application Programming Interface).

APIs are used by front-end code to handle things like, logins, real-time data fetching, or secure tasks that aren’t suitably handled by the browser alone. In this tutorial, I’ll explain how to build an API using Gatsby Functions and deploy it to Gatsby Cloud.

Preflight Checks

Gatsby Functions work when deployed to Gatsby Cloud or Netlify, and in this tutorial, I’ll be explaining how to deploy to Gatsby Cloud so you’ll need to sign up and create a free account first.

You’re also going to need either a GitHub, GitLab or BitBucket account, this is how Gatsby Cloud reads your code and then builds your “site”, or in this case, API.

For the purposes of this tutorial, I’ll be using GitHub. If you’d prefer to jump ahead, the finished demo API code can be found on my GitHub.

Getting Started

Create a new dir somewhere on your local drive and run the following in your terminal. This will set up a default package.json.

npm init -y


Type the following into your terminal to install the required dependencies.

npm install gatsby react react-dom


It’s likely your API won’t have any “pages” but to avoid seeing Gatsby’s default missing page warning when you visit the root URL in the browser, add the following to both src/pages/index.js and src/pages/404.js.

//src/pages/index.js & src/pages/404.js export default () => null;


Add the following to src/api/my-first-function.js.

I’ll explain a little later what 'Access-Control-Allow-Origin', '*' means, but in short, it makes sure that your APIs from other origins aren’t blocked by CORS.

//src/api/my-first-function.js export default function handler(req, res) { res.setHeader('Access-Control-Allow-Origin', '*'); res.status(200).json({ message: 'A ok!' });


Add the following to package.json.

//package.json ... "scripts": { "develop": "gatsby develop", "build": "gatsby build" },

Start The Gatsby Development Server

To spin up the Gatsby development server run the following in your terminal.

npm run develop

Make A Request From The Browser

With the Gatsby’s development server running you can visit http://localhost:8000/api/my-first-function, and since this is a simple GET request you should see the following in your browser.

{ "message": "A ok!"

Congratulations 🎉

You’ve just developed an API using Gatsby Functions.


If you are seeing the above response in your browser it’s safe to assume your function is working correctly locally, in the following steps I’ll explain how to deploy your API to Gatsby Cloud and access it using an HTTP request from CodeSandbox.

Push Code To Git

Before attempting to deploy to Gatsby Cloud you’ll need to have pushed your code to your Git provider of choice.

Gatsby Cloud

Log into your Gatsby Cloud account and look for the big purple button that says “Add site +”.

In the next step, you’ll be asked to either Import from a Git repository or Start from a Template, select Import from Git Repository and hit next.

As mentioned above Gatsby Cloud can connect to either GitHub, GitLab or Bitbucket. Select your preferred Git provider and hit next.

With your Git provider connected, you can search for your repository, and give your site a name.

Once you’ve selected your repository and named your site hit next.

You can skip the “Integrations” and “Setup” as we won’t be needing these.

If all has gone to plan your should be seeing something similar to the below screenshot.

You’ll see near the top on the left-hand side of the screen a URL that ends with, this will be the URL for your API and any functions you create can be accessed by adding /api/name-of-function to the end of this URL.

E.g, the complete deployed version of my-first-function.js for my demo API is as follows:

Demo API: My First Function.

Testing Your API

Visiting the URL of your API is one thing but it’s not really how APIs are typically used. Ideally to test your API you need to make a request to the function from a completely unrelated origin.

It’s here where res.setHeader('Access-Control-Allow-Origin', '*'); comes to the rescue. Whilst it’s not always desirable to allow any domain (website) to access your functions, for the most part, public functions are just that, public. Setting the Access Control header to a value of * means any domain can access your function, without this, any domain other than the domain the API is hosted on will be blocked by CORS.

Here’s a CodeSandbox that uses my-first-function from my demo API. You can fork this and change the Axios request URL to test your function.

CodeSandbox: My First Function

Getting Fancier

Sending a response from your API that says message: "A ok!" isn’t exactly exciting so in the next bit I’ll show you how to query the GitHub REST API and make a personal profile card to display on your own site using the API you just created, and it’ll look a little like this.

CodeSandbox: Demo profile card


To use the GitHub REST API you’ll need to install @octokit/rest package.

npm install @octokit/rest

Get GitHub User Raw

Add the following to src/api/get-github-user-raw.js.

// src/api/get-github-user-raw.js import { Octokit } from '@octokit/rest'; const octokit = new Octokit({ auth: process.env.OCTOKIT_PERSONAL_ACCESS_TOKEN
}); export default async function handler(req, res) { res.setHeader('Access-Control-Allow-Origin', '*'); try { const { data } = await octokit.request(`GET /users/{username}`, { username: 'PaulieScanlon' }); res.status(200).json({ message: 'A ok!', user: data }); } catch (error) { res.status(500).json({ message: 'Error!' }); }

Access Token

To communicate with the GitHub REST API you’ll need an access token. You can get this by following the steps in this guide from GitHub: Creating A Personal Access Token.

.env Variables

To keep your access token secure add the following to .env.development and .env.production.


You can read more about Gatsby environment variables in this guide from Gatsby: Environment Variables.

Start Development Server

As you did before start the Gatsby development server by typing the following in your terminal.

npm run develop

Make A Request From The Browser

With the Gatsby development server running you can visit http://localhost:8000/api/get-github-user-raw, and since this too is a simple GET request you should see the following in your browser. (I’ve removed part of the response for brevity.)

{ "message": "A ok!", "user": { "login": "PaulieScanlon", "id": 1465706, "node_id": "MDQ6VXNlcjE0NjU3MDY=", "avatar_url": "", "gravatar_id": "", "url": "", "type": "User", "site_admin": false, "name": "Paul Scanlon", "company": "Paulie Scanlon Ltd.", "blog": "", "location": "Worthing", "email": "", "hireable": true, "bio": "Jamstack Developer / Technical Content Writer (freelance)", "twitter_username": "pauliescanlon", "created_at": "2012-02-23T13:43:26Z", "two_factor_authentication": true, ... }

Here’s a CodeSandbox example of the full raw response.

CodeSandbox: Raw Response

You’ll see from the above that there’s quite a lot of data returned that I don’t really need, this next bit is completely up to you as it’s your API but I have found it helpful to manipulate the GitHub API response a little bit before sending it back to my frontend code.

If you’d like to do the same you could create a new function and add the following to src/api/get-github-user.js.

// src/api/get-github-user.js import { Octokit } from '@octokit/rest'; const octokit = new Octokit({ auth: process.env.OCTOKIT_PERSONAL_ACCESS_TOKEN
}); export default async function handler(req, res) { res.setHeader('Access-Control-Allow-Origin', '*'); try { const { data } = await octokit.request(`GET /users/{username}`, { username: 'PaulieScanlon' }); res.status(200).json({ message: 'A ok!', user: { name:, blog_url:, bio:, photo: data.avatar_url, githubUsername: `@${data.login}`, githubUrl: data.html_url, twitterUsername: `@${data.twitter_username}`, twitterUrl: `${data.twitter_username}` } }); } catch (error) { res.status(500).json({ message: 'Error!' }); }

You’ll see from the above that rather than returning the complete data object returned by the GitHub REST API I pick out just the bits I need, rename them and add a few bits before the username and URL values. This makes life a bit easier when you come to render the data in the frontend code.

Here’s a CodeSandbox example of the formatted response.

CodeSandbox: Formatted Response

This is very similar to the Profile Card CodeSandbox from earlier, but I’ve also printed the data out so you can see how each manipulated data item is used.

It’s worth noting at this point that all four of the CodeSandbox demos in this tutorial are using the demo API, and none of them are built using Gatsby or hosted on Gatsby Cloud — cool ay!

.env Variables In Gatsby Cloud

Before you deploy your two new functions you’ll need to add the GitHub Access token to the environment variables section in Gatsby Cloud.

Where To Go From Here?

I asked myself this very question. Typically speaking serverless functions are used in client-side requests and whilst that’s fine I wondered if they could also be used at build time to statically “bake” data into a page rather than relying on JavaScript which may or may not be disabled in the user’s browser.

…so that’s exactly what I did.

Here’s a kind of data dashboard that uses data returned by Gatsby Functions at both run and build time. I built this site using Astro and deployed it GitHub Pages.

The reason I think this is a great approach is because I’m able to re-use the same functionality on both the server and in the browser without duplicating anything.

In this Astro build I hit the same endpoint exposed by my API to return data that is then either baked into the page (great for SEO) or fetched at run time by the browser (great for showing fresh or up to the minute live data).

Data Dashboard

The data displayed on the left of the site is requested at build time and baked into the page with Astro. The data on the right of the page is requested at runtime using a client-side request. I’ve used slightly different endpoints exposed by the GitHub REST API to query different GitHub user accounts which create the different lists.

Everything you see on this site is provided by my more complete API. I’ve called it: Paulie API and I use it for a number of my websites.

Paulie API

Paulie API like the API from this tutorial is built with Gatsby but because Gatsby can act as both a site and an API I’ve used it to document how all my functions work and each endpoint has its own page that can be used as an interactive playground… feel free to have a look around.

So, there you have it, A Gatsby Functions API that can be used by any client-side or server-side code, from any website built with any tech stack. 🤯

Give it a go and I’d be very interested to see what you build. Feel free to share in the comments below or come find me on Twitter: @PaulieScanlon.