Skip to content
rasmusp.com

Generate Open Graph Images in Astro

If you’ve ever shared a link on Slack, LinkedIn, or Twitter and thought “hmm… that preview could look better”.

Open Graph images are used by social platforms to display visual previews when a page is shared. By automatically generating these images in Astro, you can ensure consistent, dynamic previews without manually designing an image for each page.

In this guide, I’ll walk you through a practical and flexible way to generate Open Graph images automatically in Astro, using Satori, Sharp, and React. The goal is simple: no manual image design, no forgotten previews — just consistent, on‑brand images for every page.

Setup

Prerequisites

We assume the following basic Astro project structure with dynamic routes:

/src/
/pages/
[...slug].astro

Installation

To generate OG images, install the following packages:

Terminal window
npm i react satori sharp

Generating Open Graph images

Basic Satori setup

Satori is the core library that converts JSX/React components into SVG. It’s designed specifically for server-side rendering and supports a subset of CSS properties and HTML elements.

A minimal Satori example:

import satori from 'satori'
await satori(
'<div style={{ color: "green" }}>Hey world</div>',
{
height: 630,
width: 1200,
}
)

satori() takes two arguments:

This guide focuses on width, height and fonts. Full documentation can be found in the Satori README.

Performance note: Satori is fast, but generating images on every request can impact performance. Consider caching generated images or generating them at build time for static sites.

Implementing generateOgImage()

The generateOgImage() function is the core of our OG image generation. It takes page data (like title, description, etc.) and returns a binary image buffer that can be served as an HTTP response.

Basic function

generateOgImages.ts
import satori from 'satori'
export default async function generateOgImage({ title} : { title: string }) {
const svg = await satori('<div style={{ color: "green" }}>{ title }</div>', {
height: 630,
width: 1200,
})
return new Uint8Array(svg)
}

Why Uint8Array? Satori returns an SVG string, which we convert to a Uint8Array for efficient binary handling. This format works well with Astro’s Response API when serving images.

Error handling: Consider adding try-catch blocks in production to handle potential errors gracefully, especially when dealing with font loading or template rendering.

Adding Fonts

To use custom fonts such as Inter, load the font file and include it in Satori’s font configuration. Using custom fonts is crucial for maintaining brand consistency across your OG images.

Font format considerations:

