Gatsby Image Generator

Powered by Gatsby Cloud Functions

How this was built

The stack

We leverage the dynamic power of Gatsby Cloud Functions to generate an og-image and social sharing cards for the Gatsby Marketing team.

What else?

We're using a few tools, notably:

  • Jimp
    , an amazing image transformation tool that we use to load in the image, add custom text, and generate the final image.
  • yup
    to validate the schema and display helpful messages to the consumers of this API.
  • archiver
    to generate a .zip of the bundled assets and return the stream to the user to download.

The team

This project was built by 8 team members in a four hour period as part of an internal hackathon. It goes to show you can achieve pretty sophisticated results with the power of Gatsby Functions.

Photo of Dustin Schau
Photo of Florian Kissling
Photo of Gregory Hardy
Photo of Laci Texter
Photo of Shane Thomas
Photo of Sidhartha Chatterjee
Photo of Tyler Barnes
Photo of Ward Peeters

Build it yourself

Want to try it out yourself? Go for it! Tweak to your heart's content, and customize to your needs!

The code

There are two main pieces.

  1. api/social-card.js: An API that will generate a social card for OG images
  2. api/download-assets.js: An API to return a .zip file with bundled assets (for social sharing)

api/social-card.js Source

A simple serverless function at api/social-card.js is all we need for a robust social sharing card service! The code is as follows:

import fetch from "node-fetch";
import Jimp from "jimp";
import * as yup from "yup";
import getHost from "../lib/get-host";
const schema = yup.object().shape({
text: yup.string().required(),
format: yup.string().required(),
background: "landcape-template.png",
font: "Inter-ExtraBold.ttf.fnt",
textY: 380,
background: "square-template.png",
font: "Inter-ExtraBold.ttf.fnt",
textY: 732,
const HOST = getHost();
export default async function socialCard(req, res) {
try {
const { text, format } = await schema.validate(req.query);
let options;
if (format === `landscape`) {
} else if (format === `square`) {
options = SQUARE_FORMAT;
const font = await Jimp.loadFont(`${HOST}/${options.font}`);
const imageRes = await fetch(`${HOST}/${options.background}`);
const imageBuffer = await imageRes.buffer();
let modifiedImage = await;
const imageDimensions = [
const textDimensions = [
Jimp.measureText(font, text),
Jimp.measureTextHeight(font, text),
imageDimensions[0] / 2 - textDimensions[0] / 2,
// This is approximate
return res
.header("Content-Type", "image/png")
.send(await modifiedImage.getBufferAsync(Jimp.MIME_PNG));
} catch (e) {
return res.status(500).json({
message: e.message,
stack: e.stack,

api/download-assets.js Source

This function uses the library archiver to download a .zip of assets (invoking the previous service to generate the images).api/download-assets.js

import * as yup from "yup";
import * as archiver from "archiver";
import fetch from "node-fetch";
import getHost from "../lib/get-host";
const schema = yup.object().shape({
text: yup.string().required(),
const HOST = getHost();
export default async function Bundle(req, res) {
const body = await schema.validate(req.query);
const files = await Promise.all(
["square", "landscape"].map((format) =>
.then((res) => res.buffer())
.then((buffer) => [`${format}.png`, buffer])
const buffer = await new Promise(async (resolve, reject) => {
const archive = archiver("zip", {
zlib: { level: 9 },
let responses = [];
for (let [name, buff] of files) {
archive.append(buff, { name });
archive.on("error", reject);
archive.on("data", (data) => responses.push(data));
archive.on("end", () => {
return res
"Content-Type": "application/zip",
"Content-Disposition": "attachment;",