Last couple years I see very nice generated covers for social link preview, also our lovely dev.to does it and I've tried to code something like this.
Note. I was planning to do it in Nextjs, but to avoid too much instruction I've decided to separate them. Article about Nextjs is on the way too.
Result we have to reach is this 🔥
Step 1
Let's draw some canvas on server side!
We're gonna use this package to draw canvas in nodejs.
Create lib folder.
Define our cover design theme and call it theme.js and put it into lib directory.
// use fonts in your machine
const fontFamily = "Lucida Sans Unicode";
const fontSizes = {
heading: 80,
author: 40,
authorTitle: 26
};
module.exports = {
fontStyles: {
heading: `900 ${fontSizes.heading}px ${fontFamily}`,
author: `700 ${fontSizes.author}px ${fontFamily}`,
authorTitle: `500 ${fontSizes.authorTitle}px ${fontFamily}`
},
fontSizes,
colors: {
primary: "#ffd166",
secondary: "white",
base: "#560bad"
},
avatarSize: 80,
avatarBorder: 5,
logoW: 100,
logoH: 80,
space: 40
};
Step 2
Install canvas package:
npm i --save-dev canvas
or
yarn add canvas
Create index.js file in lib folder that will create canvas and getContext to work with.
const { createCanvas, loadImage } = require("canvas");
const { wrapText } = require("./helpers");
const theme = require("./theme");
// Create canvas and get its context
const canvas = createCanvas(1200, 630);
const ctx = canvas.getContext("2d");
const {
colors,
avatarSize,
avatarBorder,
fontSizes,
fontStyles,
logoW,
logoH,
space
} = theme;
Step 3
Add main part in index.js above. Please read some hints about canvas while reading the code below.
module.exports = async function generateCover({
title,
avatarUrl,
name,
position
}) {
// Load images
const logo = await loadImage(
"https://d2fltix0v2e0sb.cloudfront.net/dev-black.png"
);
const avatar = await loadImage(avatarUrl);
// Background
ctx.fillStyle = colors.base;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Heading text
ctx.fillStyle = colors.secondary;
ctx.font = fontStyles.heading;
wrapText(
ctx,
title,
space,
fontSizes.heading + space,
canvas.width - space * 2,
fontSizes.heading
);
// Avatar
const avatarTop = canvas.height - avatarSize - avatarSize / 2;
const avatarLeft = space;
// Border around avatar
ctx.fillStyle = colors.primary;
ctx.beginPath();
ctx.arc(
avatarSize / 2 + avatarLeft,
avatarSize / 2 + avatarTop,
avatarSize / 2 + avatarBorder,
0,
2 * Math.PI
);
ctx.fill();
ctx.closePath();
// Clip image before draw
ctx.save();
ctx.beginPath();
ctx.arc(
avatarSize / 2 + avatarLeft,
avatarSize / 2 + avatarTop,
avatarSize / 2,
0,
2 * Math.PI
);
ctx.closePath();
ctx.clip();
// Put avatar
ctx.drawImage(avatar, avatarLeft, avatarTop, avatarSize, avatarSize);
// Unclip all around avatar
ctx.beginPath();
ctx.arc(0, 0, avatarSize / 2, 0, Math.PI * 2, true);
ctx.clip();
ctx.closePath();
ctx.restore();
// Author name
ctx.fillStyle = colors.secondary;
ctx.font = fontStyles.author;
ctx.fillText(
name,
avatarLeft + avatarSize + space / 2,
avatarTop + fontSizes.author - 4
);
// Author title
ctx.fillStyle = colors.primary;
ctx.font = fontStyles.authorTitle;
ctx.fillText(
position,
avatarLeft + avatarSize + space / 2,
avatarTop + fontSizes.author + fontSizes.authorTitle
);
// Add logo
ctx.drawImage(
logo,
canvas.width - logoH - 60,
canvas.height - logoH - logoH / 2 + space / 4,
logoW,
logoH
);
// Return PNG Stream
// you can pass pngConfig here
return canvas.createPNGStream();
};
Test it with simple express framework.
const app = require("express")();
const generateCover = require("./lib");
app.get("/generate-cover", async (req, res) => {
try {
const coverStream = await generateCover({
title: "Generate Link Preview Cover with Nodejs",
avatarUrl:
"https://res.cloudinary.com/practicaldev/image/fetch/s--4rczDrsA--/c_fill,f_auto,fl_progressive,h_320,q_auto,w_320/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/152066/eb216eb5-1b78-42fd-8faf-2d5bc69f075c.jpg",
name: "Orkhan Jafarov",
position: "Senior Frontend Developer"
});
res.statusCode = 200;
res.setHeader("Content-Type", "image/png");
res.setHeader("Content-Control", "public, max-age=31536000");
coverStream.pipe(res);
} catch (error) {
res.statusCode = 500;
res.end(error.message);
}
});
app.listen(3000);
Open it in browser
It's working 🔥
Last step
Add social meta tags into your html
<meta property="og:title" content="your_title" />
<meta
property="og:image"
content="http://example.com/generate-cover/uniq-id-1"
/>
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content="your_title" />
<meta
property="twitter:image"
content="http://example.com/generate-cover/uniq-id-1"
/>
Use absolute url to your image source
Done!
Try it yourself on codesandbox!
Check link preview https://0r8qz.sse.codesandbox.io/
here and here
Of course, that's only starter tutorial. There's no saving cover images and return if it's already generated. But that's up to you. For simple project it's enough, you can optimize it if you'll use less colors and it will be VERY fast.
Thank you! ✨
Idea to implement
You can pass your unique key and use a data from your DB. For example.
GET /generate-cover/uniq-id-1
app.get('/generate-cover/:postId', (req, res) => {
const { postId } = req.params;
const {
title,
author: {
avatarUrl,
name,
position
}
} = await db.posts.findOne({ id: postId });
const coverStream = await generateCover({
title,
avatarUrl,
name,
position
});
coverStream.pipe(res);
});
Top comments (3)
Give it a try to flayyer.com ✨ You can create social preview cards with React and Vue
Genius 🎖️👌
If you like it clean and simple you can also try mugshotbot.com 🔥