generateOgImages.ts
import satori from 'satori'
import path from 'node:path'
import { readFile } from 'node:fs/promises'
const InterRegularFontFile = await readFile(path.resolve('public/fonts/InterRegular.woff'))
export default async function generateOgImage({ title} : { title: string }) {
const svg = await satori('<div style={{ color: "green" }}>{ title }</div>', {
const svg = await satori('<div style={{ color: "green", fontFamily: "Inter" }}>{ title }</div>', {
height: 630,
width: 1200,
fonts: [{
name: "Inter",
weight: 400 as const,
style: "normal" as const,
data: InterRegularFontFile
}]
})
return new Uint8Array(svg)
}

Notes:

Building a Template with JSX/TSX

Using JSX allows you to create flexible, reusable templates. This approach separates your design logic from the image generation logic, making it easier to maintain and update your OG images.

template.tsx
// @ts-nocheck
function template(title: string) {
return (
<div style={{ color: 'green', fontFamily: "Inter" }}>
<h1>{ title }</h1>
</div>
)
}
export default template

The @ts-nocheck comment: Satori’s JSX support is a subset of React’s JSX, so TypeScript may complain about certain patterns. The @ts-nocheck comment disables type checking for this file. Alternatively, you can configure your tsconfig.json to be more lenient with JSX.

Styling in Satori: Satori supports a limited subset of CSS properties. Common supported properties include:

Using Tailwind CSS with Satori

Satori supports Tailwind through the tw attribute, which is a convenient way to style components without writing inline styles. This is especially useful if you’re already using Tailwind in your project.

Important limitations:

template.tsx
// @ts-nocheck
function template(title: string) {
return (
<div style={{ color: 'green', fontFamily: "Inter" }}>
<div
style={{ fontFamily: "Inter" }}
tw="text-green-500 flex h-full w-full items-center justify-center">
<h1>{ title }</h1>
</div>
)
}
export default template

Mixing styles and Tailwind: You can combine inline style props with tw attributes. Inline styles take precedence, which is useful for dynamic values or properties not supported by Tailwind.

And this is how we will use it:

generateOgImages.ts
import satori from 'satori'
import path from 'node:path'
import { readFile } from 'node:fs/promises'
import template from 'template.tsx'
const InterRegularFontFile = await readFile(path.resolve('public/fonts/InterRegular.woff'))
export default async function generateOgImage({ title} : { title: string }) {
const svg = await satori('<div style={{ color: "green", fontFamily: "Inter" }}>{ title }</div>', {
const svg = await satori(
template( title ),
{
height: 630,
width: 1200,
fonts: [{
name: "Inter",
weight: 400 as const,
style: "normal" as const,
data: InterRegularFontFile
}]
})
return new Uint8Array(svg)
}

Converting SVG to PNG / WebP

Satori outputs SVG, but social platforms typically prefer raster formats like PNG or WebP. SVG files can be larger and may not render consistently across all platforms.

Use Sharp to convert:

generateOgImages.ts
import satori from 'satori'
import path from 'node:path'
import { readFile } from 'node:fs/promises'
import template from 'template.tsx'
import sharp from 'sharp'
const InterRegularFontFile = await readFile(path.resolve('public/fonts/InterRegular.woff'))
export default async function generateOgImage({ title} : { title: string }) {
const svg = await satori(
template( title ),
{
height: 630,
width: 1200,
fonts: [{
name: "Inter",
weight: 400 as const,
style: "normal" as const,
data: InterRegularFontFile
}]
})
const webp = await sharp(Buffer.from(svg)).webp().toBuffer()
return new Uint8Array(svg)
return new Uint8Array(webp)
}

Sharp configuration: You can customize the output quality:

const webp = await sharp(Buffer.from(svg))
.webp({ quality: 90 }) // Adjust quality (1-100)
.toBuffer()

Performance tip: Sharp operations are asynchronous and CPU-intensive. For high-traffic sites, consider caching generated images or using a CDN.

Astro GET Integration

To serve OG images in Astro, we create a dynamic API route that generates images on-demand. This route will be called whenever a social media platform requests an OG image.

File structure:

Our new route structure:

/src/
/pages/
/...slug/
og.webp.ts
[...slug].astro

The [...slug] pattern matches any path, and the og.webp.ts file will handle requests to /{slug}/og.webp.

Implementation:

Here is the code for the GET route for the image:

import { getCollection } from 'astro:content'
import generateOgImage from 'YOUR-PATH/generateOgImage.ts'
import type { APIRoute } from 'astro'
export async function getStaticPaths() {
const pages = await getCollection('pages')
return pages.map((page) => ({
params: { slug: page.id },
props: { page },
}))
}
export const GET: APIRoute = async function get({ props }) {
const image = await generateOgImage({
title: props.page.data.title,
})
return new Response(image, {
headers: {
'Content-Type': 'image/webp',
'Cache-Control': 'public, max-age=31536000, immutable' // Cache for 1 year
},
})
}

Key points:

Error handling: Consider adding error handling for missing pages or generation failures:

export const GET: APIRoute = async function get({ props }) {
try {
const image = await generateOgImage({
title: props.page.data.title,
})
return new Response(image, {
headers: {
'Content-Type': 'image/webp',
'Cache-Control': 'public, max-age=31536000, immutable'
},
})
} catch (error) {
return new Response('Failed to generate image', { status: 500 })
}
}

Dynamic routes: If you’re using server-side rendering (SSR), you can use getStaticPaths() with fallback: 'blocking' or handle dynamic routes differently.

Performance considerations

A few practical notes before you ship this to production:

In most cases, the performance impact is minimal — especially compared to the consistency you gain.

Conclusion

By combining Satori, Sharp, and React, you’ve created a powerful system for generating Open Graph images in Astro.

This workflow ensures that every page in your Astro site gets a beautiful, consistent OG image — automatically. Your social media shares will now stand out with professional, on-brand preview images that accurately represent your content.