Auto Generating Post Thumbnails with Node.JS
Johnny Simpson
Posted on February 21, 2022
Every time I post an article, I create a thumbnail to go along with it. Often this part is the most tedious. I usually do it in Photoshop or another image editor. To try and make this easier, I've recently automated the generation of post thumbnails of this image with Javascript and Node.JS. In this tutorial we'll be looking at how you can generate your own article images automatically, using Node.JS and Canvas. The final code can be found in this Git Gist.
Here is an example of an image I generated using this method:
How to use Canvas in Node.JS
Since Node.JS is a backend language, it doesn't have canvas right out of the box. We have to use a component called canvas, and import it into our Node.JS. This can be installed with the line npm i canvas
, and imported into any Node.JS file.
How to use Emojis with Node.JS Canvas
You can do most of what I'm going to do here with the default canvas module - but for the images I generate, I also wanted to use emojis. As such, I'm using a fork of that package, called @napi-rs/canvas
, which supports Emojis. The version I am using is 0.1.14, so if you start running into issues replicating this guide, try installing it with the command npm i @napi-rs/canvas@0.1.14
.
Now that we've covered the basics, let's get started. First off, let's import all of our packages. I am importing a few things here:
- canvas - this is how we will create our image.
- fs - this is how we will write our image to our server and save it.
- cwebp - this is how we'll save our image as a webp file - so it's optimised for web.
- fonts - I'm importing 3 fonts - two are versions Inter, which is a great font, and the last is the Apple Emoji font. You can find Inter here, and the Apple Emoji Font here.
Don't forget to install dependencies, using npm i @napi-rs/canvas
, and npm i cwebp
!
import canvas from '@napi-rs/canvas' // For canvas.
import fs from 'fs' // For creating files for our images.
import cwebp from 'cwebp' // For converting our images to webp.
// Load in the fonts we need
GlobalFonts.registerFromPath('./fonts/Inter-ExtraBold.ttf', 'InterBold');
GlobalFonts.registerFromPath('./fonts/Inter-Medium.ttf','InterMedium');
GlobalFonts.registerFromPath('./fonts/Apple-Emoji.ttf', 'AppleEmoji');
How to auto generate post thumbnails with Javascript
Next up we need to write a utility function for wrapping text. This is a pre-requisite to what we're going to do in our canvas. When we write text on an HTML canvas, it typically doesn't wrap automatically. Instead, we need to create a function which measures the width of the container, and decides whether to wrap or not. This is a useful canvas utility function in general, so it may be worth saving! The annotated function is shown below:
// This function accepts 6 arguments:
// - ctx: the context for the canvas
// - text: the text we wish to wrap
// - x: the starting x position of the text
// - y: the starting y position of the text
// - maxWidth: the maximum width, i.e., the width of the container
// - lineHeight: the height of one line (as defined by us)
const wrapText = function(ctx, text, x, y, maxWidth, lineHeight) {
// First, split the words by spaces
let words = text.split(' ');
// Then we'll make a few variables to store info about our line
let line = '';
let testLine = '';
// wordArray is what we'l' return, which will hold info on
// the line text, along with its x and y starting position
let wordArray = [];
// totalLineHeight will hold info on the line height
let totalLineHeight = 0;
// Next we iterate over each word
for(var n = 0; n < words.length; n++) {
// And test out its length
testLine += `${words[n]} `;
var metrics = ctx.measureText(testLine);
var testWidth = metrics.width;
// If it's too long, then we start a new line
if (testWidth > maxWidth && n > 0) {
wordArray.push([line, x, y]);
y += lineHeight;
totalLineHeight += lineHeight;
line = `${words[n]} `;
testLine = `${words[n]} `;
}
else {
// Otherwise we only have one line!
line += `${words[n]} `;
}
// Whenever all the words are done, we push whatever is left
if(n === words.length - 1) {
wordArray.push([line, x, y]);
}
}
// And return the words in array, along with the total line height
// which will be (totalLines - 1) * lineHeight
return [ wordArray, totalLineHeight ];
}
Now that we have our utility function complete, we can write our generateMainImage function. This will take all the info we give it, and produce an image for your article or site.
For context, on Fjolt, I give each category in the database two colors - which lets me generate a gradient background for each image per category. In this function, you can pass whatever colors you want in and achieve the same effect - or you can change the function entirely! The choice is yours.
// This function accepts 5 arguments:
// canonicalName: this is the name we'll use to save our image
// gradientColors: an array of two colors, i.e. [ '#ffffff', '#000000' ], used for our gradient
// articleName: the title of the article or site you want to appear in the image
// articleCategory: the category which that article sits in - or the subtext of the article
// emoji: the emoji you want to appear in the image.
const generateMainImage = async function(canonicalName, gradientColors, articleName, articleCategory, emoji) {
articleCategory = articleCategory.toUpperCase();
// gradientColors is an array [ c1, c2 ]
if(typeof gradientColors === "undefined") {
gradientColors = [ "#8005fc", "#073bae"]; // Backup values
}
// Create canvas
const canvas = createCanvas(1342, 853);
const ctx = canvas.getContext('2d')
// Add gradient - we use createLinearGradient to do this
let grd = ctx.createLinearGradient(0, 853, 1352, 0);
grd.addColorStop(0, gradientColors[0]);
grd.addColorStop(1, gradientColors[1]);
ctx.fillStyle = grd;
// Fill our gradient
ctx.fillRect(0, 0, 1342, 853);
// Write our Emoji onto the canvas
ctx.fillStyle = 'white';
ctx.font = '95px AppleEmoji';
ctx.fillText(emoji, 85, 700);
// Add our title text
ctx.font = '95px InterBold';
ctx.fillStyle = 'white';
let wrappedText = wrapText(ctx, articleName, 85, 753, 1200, 100);
wrappedText[0].forEach(function(item) {
// We will fill our text which is item[0] of our array, at coordinates [x, y]
// x will be item[1] of our array
// y will be item[2] of our array, minus the line height (wrappedText[1]), minus the height of the emoji (200px)
ctx.fillText(item[0], item[1], item[2] - wrappedText[1] - 200); // 200 is height of an emoji
})
// Add our category text to the canvas
ctx.font = '50px InterMedium';
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.fillText(articleCategory, 85, 553 - wrappedText[1] - 100); // 853 - 200 for emoji, -100 for line height of 1
if(fs.existsSync(`./views/images/intro-images/${canonicalName}.png`))) {
return 'Images Exist! We did not create any'
}
else {
// Set canvas as to png
try {
const canvasData = await canvas.encode('png');
// Save file
fs.writeFileSync(`./views/images/intro-images/${canonicalName}.png`), canvasData);
}
catch(e) {
console.log(e);
return 'Could not create png image this time.'
}
try {
const encoder = new cwebp.CWebp(path.join(__dirname, '../', `/views/images/intro-images/${canonicalName}.png`));
encoder.quality(30);
await encoder.write(`./views/images/intro-images/${canonicalName}.webp`, function(err) {
if(err) console.log(err);
});
}
catch(e) {
console.log(e);
return 'Could not create webp image this time.'
}
return 'Images have been successfully created!';
}
}
Generating Article Image with Node.JS in detail
Let's look at this function in detail, so we can fully understand what's going on. We start by prepping our data - making our category uppercase, and setting a default gradient. Then we create our canvas, and use getContext to initiate a space where we can draw on.
articleCategory = articleCategory.toUpperCase();
// gradientColors is an array [ c1, c2 ]
if(typeof gradientColors === "undefined") {
gradientColors = [ "#8005fc", "#073bae"]; // Backup values
}
// Create canvas
const canvas = createCanvas(1342, 853);
const ctx = canvas.getContext('2d')
Then we draw our gradient:
// Add gradient - we use createLinearGradient to do this
let grd = ctx.createLinearGradient(0, 853, 1352, 0);
grd.addColorStop(0, gradientColors[0]);
grd.addColorStop(1, gradientColors[1]);
ctx.fillStyle = grd;
// Fill our gradient
ctx.fillRect(0, 0, 1342, 853);
And write our emoji text onto the image.
// Write our Emoji onto the canvas
ctx.fillStyle = 'white';
ctx.font = '95px AppleEmoji';
ctx.fillText(emoji, 85, 700);
Now we get to use our wrapping function, wrapText
. We'll pass in our quite long articleName, and start it near the bottom of our image at 85, 753. Since wrapText returns an array, we'll then iterate through that array to figure out the coordinates of each line, and paint them onto the canvas:
After that, we can add on our category, which should be above both the emoji and title text - both of which we now have calculated.
// Add our title text
ctx.font = '95px InterBold';
ctx.fillStyle = 'white';
let wrappedText = wrapText(ctx, articleName, 85, 753, 1200, 100);
wrappedText[0].forEach(function(item) {
// We will fill our text which is item[0] of our array, at coordinates [x, y]
// x will be item[1] of our array
// y will be item[2] of our array, minus the line height (wrappedText[1]), minus the height of the emoji (200px)
ctx.fillText(item[0], item[1], item[2] - wrappedText[1] - 200); // 200 is height of an emoji
})
// Add our category text to the canvas
ctx.font = '50px InterMedium';
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.fillText(articleCategory, 85, 553 - wrappedText[1] - 100); // 853 - 200 for emoji, -100 for line height of 1
How to save Canvas Images to Server with Node.JS
Alright, now we've created our image, let's save it to our server:
- First of all, we'll check if the file exists. If it does, we'll return that the image exists and do nothing else.
- If the file doesn't exist, we'll try to create a png version of it, using
canvas.encode
, and then usefs.writeFileSync
to save it. - If all goes well, we'll then use
cwebp
to save an alternative,.webp
version of the file, which should be much smaller than the.png
version.
if(fs.existsSync(`./views/images/intro-images/${canonicalName}.png`))) {
return 'Images Exist! We did not create any'
}
else {
// Set canvas as to png
try {
const canvasData = await canvas.encode('png');
// Save file
fs.writeFileSync(`./views/images/intro-images/${canonicalName}.png`), canvasData);
}
catch(e) {
console.log(e);
return 'Could not create png image this time.'
}
try {
const encoder = new cwebp.CWebp(path.join(__dirname, '../', `/views/images/intro-images/${canonicalName}.png`));
encoder.quality(30);
await encoder.write(`./views/images/intro-images/${canonicalName}.webp`, function(err) {
if(err) console.log(err);
});
}
catch(e) {
console.log(e);
return 'Could not create webp image this time.'
}
return 'Images have been successfully created!';
}
Now we have a function which will auto generate images for us. As you might expect, if you need to run this function where you want to auto generate the image. If you had this saved and running in a file called index.js
, we could run it in Node.js with the following command:
node index.js
I run this every time I write a new article - so when the article is saved to the database, an image is also produced for it. Here is another example of an image generated this way:
How to add Node.JS Images to Your Site
Now your image should be saved to your server. If you have it in a location that is accessible via URL, you can add these images as the "featured images" on posts and web pages. To add these images to your posts as post thumbnails so they show up in social media, you simply need to add the following two meta tags to the head of your page. If you're interested in the full list of HTML and SEO meta tags, you can find out guide on that here.
<meta property="og:image" content="{{url to image}}" />
<meta name="twitter:image" content="{{url to image}}" />
Conclusion
Thanks for reading. In this guide we've covered how to use Node.JS to create post thumbnails. We've also covered how to use emojis in your Node.JS canvas. Here are some useful links for you:
- The final code can be found in this Git Gist
- Our Complete Javascript Guide
- More Javascript Content
Posted on February 21, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.