Generate OpenGraph Images Dynamically
Reuben Tier
Posted on January 2, 2023
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
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).
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>
</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.
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 {
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 or my brand new Discord server.
Posted on January 2, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.