Generate Social Image Covers With Eleventy And Node-Canvas
This static blog is generated with Eleventy and all its social images are automatically generated with node-canvas. In this tutorial we’ll set up a basic version of this script so you can use it on your blog as well.
For this tutorial we assume you already have an Eleventy site that you want to add social images to.
If you need to set up a new site you can follow the Eleventy getting started guide to set up your site.
If you’re in a hurry and just want to copy paste the code, skip to the conclusion
Installing Node-Canvas
node-canvas
is an HTML Canvas implementation in Node. It allows us to draw to a canvas in Node just as we would in the browser, this makes it super easy to get started with.
Note that we use canvas
instead of node-canvas
to install the package.
npm install canvas
We’ll leave our canvas in the oven while we work on preparing the Eleventy script.
Configuring the Eleventy Script
We’re going to edit the .eleventy.js
file. It’s the file that we use to configure our Eleventy site. If this file doesn’t exist in your project, create it and paste the contents below.
Please note that this tutorial assumes your site articles are located in the src
directory and the output location is the dist
directory. If your articles are in a different directory the code will still work but you might have to adjust some paths.
// .eleventy.js
module.exports = function (eleventyConfig) {
return {
dir: {
input: 'src',
output: 'dist',
},
};
};
Run npx @11ty/eleventy
to test if the pages are generated in the dist
folder.
We insert the addTransform
hook to intercept the moment Eleventy generates an article so we can create our social image at the same time.
module.exports = function (eleventyConfig) {
eleventyConfig.addTransform('social-image', async function (content) {
// this will run each time a file is handled by Eleventy
return content;
});
return {
dir: {
input: 'src',
output: 'dist',
},
};
};
We’re only interested in articles, so let’s filter out any other files.
this.inputPath
contains the location of the input file. We check if it ends with .md
(for Markdown) and else we exit by returning the article contents.
module.exports = function (eleventyConfig) {
eleventyConfig.addTransform('social-image', async function (content) {
// only handle blog posts
if (!this.inputPath.endsWith('.md')) return content;
// do something with the article
// return normal content
return content;
});
return {
dir: {
input: 'src',
output: 'dist',
},
};
};
It’s time to add the function that will generate the social image for our article.
We create the createSocialImageForArticle
function that returns a Promise
. It receives the location of the input
article and the location of the output
social image.
In the example below we use the same name as the input article but replace the extension with .jpeg
.
const createSocialImageForArticle = (input, output) =>
new Promise((resolve, reject) => {
// this is where we'll generate the social image
});
module.exports = function (eleventyConfig) {
eleventyConfig.addTransform('social-image', async function (content) {
// only handle blog posts
if (!this.inputPath.endsWith('.md')) return content;
try {
await createSocialImageForArticle(
// our input article
this.inputPath,
// the output image name
this.outputPath.replace('.html', '.jpeg')
);
} catch (err) {
console.error(err);
}
// return normal content
return content;
});
return {
dir: {
input: 'src',
output: 'dist',
},
};
};
We’ve filtered out non-articles and have set up a function to draw our canvas and output the matching social image, let’s move on to implementing the function.
Drawing the Social Image with Node-Canvas
We’re now ready to draw the social image cover.
In the code examples below we’ll only focus on the createSocialImageForArticle
function, we won’t touch any other code so the rest of the code is hidden.
const createSocialImageForArticle = (input, output) =>
new Promise((resolve, reject) => {
// this is where we'll generate the social image
});
First we need to load the node modules we’re going to use.
// for handling files
const fs = require('fs');
// for managing path information
const path = require('path');
// for creating the canvas
const { createCanvas } = require('canvas');
// our good old function
const createSocialImageForArticle = (input, output) =>
new Promise((resolve, reject) => {
// this is where we'll generate the social image
});
For this part we’ll assume the title of the article is defined in the Front Matter of the file.
---
title: My article title
---
Our goal is to draw the title of the article on the cover image. To do that we’ll have to read out the file data and “parse” the YAML Front Matter.
const fs = require('fs');
const path = require('path');
const { createCanvas } = require('canvas');
const createSocialImageForArticle = (input, output) =>
new Promise((resolve, reject) => {
// read data from input file
const data = fs.readFileSync(input, {
encoding: 'utf-8',
});
// get title from file data
const [, title] = data.match(/title:(.*)/);
// `title` contains the article title
});
Alright, we’re ready to start drawing. Let’s create a canvas with a white background and draw our article title to it in black.
const fs = require('fs');
const path = require('path');
const { createCanvas } = require('canvas');
const createSocialImageForArticle = (input, output) =>
new Promise((resolve, reject) => {
// read data from input file
const data = fs.readFileSync(input, {
encoding: 'utf-8',
});
// get title from file data
const [, title] = data.match(/title:(.*)/);
// draw cover image
const canvas = createCanvas(1024, 512);
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'black';
ctx.font = '64px sans-serif';
ctx.fillText(title, 0, 64);
});
If you’ve worked with the Canvas API before this is all very familiar.
Now for the final act, saving the canvas data to disk.
We’re writing our image before Eleventy writes the article HTML file so we need to make sure the article directory already exists.
const fs = require('fs');
const path = require('path');
const { createCanvas } = require('canvas');
const createSocialImageForArticle = (input, output) =>
new Promise((resolve, reject) => {
// read data from input file
const data = fs.readFileSync(input, {
encoding: 'utf-8',
});
// get title from file data
const [, title] = data.match(/title:(.*)/);
// draw cover image
const canvas = createCanvas(1024, 512);
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'black';
ctx.font = '64px sans-serif';
ctx.fillText(title, 0, 64);
// test if the output directory already exists, if not, create
const outputDir = path.dirname(output);
if (!fs.existsSync(outputDir))
fs.mkdirSync(outputDir, { recursive: true });
// write the output image
const stream = fs.createWriteStream(output);
stream.on('finish', resolve);
stream.on('error', reject);
canvas
.createJPEGStream({
quailty: 0.8,
})
.pipe(stream);
});
We’re done. If we now run npx @11ty/eleventy
we should see a JPEG pop up next to our article HTML file.
We can now reference the image in a open graph meta tag like so.
<meta
property="og:image"
content="https://site.domain/article-path/index.jpeg"
/>
Conclusion
Using two amazing open source tools we’ve quickly set up flexible and automated generation of social images.
The node-canvas
API allows us to easily finetune our graphics. It exports useful helper methods like loadImage
to quickly load and draw images on top of your canvas, and registerFont
to facilitate loading and using locally hosted fonts.
Inspect the finished script below, it’s ready for some serious copy paste action.
// .eleventy.js
const fs = require('fs');
const path = require('path');
const { createCanvas } = require('canvas');
const createSocialImageForArticle = (input, output) =>
new Promise((resolve, reject) => {
// read data from input file
const data = fs.readFileSync(input, {
encoding: 'utf-8',
});
// get title from file data
const [, title] = data.match(/title:(.*)/);
// draw cover image
const canvas = createCanvas(1024, 512);
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'black';
ctx.font = '64px sans-serif';
ctx.fillText(title, 0, 64);
// test if the output directory already exists, if not, create
const outputDir = path.dirname(output);
if (!fs.existsSync(outputDir))
fs.mkdirSync(outputDir, { recursive: true });
// write the output image
const stream = fs.createWriteStream(output);
stream.on('finish', resolve);
stream.on('error', reject);
canvas
.createJPEGStream({
quailty: 0.8,
})
.pipe(stream);
});
module.exports = function (eleventyConfig) {
eleventyConfig.addTransform('social-image', async function (content) {
// only handle blog posts
if (!this.inputPath.endsWith('.md')) return content;
try {
await createSocialImageForArticle(
// our input article
this.inputPath,
// the output image name
this.outputPath.replace('.html', '.jpeg')
);
} catch (err) {
console.error(err);
}
// return normal content
return content;
});
return {
dir: {
input: 'src',
output: 'dist',
},
};
};