How to auto-generate OpenGraph images

·

Ever seen those nice image previews when you share a link on Twitter, LinkedIn, Mastodon, etc? Those are called OpenGraph images, more on that in a minute. Creating those images, is however, time-consuming. Especially if you are focused on writing. I used to use Unsplash images, which are free, up until the moment I shared this very important issue, that I’ve opened on GitHub, to my LinkedIn followers.

The image you see when you share any GitHub issue—is auto generated, and contains the information about the issue itself. Ain’t this cool? We can do the same!

What is OpenGraph?

OpenGraph is a protocol that enables any web page to become a rich object in a social graph - https://ogp.me/

The idea was created, and popularized by Facebook, but in a nutshell—it’s a bunch of meta tags, that different platforms such as Twitter, or LinkedIn—can read, and generate preview cards based on them. They were initially made to index the content inside a graph, but we care only about the preview card.

You can visit the official website to learn more about all the meta tags. We, however, will focus only on one: og:image. And because back in the days when Facebook was popular, Twitter tried to compete with it, it’s no surprise that Twitter has its own set of meta tags. For our use case, we will focus on twitter:image meta tag. You can read more about Twitter tags here. Also keep in mind that Twitter can parse OpenGraph tags, in case Twitter tags are not present.

How to auto-generate the image

Meet satori. Satori is an extremely cool library that converts HTML and CSS into SVG images. One thing to keep in mind is that satori supports a sub-set of the CSS properties, so you can’t go too wild. Refer to this table to learn more.

satori also supports JSX, but I don’t see a reason to bring an entire Frontend framework, just to render one image. Instead, we will use satori-html which is an HTML adapter this will allow us to write plain HTML.

First, install the libraries:

yarn add satori satori-html

As an example, we will use my website’s OpenGraph image which can be found here. I’ll spit the code first, and then we will discuss it.

const markup = html`
  <div
    style="width: 1200px; height: 630px; display: flex; flex-direction: column; flex-wrap: wrap; justify-content: center; align-items: center; padding: 0 24px;"
  >
    <img
      src="https://www.yieldcode.blog/images/cover.jpg"
      width="500"
      height="500"
      style="width: 150px; height: 150px; border: 1px solid white; border-radius: 50%;"
    />
    <h1 style="color: rgb(17,24,39); font-size: 5.5rem; font-weight: 700">
      ${SITE_AUTHOR}
    </h1>
    <h2 style="color: rgb(82,82,82); font-size: 2.5rem; font-weight: 700">
      ${SITE_AUTHOR_TITLE}
    </h2>
    <h3 style="color: rgb(82,82,82); font-size: 1.5rem; font-weight: 400">
      ${SITE_DESCRIPTION}
    </h3>
  </div>
`;

const svg = await satori(markup, {
  width: 1200,
  height: 630,
  embedFont: true,
  fonts: [
    {
      name: "Inter",
      data: interRegularData,
      weight: 400,
      style: "normal",
    },
    {
      name: "Inter",
      data: interBoldData,
      weight: 700,
      style: "normal",
    },
  ],
});

The first thing we notice straight away, is that satori does not support classes, and instead requires the usage of style attributes.

The html` thingy comes from satori-html:

import { html } from "satori-html";

Handling fonts

The other thing we notice—is how fonts are handled. Since we do not render complete HTML page, fonts are not handled through the head tags. Instead, we need to load the fonts manually, and feed them to satori. In my case I use the Inter font in its regular and bold variations. This means that I need to get the font files and read them.

import fs from "fs/promises";

const [interRegularData, interBoldData] = await Promise.all([
  fs.readFile("./src/assets/fonts/inter/Inter-Regular.woff"),
  fs.readFile("./src/assets/fonts/inter/Inter-Bold.woff"),
]);

To SVG or not to SVG?

We could stop there, but Twitter being Twitter:

URL of image to use in the card. Images must be less than 5MB in size. JPG, PNG, WEBP and GIF formats are supported. Only the first frame of an animated GIF will be used. SVG is not supported.

So for the sake of compatibility, it would be best to convert the SVG to PNG. Meet sharp—a high performance Node.js image processing library. Sharp can do a lot of things such as: converting one type to another, resize, and rotate images.

The conversion is straight forward:

const png = await sharp(Buffer.from(svg)).png().toBuffer();

That’s it. What’s left now, is sending the png as a body of the response, together with the correct header: Content-Type: image/png. If you are familiar with express-like libraries, this should be easy for you.

Bonus - Astro

I <3 Astro. I think it’s a great framework to build static websites. And it’s not a surprise that all my websites, including this blog, are powered by Astro. It can be a bit tricky to add this functionality to Astro, so I decided to dedicate a section for this.

Astro has a notion of APIRoute. APIRoute is a away to generate non HTML pages, such as: RSS feeds, sitemaps, and images. Following our example image, we would like to create a new file, inside our pages folder, named og.png.ts. The content of this file is very simple:

import type { APIRoute } from "astro";

export const get: APIRoute = async function get() {
  // read fonts

  // created markup

  // generate svg with satori

  // convert svg to png with sharp

  return new Response(png, {
    status: 200,
    headers: {
      "Content-Type": "image/png",
    },
  });
};

And that’s it. Upon building your static website, Astro will also generate a file named og.png which will be the image that satori generated from your HTML markup, and sharp converted to PNG.

Bonus 2

It’s even possible to generate multiple images for an Astro collection. The same way Astro generates HTML pages from your collection, you can generate a cover image for each post. Suppose you have your posts inside src/pages/[slug]/index.astro, you can create a new file named src/pages/[slug]/cover.png.ts with the following content:

export async function getStaticPaths() {
  const posts = await getCollection("post");
  return posts.map((entry) => ({
    params: { slug: entry.slug },
    props: { post: entry },
  }));
}

export const get: APIRoute = async function get({ props: { post } }) {
  // do whatever you want with post - extract the content, excerpt, date, tags
  // load fonts
  // create html markup
  // create svg with satori
  // create png with sharp
  // send response
};

The meta tags

One really final touch—don’t forget to add the proper meta tags:

<meta property="og:image" content="https://www.yieldcode.blog/og.png" />

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://www.yieldcode.blog/og.png" />

Share this:

Published by

Dmitry Kudryavtsev

Dmitry Kudryavtsev

Senior Software Engineer / Tech Lead / Consultant

With more than 14 years of professional experience in tech, Dmitry is a generalist software engineer with a strong passion to writing code and writing about code.


Technical Writing for Software Engineers - Book Cover

Recently, I released a new book called Technical Writing for Software Engineers - A Handbook. It’s a short handbook about how to improve your technical writing.

The book contains my experience and mistakes I made, together with examples of different technical documents you will have to write during your career. If you believe it might help you, consider purchasing it to support my work and this blog.

Get it on Gumroad or Leanpub


From Applicant to Employee - Book Cover

Were you affected by the recent lay-offs in tech? Are you looking for a new workplace? Do you want to get into tech?

Consider getting my and my wife’s recent book From Applicant to Employee - Your blueprint for landing a job in tech. It contains our combined knowledge on the interviewing process in small, and big tech companies. Together with tips and tricks on how to prepare for your interview, befriend your recruiter, and find a good match between you and potential employer.

Get it on Gumroad or LeanPub