Generate OpenGraph Images Dynamically
Tags: javascript, webdev, astro, seo
Published: 2023-01-02 22:54:07

> This tutorial was originally posted on my new blog. Check it out [here](https://blog.otterlord.dev/post/dynamic-opengraph/) and watch out for new and exclusive articles soon.

Vercel's [OG image generation](https://vercel.com/docs/concepts/functions/edge-functions/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](https://github.com/natemoo-re/satori-html) is a library by [Nate Moore](https://github.com/natemoo-re) to convert raw HTML into JSX suitable for Satori. This will allow us to use HTML to generate our images.

```sh
npm install satori satori-html @resvg/resvg-js
```

## 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](https://fonts.google.com/specimen/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](https://github.com/geoffrich/sveltekit-satori/blob/main/vite.config.js#L12), 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.

```ts
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
        };
      }
    }
  };
}
```

## 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).

```ts
import satori from 'satori';
import { html } from 'satori-html';
import { Resvg } from '@resvg/resvg-js';
import OpenSans from '../../../lib/OpenSans-Regular.ttf'
```

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.

```ts
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>`
}
```

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.

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

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

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

  const image = resvg.render();
```

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

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

## 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](https://twitter.com/theotterlord) or my brand new [Discord](https://dsc.gg/otterlord) server.