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.








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.
api/social-card.js
: An API that will generate a social card for OG imagesapi/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(),});const LANDSCAPE_FORMAT = {background: "landcape-template.png",font: "Inter-ExtraBold.ttf.fnt",textY: 380,};const SQUARE_FORMAT = {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`) {options = LANDSCAPE_FORMAT;} 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 Jimp.read(imageBuffer);const imageDimensions = [modifiedImage.getWidth(),modifiedImage.getHeight(),];const textDimensions = [Jimp.measureText(font, text),Jimp.measureTextHeight(font, text),];modifiedImage.print(font,imageDimensions[0] / 2 - textDimensions[0] / 2,// This is approximateoptions.textY,text);return res.header("Content-Type", "image/png").status(200).send(await modifiedImage.getBufferAsync(Jimp.MIME_PNG));} catch (e) {console.error(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) =>fetch(`${HOST}/api/social-card?text=${body.text}&format=${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.finalize();archive.on("error", reject);archive.on("data", (data) => responses.push(data));archive.on("end", () => {resolve(Buffer.concat(responses));});});return res.set({"Content-Type": "application/zip","Content-Disposition": "attachment; filename=assets.zip",}).status(200).send(buffer);}