How I generate an Open Graph image for every post
By Flavio Copes
How I generate a unique Open Graph preview image for every blog post at build time in Astro, using astro-og-canvas, instead of one shared image.
When you share a link on X, LinkedIn, or in a chat, the preview card you see comes from the page’s Open Graph image.
For years I used a single image for my whole site. Every shared link looked the same.
Now each post gets its own image, with the post title on it, generated automatically at build time. Here’s how.
The tool
I use astro-og-canvas. You give it a title and a description, and it draws a card and saves it as a PNG.
It runs during the build, so the images are plain static files. No runtime, no external service.
The image route
Astro can generate files from a route. I created src/pages/og/[...route].ts and pointed it at my posts:
import { OGImageRoute } from 'astro-og-canvas'
import { getCollection } from 'astro:content'
const posts = await getCollection('posts')
const pages = Object.fromEntries(posts.map((post) => [post.id, post.data]))
export const { getStaticPaths, GET } = await OGImageRoute({
param: 'route',
pages,
getImageOptions: (path, page) => ({
title: page.title,
description: page.description,
}),
})
This generates one image per post at /og/<slug>.png.
Notice the await before OGImageRoute. It’s an async function, and if you forget it, Astro can’t find getStaticPaths and the build fails. That one tripped me up.
Pointing the tags at it
The last step is telling each page to use its image. In my <head> I set the Open Graph and Twitter tags to the generated URL:
<meta property="og:image" content="https://flaviocopes.com/og/the-slug.png" />
<meta name="twitter:image" content="https://flaviocopes.com/og/the-slug.png" />
Now every post has a unique card. You can see one by opening any /og/<slug>.png directly.
A word on build time
Generating an image for every post adds time to the build. With 1700+ posts, that’s real.
astro-og-canvas caches each image, so unchanged posts aren’t redrawn. But on Cloudflare Pages there’s a catch with where that cache lives, and I cover it in the next post.
Related posts about astro: