Generate beautiful og images to your blog posts in astro using satori
Open Graph (OG) images are crucial for link sharing - they’re the visual preview that appears when someone shares your blog post on Twitter, LinkedIn, Facebook, Slack, iMessage, Discord, or any other platform that supports rich link previews. Instead of manually creating these images for every post, I’ve automated the process using Satori and Astro. Here is the OpenGraph image for this post as an example:
In this post, I’ll show you exactly how I implemented automatic OG image generation for this blog, complete with code examples you can copy and adapt for your own projects.
What is Satori?
Satori is a library developed by Vercel that converts HTML and CSS into SVG. It’s perfect for generating images programmatically because:
- ✅ It supports most CSS features you’d need for image generation
- ✅ Works in both Node.js and edge environments
- ✅ Generates crisp SVG output that can be converted to PNG
- ✅ Supports custom fonts and complex layouts
Project Setup
First, let’s install the required dependencies:
npm install satori @resvg/resvg-js
- satori: Generates SVG from HTML/CSS-like objects
- @resvg/resvg-js: Converts SVG to PNG format
The Implementation
Here’s how I structured the OG image generation system in my Astro blog:
1. Main Generator Function
// src/utils/generateOgImage.js
import { Resvg } from "@resvg/resvg-js";
import postOgImage from "./og-template/post.js";
function svgBufferToPngBuffer(svg) {
const resvg = new Resvg(svg);
const pngData = resvg.render();
return pngData.asPng();
}
export async function generateOgImageForPost({ post }) {
const svg = await postOgImage(post);
return svgBufferToPngBuffer(svg);
}
This function takes a post object and returns a PNG buffer. Simple and clean! 🎨
2. The Template Engine
Here’s where the magic happens - the actual template that generates our OG images:
// src/utils/og-template/post.js
import satori from "satori";
import path from "path";
import fs from "fs";
function safeText(text) {
const emojiPattern = /[^\x00-\x7F]+/gu;
return text.replace(emojiPattern, "").trim();
}
export default async post => {
const robotoFontPath = path.resolve("./public/fonts/roboto-bold.ttf");
const robotoFontBuffer = fs.readFileSync(robotoFontPath);
// Get the background image as base64
const bgImagePath = path.resolve("./public/images/og_bg.png");
const bgImageBuffer = fs.readFileSync(bgImagePath);
// Create the structure using vanilla JS objects instead of JSX
const svg = await satori(
{
type: "div",
props: {
style: {
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
background: `url('data:image/png;base64,${bgImageBuffer.toString("base64")}')`,
backgroundSize: "cover",
backgroundPosition: "center",
position: "relative",
},
children: [
{
type: "p",
props: {
style: {
fontSize: 82,
fontWeight: "bold",
color: "#222222",
textAlign: "center",
maxWidth: "85%",
maxHeight: "100%",
overflow: "hidden",
fontFamily: "Roboto",
textShadow: "1px 3px 6px rgba(0,0,0,0.2)",
},
children: safeText(post.data.title),
},
},
],
},
},
{
width: 1200,
height: 630,
embedFont: true,
fonts: [
{
name: "Roboto",
data: robotoFontBuffer,
style: "bold",
},
],
}
);
return svg;
};
Key Implementation Details:
🎯 Font Handling: I load Roboto Bold from the public folder and embed it directly in the SVG for consistent rendering.
🖼️ Background Image: The template uses a background image (og_bg.png
) that I created to match my blog’s branding.
✂️ Text Sanitization: The safeText
function removes emojis and non-ASCII characters to prevent rendering issues.
📏 Optimal Dimensions: 1200x630 pixels is the recommended size for most social platforms.
3. Astro Integration
This is how we integrate the generator with Astro’s routing system:
// src/pages/[slug]/og.png.js
import { getCollection } from "astro:content";
import { generateOgImageForPost } from "../../utils/generateOgImage.js";
export async function getStaticPaths() {
const posts = await getCollection("blog", ({ data }) => !data.hidden);
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
}
export async function GET({ props }) {
const image = await generateOgImageForPost(props);
return new Response(image, {
headers: { "Content-Type": "image/png" },
});
}
// Set to true to ensure static generation at build time
export const prerender = true;
This creates a route like /my-blog-post/og.png
for each blog post that generates and serves the OG image. The beauty of this approach is that Astro renders all images at build time with prerender: true
, which means they’re served statically with incredible efficiency without needing to re-render on every request.
Using the Generated Images
In your blog post layouts, reference the generated OG image:
---
// In your blog post layout
const ogImageUrl = `${Astro.site}${Astro.params.slug}/og.png`;
---
<head>
<meta property="og:image" content={ogImageUrl} />
<meta name="twitter:image" content={ogImageUrl} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
</head>
Performance Considerations
⚡ Build-time Generation: Using prerender: true
ensures all OG images are generated at build time, not on request.
🗜️ File Size: PNG images are around 50-100KB each, which is acceptable for social media previews.
🚀 CDN Friendly: Since images are pre-generated, they can be served efficiently from your CDN.
Make it your own
While this post focuses on my Astro implementation, the core Satori-based generation logic can easily be used as a standalone Node.js script. The generateOgImageForPost()
function and template system are framework-agnostic and can run anywhere Node.js is available.
Here are some alternative approaches:
🏗️ Build-time Generation: Run the generator as a build script with any CMS (WordPress, Strapi, Contentful, etc.) and save the images to your static assets folder.
📦 Standalone Package: Extract the generation logic into a reusable npm package for use across different projects.
⚡ Serverless Functions: Deploy the generator as a serverless function for on-demand image generation.
🔄 CI/CD Integration: Include the generator in your deployment pipeline to automatically create OG images for new content.
The beauty of Satori is that it’s not tied to any specific framework - you can generate beautiful OG images anywhere you can run JavaScript!
You can also extend this system further by:
🎨 Multiple Templates: Create different templates for different post categories 🏷️ Tag-based Styling: Change colors or layouts based on post tags 👤 Author Images: Add author avatars to the generated images 📊 Dynamic Backgrounds: Generate different background patterns programmatically
Automatic OG image generation with Satori and Astro is a game-changer for content creators. It ensures every blog post has a professional-looking social media preview without the manual work of designing each image.
The setup takes some initial effort, but once it’s running, you’ll never have to think about OG images again. Your social media shares will look consistent and professional, potentially increasing click-through rates and engagement. You can check out the complete implementation in my blog’s GitHub repository.
Related Posts
- 9 min readAstro vs WordPress: A Performance Comparison After Migrating My Blog
- 6 min readYou May Not Need React
- 4 min readCoolest WASM Superpowers in Your Browser
- 2 min readVibe-coding a Headless News Website with Arc XP's View API in 2 hours
- 9 min readWordPress to MDX (Astro) migration script
- 8 min readI moved my blog from WordPress to Astro
Share