Mastering Dynamic OG Image Generation with Satori
Open Graph images are the unsung heroes of social media engagement. When someone shares your blog post on Twitter, LinkedIn, Facebook, or Discord, the OG image is often what determines whether people click through or scroll past. Yet creating these images manually for every post is tedious and time-consuming.
In this comprehensive guide, we’ll explore how to automatically generate beautiful, branded OG images using Satori and AstroPaper, eliminating manual design work while maintaining visual consistency across your content.
Understanding Open Graph Images
Before diving into implementation, let’s understand why OG images matter and what makes them effective.
The Psychology of Social Sharing
When users encounter a shared link in their feed, they make a split-second decision about whether to engage. Research shows that posts with compelling images receive:
- 2.3x more engagement on Twitter compared to text-only posts
- 2.5x higher click-through rates on LinkedIn
- 87% of interactions on Facebook are with posts containing images
Your OG image is prime real estate. It’s often larger than the accompanying text and can communicate your post’s value proposition instantly.
Anatomy of an Effective OG Image
Great OG images share common characteristics:
| Element | Purpose | Best Practice |
|---|---|---|
| Title | Communicate topic | Large, readable text |
| Branding | Build recognition | Consistent logo/colors |
| Visual hierarchy | Guide attention | Clear focal point |
| Dimensions | Prevent cropping | 1200x630 pixels |
| Contrast | Ensure readability | Dark text on light bg (or vice versa) |
How Satori Works
Satori is a library that converts HTML and CSS to SVG. This might sound simple, but it’s revolutionary for dynamic image generation.
The Satori Pipeline
React/HTML Template → Satori → SVG → Sharp/resvg → PNG
Here’s what happens at each stage:
- Template Definition: You define your image layout using React components or HTML
- Satori Processing: Converts the markup to SVG, handling layout calculations
- Rasterization: Sharp or resvg converts the SVG to PNG for universal compatibility
Why Not Just Use Canvas?
You might wonder why we don’t use HTML Canvas directly. The answer is portability and developer experience:
// Canvas approach: Manual positioning, no CSS
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#000';
ctx.font = 'bold 48px sans-serif';
ctx.fillText('My Title', 50, 100);
// Painful for complex layouts...
// Satori approach: Familiar HTML/CSS
const element = (
<div style={{ display: 'flex', padding: 40 }}>
<h1 style={{ fontSize: 48, fontWeight: 'bold' }}>
My Title
</h1>
</div>
);
Satori lets you use Flexbox, padding, margins, and other CSS properties you already know. It’s also SSR-friendly and runs in Node.js without browser APIs.
AstroPaper’s Implementation
AstroPaper includes a sophisticated OG image generation system. Let’s examine how it works.
The Generation Endpoint
AstroPaper creates an endpoint at /posts/[slug]/index.png for each post:
// src/pages/posts/[...slug]/index.png.ts
import { getCollection } from 'astro:content';
import { generateOgImage } from '@/utils/generateOgImage';
export async function getStaticPaths() {
const posts = await getCollection('posts');
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
}
export async function GET({ props }) {
const { post } = props;
const png = await generateOgImage({
title: post.data.title,
description: post.data.description,
pubDate: post.data.pubDatetime,
});
return new Response(png, {
headers: { 'Content-Type': 'image/png' },
});
}
The Satori Template
The magic happens in the template that Satori renders:
const OgTemplate = ({ title, description, pubDate }) => (
<div
style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
height: '100%',
padding: 60,
background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)',
color: '#ffffff',
}}
>
<div style={{ display: 'flex', marginBottom: 'auto' }}>
<img src={logoBase64} width={48} height={48} />
<span style={{ marginLeft: 16, fontSize: 24 }}>TechPulse</span>
</div>
<h1 style={{
fontSize: 64,
fontWeight: 'bold',
lineHeight: 1.2,
marginBottom: 24,
}}>
{title}
</h1>
<p style={{ fontSize: 28, opacity: 0.8 }}>
{description}
</p>
<div style={{ marginTop: 'auto', fontSize: 20, opacity: 0.6 }}>
{formatDate(pubDate)}
</div>
</div>
);
Font Loading
Satori requires font files to render text. AstroPaper handles this elegantly:
import { readFile } from 'fs/promises';
import path from 'path';
const fontPath = path.resolve('./public/fonts/inter-semibold.woff');
const fontData = await readFile(fontPath);
const svg = await satori(element, {
width: 1200,
height: 630,
fonts: [
{
name: 'Inter',
data: fontData,
weight: 600,
},
],
});
Customizing OG Images
Let’s explore how to customize the generated images for your brand.
Custom Color Schemes
Modify the background gradient to match your brand:
const brandGradients = {
tech: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
nature: 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)',
sunset: 'linear-gradient(135deg, #ff6b6b 0%, #feca57 100%)',
corporate: 'linear-gradient(135deg, #2c3e50 0%, #4ca1af 100%)',
};
Tag-Based Styling
Different post categories can have different visual treatments:
const getStyleByTag = (tags: string[]) => {
if (tags.includes('javascript')) {
return { accent: '#f7df1e', icon: jsIcon };
}
if (tags.includes('react')) {
return { accent: '#61dafb', icon: reactIcon };
}
if (tags.includes('astro')) {
return { accent: '#ff5d01', icon: astroIcon };
}
return { accent: '#6366f1', icon: defaultIcon };
};
Dynamic Backgrounds
For visual variety, you can generate backgrounds based on the post title:
const generateBackground = (title: string) => {
// Create a deterministic color from the title
const hash = title.split('').reduce((acc, char) => {
return char.charCodeAt(0) + ((acc << 5) - acc);
}, 0);
const hue = Math.abs(hash) % 360;
return `hsl(${hue}, 70%, 15%)`;
};
Performance Considerations
Build-Time Generation
OG images are generated at build time in Astro’s SSG mode. This means:
- Zero runtime cost: Images are pre-generated static files
- Build time impact: Each image takes ~100-200ms to generate
- Caching benefits: Images are cached by CDNs like any static asset
For a blog with 100 posts, expect ~15-30 seconds additional build time.
Optimizing Generation Speed
Several strategies can improve build performance:
// Use a shared Satori instance
const satori = await import('satori');
const resvg = await import('@resvg/resvg-js');
// Pre-load fonts once
const fonts = await loadFonts();
// Generate images in parallel (with concurrency limit)
import pLimit from 'p-limit';
const limit = pLimit(5); // 5 concurrent generations
const images = await Promise.all(
posts.map(post => limit(() => generateOgImage(post)))
);
Best Practices
1. Test Across Platforms
Different platforms display OG images differently:
- Twitter: Crops to 2:1 aspect ratio for summary_large_image
- LinkedIn: May add rounded corners
- Discord: Shows full image with title overlay
- Slack: Unfurls with metadata below
Use tools like opengraph.xyz to preview your images.
2. Keep Text Readable
Ensure sufficient contrast and font size:
// Minimum readable sizes
const typography = {
title: { fontSize: 48, minContrast: 4.5 },
subtitle: { fontSize: 24, minContrast: 3.0 },
meta: { fontSize: 18, minContrast: 3.0 },
};
3. Include Branding Consistently
Every OG image should reinforce your brand:
- Logo in a consistent position
- Brand colors in the palette
- Typography matching your site
4. Handle Long Titles
Implement text truncation for very long titles:
const truncateTitle = (title: string, maxLength = 80) => {
if (title.length <= maxLength) return title;
return title.slice(0, maxLength - 3) + '...';
};
Debugging OG Images
When things go wrong, here’s how to diagnose issues:
Missing Images
Check that the endpoint is generating correctly:
curl -I https://yoursite.com/posts/your-post/index.png
Font Rendering Issues
Ensure fonts are loaded correctly:
console.log('Font buffer size:', fontData.byteLength);
console.log('Font loaded:', fontData.byteLength > 0);
Layout Problems
Render to SVG first and inspect in browser:
const svg = await satori(element, options);
fs.writeFileSync('debug.svg', svg);
// Open debug.svg in browser to inspect
Conclusion
Dynamic OG image generation transforms a tedious manual task into an automated part of your build process. With Satori and AstroPaper, you get:
- Consistency: Every post has a professionally styled preview
- Efficiency: Zero manual design work per post
- Flexibility: Easy to customize for your brand
- Performance: Pre-generated at build time
The investment in setting up dynamic OG images pays dividends in social media engagement and brand recognition. Your posts will stand out in crowded feeds, driving more traffic to your content.
Next Steps: Check out our guide on SEO Optimization for Astro Sites to learn more about maximizing your content’s discoverability.