⚡️ Generate Link Preview Cover with Nodejs

orkhanjafarovr

Orkhan Jafarov

Posted on December 22, 2020

⚡️ Generate Link Preview Cover with Nodejs

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 🔥

result we have to reach

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
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Open it in browser

cover 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"
/>
Enter fullscreen mode Exit fullscreen mode

Use absolute url to your image source

Done!

Try it yourself on codesandbox!

Check link preview https://0r8qz.sse.codesandbox.io/ here and here

twitter preview

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);
});
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
orkhanjafarovr
Orkhan Jafarov

Posted on December 22, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related