OG Image Generation with Astro

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

Terminal window
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 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

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). You can find fonts to download on Google Fonts.

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.

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>

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

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();

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

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

That’s it!

Navigate to http://localhost:4321/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. Check out the source code for this post on GitHub.

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