You've built a blog. You write a post. You create a 1200×630px preview image in Figma. You export it as PNG. You upload it to your `public/` folder. You add it to the meta tags. Then you write the next post and do it all again. By post 50, you're spending 10 minutes per post on OG images alone. By post 100, you're using a template so generic it doesn't stand out anymore. This is not the move.
Satori is Vercel's library that converts JSX to SVG to PNG at request time. You write a single React component. One Astro endpoint renders it for any URL. Every blog post gets a uniquely branded share card. Update the design once, all cards update instantly. No Figma. No exports. No uploads. Ship it.
Why Static OG Images Are Dead
Static images are design debt. You create one, commit it, forget about it. Six months later your brand colour changes. Now every old post has the wrong colour in its preview. You either (a) recreate all 50 old images, or (b) leave them broken. If you're smart, you built a template and reused it — but now all your cards look identical. Click on your blog on Slack and every thumbnail is the same blue box with white text. No personality. No visual hierarchy. Just noise.
Dynamic OG generation solves this. The card is generated at request time from data in your URL or database. Blog post title? Pull it from the URL slug. Author name? Fetch it from a query param. Post category? Check the frontmatter. The card is always in-sync with your content. Update your brand, and every card updates next time someone shares it. No manual work. No stale images. Pure scalability.
Satori: JSX to PNG in Milliseconds
Satori takes React JSX and converts it to an SVG, then to PNG. No headless browser. No Puppeteer startup overhead. Pure server-side rendering. A blog post card that would take 800ms to render with Puppeteer renders in 50ms with Satori. That matters when you're generating images on every share.
Here's the shape: you write a React component that looks like a share card. You pass it props (title, author, category). Satori renders it, exports PNG bytes, and you send it as a response header. The user's browser caches it. Next time someone shares the link, the cached PNG is used.
Install Satori and its peer dependencies:
npm install satori sharp
Sharp handles PNG encoding. Satori does the SVG conversion. You provide the fonts.
Build an OG Card Component
Create a React component that lives outside your Astro structure (plain TSX, no framework sugar):
// src/lib/og-card.tsx
import React from 'react';
interface OGCardProps {
title: string;
author: string;
category: string;
}
export function OGCard({ title, author, category }: OGCardProps) {
return (
<div
style={{
width: 1200,
height: 630,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: 60,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
fontFamily: 'Inter, sans-serif',
color: 'white',
}}
>
<div>
<p style={{ fontSize: 18, margin: 0, opacity: 0.8 }}>{category}</p>
<h1
style={{
fontSize: 56,
fontWeight: 700,
margin: '20px 0 0 0',
lineHeight: 1.2,
}}
>
{title}
</h1>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<p style={{ fontSize: 20, margin: 0 }}>aidxn.com</p>
<p style={{ fontSize: 16, margin: 0 }}>by {author}</p>
</div>
</div>
);
}
This is a 1200×630 card with your brand gradient, title, category, author, and logo area. You control every pixel in code. Update the gradient and every card updates.
Astro Endpoint (The Secret Sauce)
Create an Astro endpoint that generates the PNG on-the-fly:
// src/pages/og/[...slug].png.ts
import { OGCard } from '../../lib/og-card';
import satori from 'satori';
import sharp from 'sharp';
export async function GET({ params, url }) {
const title = url.searchParams.get('title') || 'Untitled Post';
const author = url.searchParams.get('author') || 'Aiden Wood';
const category = url.searchParams.get('category') || 'Web Development';
// Load fonts (Satori needs font files)
const interRegular = await fetch(
'https://cdn.jsdelivr.net/npm/inter@3.19.0/Inter_18pt-Regular.woff'
).then((res) => res.arrayBuffer());
const svg = await satori(
<OGCard title={title} author={author} category={category} />,
{
width: 1200,
height: 630,
fonts: [
{
name: 'Inter',
data: interRegular,
weight: 400,
style: 'normal',
},
],
}
);
const png = await sharp(Buffer.from(svg)).png().toBuffer();
return new Response(png, {
headers: {
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=31536000, immutable',
},
});
}
This endpoint lives at `/og/[slug].png`. Call it with query params: `/og/my-post.png?title=My Post&author=Aiden&category=DevOps`. Satori renders the card, sharp encodes it to PNG, and a one-year cache header ensures it's served from edge CDN. No re-renders.
Wire It Into Your Blog Meta Tags
In your blog post layout, construct the OG image URL:
const ogImageUrl = new URL(
`/og/blog.png`,
Astro.site
);
ogImageUrl.searchParams.set('title', title);
ogImageUrl.searchParams.set('author', author);
ogImageUrl.searchParams.set('category', category);
<meta property="og:image" content={ogImageUrl.toString()} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
Now every blog post dynamically links to its own personalized OG card. Share a post on Twitter and the card is generated on-demand, cached for a year, and cached in Twitter's preview service too.
Font Loading (The Gotcha)
Satori needs font files as ArrayBuffers. The most common mistake: loading fonts from Google Fonts or system paths, then wondering why Satori renders in a fallback serif. The fix is to fetch the font file directly from a CDN (jsdelivr, unpkg, etc.) and pass it to Satori's `fonts` array. Bold, italic, and multiple weights each need their own entry. If you skip this, your cards will look wrong.
For production, cache the font fetch outside the GET handler:
let fontCache: Buffer | null = null;
async function getFont() {
if (fontCache) return fontCache;
const res = await fetch('https://cdn.jsdelivr.net/...');
fontCache = await res.arrayBuffer();
return fontCache;
}
Caching Strategy
The cache header `max-age=31536000, immutable` tells browsers and CDN that this image never changes. Smart: the URL includes the post data as query params. If the title changes, the URL changes, and a new image is generated. Old cached images stay cached forever. New images get cached separately. This scales infinitely and costs nothing after the first render.
Add Netlify image optimization if you're on Netlify to further compress the PNG:
// astro.config.mjs
export default defineConfig({
integrations: [netlify({ imageCDN: true })],
});
Six FAQs
Can I use custom fonts?
Yes. Download your font file (.woff, .woff2, .ttf) and serve it from your public folder or a CDN. Fetch it in the endpoint and pass it to Satori.
Does Satori work with Tailwind classes?
No. Satori doesn't parse Tailwind. Write inline styles or use a CSS-in-JS library like styled-components in your OG component.
What if my title is 200 characters long?
Truncate it in the component. Add `maxLength` logic and ellipsis: `title.length > 100 ? title.slice(0, 97) + '...' : title`.
Can I use images inside the OG card?
Yes, but they need to be base64-encoded or fetched as URLs. Satori can render image tags, but complex image handling adds latency. Keep it simple: solid gradients and text are fastest.
How do I preview the OG image locally?
Visit `http://localhost:3000/og/my-post.png?title=Test&author=You&category=Dev` in your browser. You'll see the rendered PNG.
Can I generate OG images for pages besides blog posts?
Absolutely. Create endpoints for `/og/product.png`, `/og/pricing.png`, etc. Each can have its own component design.
The Bottom Line
Static OG images don't scale. Satori + Astro endpoints do. One React component, infinite variations. Generate branded share cards on-demand for every URL, cache them for a year, and never manually create another preview image. Velocity X ships dynamic OG generation as standard. If your site has a blog, add this endpoint and watch your social click-through rates climb. The card always matches your brand because it's always generated from your current design code.
Need to scale this further? Check out Aidxn Design's web performance consulting to optimize your image pipeline end-to-end, or read more about offloading third-party scripts to keep your critical path lean.