DEV Community

Cover image for Generate OpenGraph Images Dynamically
Reuben Tier
Reuben Tier

Posted on • Edited on • Originally published at blog.otterlord.dev

Generate OpenGraph Images Dynamically

This tutorial was originally posted on my new blog. Check it out here and watch out for new and exclusive articles soon.

Vercel's OG image generation library is an awesome tool for dynamically generating images for your website's content. It's a great way to make your content stand out on social media. In this post, we'll look at what makes up Vercel's OG image generation library and how we can recreate it without being limited to Vercel.
I'll be using Astro, but the general concepts can be applied to any framework.

What makes up Vercel's OG image generation library?

Satori

Satori is the library used in the @vercel/og package and is also maintained by Vercel. Satori takes a JSX component, and renders that into an SVG.

ReSVG

ReSVG is a library for SVGs and can be used to render SVGs to PNG images. ReSVG is used in the @vercel/og package to render the SVGs generated by Satori into PNG images.

Installing Some Dependencies

We'll use Satori & ReSVG to recreate the exact same functionality, but we will add one more thing. Satori-HTML is a library by Nate Moore to convert raw HTML into JSX suitable for Satori. This will allow us to use HTML to generate our images.

npm install satori satori-html @resvg/resvg-js
Enter fullscreen mode Exit fullscreen mode

Loading Fonts

Satori requires that you load the fonts you want to use in your SVG. Download the font(s) you want to use, and import them into your project. I'll be using Open Sans for this example.

We need to add a Vite plugin to load the font files. I can't remember where exactly I first found the code, but the first instance of it I could find when coming back to writing this is from geoffrich/sveltekit-satori, so thanks to them for making my day much easier :D

Navigate to astro.config.mjs and add the following method. Also, we need to exclude the @resvg/resvg-js package from Vite's dependency optimization, otherwise we get some odd behaviour.

export default defineConfig({
  ...

  vite: {
    plugins: [rawFonts(['.ttf'])],
    optimizeDeps: { exclude: ['@resvg/resvg-js'] }
  },
});

function rawFonts(ext) {
  return {
    name: 'vite-plugin-raw-fonts',
    transform(_, id) {
      if (ext.some(e => id.endsWith(e))) {
        const buffer = fs.readFileSync(id);
        return {
          code: `export default ${JSON.stringify(buffer)}`,
          map: null
        };
      }
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

Creating an Endpoint

We'll create an endpoint to generate our images at /api/og.png.ts. Again, this can be done with any framework, but I'll be using Astro. Import satori, satori-html, and @resvg/resvg-js into the file. Also, import any fonts you want to use (at least one).

import satori from 'satori';
import { html } from 'satori-html';
import { Resvg } from '@resvg/resvg-js';
import OpenSans from '../../../lib/OpenSans-Regular.ttf'
Enter fullscreen mode Exit fullscreen mode

Then define a function get to handle the request. Use a html tagged template to take some HTML and return a JSX component. You can either pass inline styles, or use Tailwind classes by including the tw prop.

export async function get() {
  const out = html`<div tw="flex flex-col w-full h-full bg-white">
    <h1 tw="text-6xl text-center">Hello World</h1>
  </div>`
}
Enter fullscreen mode Exit fullscreen mode

Continue by rendering the JSX component to an SVG using Satori. You can pass in the fonts to Satori, as well as a height and width for the SVG.

  let svg = await satori(out, {
    fonts: [
      {
        name: 'Open Sans',
        data: Buffer.from(OpenSans),
        style: 'normal'
      }
    ],
    height: 630,
    width: 1200
  });
Enter fullscreen mode Exit fullscreen mode

Use new Resvg to load the SVG into Resvg, passing any arguments. Call .render() to render the SVG to an image.

  const resvg = new Resvg(svg, {
    fitTo: {
      mode: 'width',
      value: width
    }
  });

  const image = resvg.render();
Enter fullscreen mode Exit fullscreen mode

Finally, return the image as a PNG, and set the headers for content-type and cache the image if your usecase is static.

  return {
    headers: {
      'Content-Type': 'image/png',
      'Cache-Control': 'public, max-age=31536000, immutable'
    },
    body: image.asPng()
  }
Enter fullscreen mode Exit fullscreen mode

That's it!

Navigate to http://localhost:3000/api/og.png to view the image. You can modify the HTML to see how it affects the image. You can use this to generate OG images at build time, or add SSR support to generate images at runtime time.


Thanks for reading. If you have any questions, feel free to reach out to me on Twitter or my brand new Discord server.

Top comments (0)