Skip to content
TechPulse Blog
Go back

Mastering Dynamic OG Image Generation with Satori

Updated:

By Marcus Rodriguez

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:

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:

ElementPurposeBest Practice
TitleCommunicate topicLarge, readable text
BrandingBuild recognitionConsistent logo/colors
Visual hierarchyGuide attentionClear focal point
DimensionsPrevent cropping1200x630 pixels
ContrastEnsure readabilityDark 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:

  1. Template Definition: You define your image layout using React components or HTML
  2. Satori Processing: Converts the markup to SVG, handling layout calculations
  3. 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:

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:

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:

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:

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.


Share this post:

Previous Post
Building Accessible Web Components: A Developer's Complete Guide
Next Post
Mastering Astro Content Collections: A Complete Schema Guide