Create social media image programmatically - Part 1
Now that my site is up, I’ve run into a minor issue: when I share a post social media, there is no cover picture 😫.
First I’ve thought about setting up an image named banner.png
or banner.jpg
stored in the posts/images
folder to be used as the Open Graph image, like this:
<meta property="og:image" content="(...)/posts/images/banner.jpg" />
// If a post has no image, I show my avatar instead:
<meta property="og:image" content="(...)/images/avatar.png" />
But there lies the problem: making those custom banner images for each post is a hassle. Sure, if I feel inspired I will make one, but I wanted to have a fallback in case I don't.
I’ve had this thought of programmatically generating them since I read Flavio Copes Article: How to create and save an image with Node.js and Canvas.
Tools Used
To create this image on a fly, we will need a few things:
Gatsby
If you haven’t used Gatsby before, then I highly recommend it! It’s React-based framework for creating websites and apps. It's great whether you're building a portfolio site or blog, or a high-traffic e-commerce store or company homepage.
Although this tutorial uses Gatsby/React, I will assume that you have a working knowlege of it and have a project set to go.
Canvas
Canvas is a Cairo-backed Canvas implementation for Node.js. let’s add it to our gatsby site.
yarn add canvas
This package provides us a Node.js based implementation of the Canvas API that we know and love in the browser.
In other words, everything I use to generate images also works in the browser.
Except instead of getting a Canvas instance from a <canvas>
HTML element, I load the library, get the function createCanvas out of it:
const { createCanvas } = require('canvas')
Crypto-Js
Crypto-Js is a JavaScript library of crypto standards. It comes with a growing collection of standard and secure cryptographic algorithms implemented in JavaScript using best practices and patterns. They are fast, and they have a consistent and simple interface.
We will be using the MD5
module for hashing the post slug to guarantee a unique filename. So, let’s add it to our project:
yarn add crypto-js
Let’s code!
Now that I’ve install canvas
and crypto-js
to my gatsby site, all the actual code will be done on gatsby-node.js
file. I will use Gatsby Tutorial Part 7 as example, but we will add the frontmatter title
to the query, so we can pass it to our function later:
const path = require(`path`)
// ...
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions
const result = await graphql(`
query {
allMarkdownRemark {
edges {
node {
fields {
slug
}
frontmatter {
title
}
}
}
}
}
`)
result.data.allMarkdownRemark.edges.forEach(({ node }) => {
createPage({
path: node.fields.slug,
component: path.resolve(`./src/templates/blog-post.js`),
context: {
// Data passed to context is available
// in page queries as GraphQL variables.
slug: node.fields.slug,
title: node.frontmatter.title,
},
})
})
}
Step 1 - Load The Libraries
First things first: let’s add the libraries we need at the top of the gatsby-node.js
.
const fs = require('fs')
const MD5 = require('crypto-js/md5')
const { createCanvas, loadImage, registerFont } = require('canvas')
const { createFilePath } = require(`gatsby-source-filesystem`);
fs
used for saving the image file to disk.
MD5
for hashing the slug string.
createCanvas
creates a Canvas instance.
loadImage
convenience method for loading images.
registerFont
register the font with Canvas.
Now that we’ve loaded the libraries, let's call the createPageCover()
function before createPage()
:
result.data.allMarkdownRemark.edges.forEach(({ node }) => {
// create page cover
const coverPicture = await createPageCover(
node.frontmatter.title,
node.fields.slug
)
createPage({
path: node.fields.slug,
component: path.resolve(`./src/templates/blog-post.js`),
context: {
slug: node.fields.slug,
title: node.frontmatter.title,
coverPicture: coverPicture, // pass coverPicture in context
},
})
})
Now let’s declare our createPageCover()
function:
async function createPageCover(title, slug) {
// We'll create the image inside the public directory
const outputFilename = `./public/${MD5(title + slug)}.png`
// If the hash named file exists, return the string
if (fs.existsSync(outputFilename)) {
return outputFilename
}
console.log(`Creating page cover for ${slug} page`)
// Set the font family used to write the name of the post
registerFont(require.resolve(`./src/fonts/Roboto-Black.ttf`), {
family: 'Roboto'
})
// Set standard social media picture size
const width = 1200
const height = 630
// Create the canvas and get its context
const canvas = createCanvas(width, height)
const context = canvas.getContext('2d')
// Create background
context.fillStyle = '#1F2937'
context.fillRect(0, 0, width, height)
// Add post title at the top of the image
context.font = 'bold 60pt Roboto'
context.textAlign = 'center'
context.textBaseline = 'top'
context.fillStyle = '#fff'
context.fillText(title, 600, 215)
// Add website url to the bottom of the image
const text = 'rodrigopassos.com'
context.fillStyle = '#fff'
context.font = 'bold 30pt Roboto'
context.fillText(text, 630, 530)
// Load avatar and add it to the image
const image = await loadImage(require.resolve(`./src/images/avatar.png`))
context.drawImage(image, 380, 520, 70, 70)
// Write image to file
const buffer = canvas.toBuffer('image/png')
fs.writeFileSync(outputFilename, buffer)
console.log(`${outputFilename} created`);
// return hashed filename
return outputFilename
}
A post titled Hello World 2021! will generate the image bellow:
Congratulations 🎉 you created a social media image programmatically!
You could use the same technique to generate memes, graphs, or any over canvas based images.
Special thanks to @flaviocopes for his articles and knowledge.
Of course, for our purposes we are far from done: what happens when the title is long? In the next article I will show how we can add text wrap and position based on its length.