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:

Cover Example

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.