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].astroInstallation
To generate OG images, install the following packages:
- Satori - Converts a React/JSX template into an SVG. It’s a lightweight library that renders JSX to SVG, making it perfect for server-side image generation.
- Sharp - A high-performance image processing library that converts the SVG into PNG, WebP, JPG or other formats. Sharp is much faster than alternatives like jimp or native Node.js image processing.
- React - Used to build the template in JSX/TSX. Even though you’re not building a React app, React is required as a peer dependency for Satori to parse JSX syntax.
npm i react satori sharpGenerating 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:
- element — HTML string or JSX/TSX component. You can pass a string directly, but using JSX components gives you more flexibility and type safety.
- options — Configuration object with width, height, fonts, styles, and more. The most important options are:
- width and height - Dimensions in pixels. In this guide we set it to the recommend size 1200x630. This size works well across all major social platforms including Twitter, Facebook, LinkedIn, and Discord.
- fonts - Array of font definitions for custom typography
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
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:
- Satori works with .woff, .ttf, and .oft fonts. .ttf and oft are faster to load and parse, but they are typically larger in size. .woff is a good balance of size and parsing speed.
- You can load multiple font weights (regular, bold, etc.) by adding multiple font objects
- Font files should be placed in your public directory or imported as static assets
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:
- The font object must include name, weight, style, and data. The as const assertions ensure TypeScript recognizes the literal types.
- The loaded font has to be an ArrayBuffer (web) or Buffer (Node.js). readFile returns a Buffer in Node.js environments.
- Multiple weights: To use bold or italic variants, add additional font objects to the array with different weights/styles.
- Font loading: Fonts are loaded at module initialization. For better performance, consider lazy loading fonts only when needed.
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.
// @ts-nocheckfunction template(title: string) { return ( <div style={{ color: 'green', fontFamily: "Inter" }}> <h1>{ title }</h1> </div> )}
export default templateThe @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:
- Layout: display, flexDirection, justifyContent, alignItems
- Spacing: padding, margin, gap
- Typography: fontSize, fontWeight, lineHeight, color
- Background: backgroundColor, backgroundImage
- Borders: border, borderRadius
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:
- Not all Tailwind classes are supported
- Complex utilities like backdrop-blur may not work
- Always test your design to ensure it renders correctly
// @ts-nocheckfunction 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 templateMixing 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:
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:
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].astroThe [...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:
- getStaticPaths() pre-generates all routes at build time for static sites
- The GET function receives the page data via props
- We return a Response with the image buffer and appropriate headers
- Caching headers: The Cache-Control header tells browsers and CDNs to cache the image, reducing server load
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:
- On static sites, images are generated at build time, so there’s no runtime cost
- On SSR sites, consider caching or serving images through a CDN
- Keep an eye on generation time if your templates become more complex
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.