Every web page that gets shared on social media needs an Open Graph image. It's that 1200x630 preview card you see on Twitter, Facebook, LinkedIn, and Slack. Without one, your shared links look like plain text. With a good one, click-through rates go up significantly.
I've been building web projects for years, and creating OG images has always been my least favorite part of the process. Open Figma, create a canvas, add text, pick colors, export, upload. Repeat for every page. For a blog with 30 posts, that's 30 images. For a documentation site with 100 pages, it's simply not viable.
So I built ogimg.xyz — an API that generates OG images programmatically. Send a POST request, get back a PNG. Here's how it works under the hood.
The Architecture
The entire application is a Next.js 15 project deployed on Vercel. The image generation endpoint is a Vercel Edge Function, which means it runs on Cloudflare's network close to the user — no cold starts, no long-running server processes.
The core pipeline:
Request → Validate API Key → Resolve Template → Satori (JSX → SVG) → Resvg (SVG → PNG) → Response
Let me break down each step.
Step 1: Request Validation
Every API call requires an API key passed via the Authorization: Bearer header. The key is looked up in a Neon PostgreSQL database (via Drizzle ORM). I check:
- Is the key valid and active?
- What plan tier is this user on?
- Have they exceeded their monthly quota?
Rate limiting uses a sliding window counter. Free users get 50 images/month, Hobby gets 1,000, and Pro gets 10,000.
curl -X POST https://ogimg.xyz/api/generate?utm_source=devto&utm_medium=article&utm_campaign=wf5 \
-H "Authorization: Bearer og_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"title": "My Awesome Blog Post",
"description": "Learn how to build better software",
"template": "gradient",
"backgroundColor": "#0f172a",
"textColor": "#f8fafc"
}'
Step 2: The Template System
Each template is a React component. Yes, actual JSX — because the rendering engine (Satori) understands React components.
Here's a simplified version of what a template looks like:
function GradientTemplate({ title, description, backgroundColor, textColor, pattern }: TemplateProps) {
return (
<div style={{
width: 1200,
height: 630,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
padding: 80,
background: `linear-gradient(135deg, ${backgroundColor}, ${adjustColor(backgroundColor, 30)})`,
color: textColor,
}}>
{pattern && <PatternOverlay type={pattern} />}
<h1 style={{ fontSize: 56, fontWeight: 700, lineHeight: 1.2 }}>
{title}
</h1>
{description && (
<p style={{ fontSize: 28, opacity: 0.8, marginTop: 24 }}>
{description}
</p>
)}
</div>
);
}
There are 10 templates total: gradient, minimal, bold, split, centered, documentation, dark, light, branded, and announcement. Each is designed for a different use case.
Step 3: Background Patterns
I built 10 SVG-based background patterns: dots, grid, diagonal lines, cross-hatch, waves, circles, hexagons, triangles, diamonds, and noise. They're rendered as inline SVG within the template and can be customized with color and opacity.
The pattern is layered behind the text content using Flexbox positioning (Satori doesn't support position: absolute, so everything is Flexbox-based).
Step 4: Satori — JSX to SVG
Satori is Vercel's library for converting React components (JSX) to SVG. It implements a subset of CSS Flexbox and renders text using actual font files (loaded as ArrayBuffers).
Key limitations I had to work around:
-
No
position: absolute: All layout must use Flexbox. This makes some designs trickier but keeps the rendering engine simple and fast. -
Limited CSS properties: No
box-shadow, nobackdrop-filter, no CSS gradients on text. You work within the constraints. - Font loading: You must provide font files explicitly. I bundle Inter and a few other Google Fonts as static assets.
Despite the limitations, Satori is fast. It converts a template to SVG in about 50-100ms.
Step 5: SVG to PNG
The SVG from Satori is converted to PNG using Resvg (via @vercel/og). This is a Rust-based SVG renderer compiled to WebAssembly, so it runs on the Edge without any native dependencies. The PNG conversion takes another 50-150ms.
The total pipeline from request to response is typically 200-500ms, depending on complexity and edge location.
Step 6: The Watermark
Free tier images get a small "Powered by ogimg.xyz" watermark in the bottom-right corner. It's rendered as part of the template — just an additional text element added conditionally based on the user's plan tier. Paid plans remove it entirely.
URL Auto-Fetch Mode
There's a convenience feature where you send a URL instead of content:
curl -X POST https://ogimg.xyz/api/generate?utm_source=devto&utm_medium=article&utm_campaign=wf5 \
-H "Authorization: Bearer og_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/blog/my-post",
"template": "minimal"
}'
The API fetches the URL, parses the HTML for <title>, <meta name="description">, and <link rel="icon">, and uses those as the template input.
Important security note: I implemented SSRF protection here. The fetch is restricted to public IP ranges only (no 127.0.0.1, no 10.x.x.x, no 169.254.x.x). Schemes are limited to http and https. There's a 5-second timeout. If you're building something similar that accepts user-provided URLs, don't skip this step.
Billing Integration
I use LemonSqueezy as the merchant of record. This means LemonSqueezy handles:
- Payment processing (Stripe under the hood)
- Global tax calculation and remittance (VAT, GST, sales tax)
- Invoicing
- Subscription management
On my end, I listen for LemonSqueezy webhooks to update user plan tiers in my database. When a user upgrades, their quota and feature access update immediately.
Pricing tiers:
| Plan | Price | Images/Month | Templates | Watermark |
|---|---|---|---|---|
| Free | $0 | 50 | 4 | Yes |
| Hobby | $4.90/mo | 1,000 | 10 | No |
| Pro | $9.90/mo | 10,000 | 10 | No |
| Lifetime | $149 once | 10,000 | 10 | No |
The Live Playground
I built an interactive playground at ogimg.xyz where you can:
- Select a template from a visual grid
- Enter your title and description
- Pick background colors and patterns
- See the preview update in real-time
- Copy the equivalent API call (curl or JavaScript)
This serves two purposes: it lets non-technical users generate images without writing code, and it lets developers experiment before integrating the API.
Lessons Learned
Satori is great but limited. If you need pixel-perfect designs, you'll hit its CSS limitations. For structured templates with text and simple graphics, it's perfect.
Edge rendering is the right call for this use case. No cold starts, globally distributed, and fast enough for real-time generation.
Freemium works for developer tools. Developers need to test before they commit. A generous free tier with a clear upgrade path converts better than a free trial with a time limit.
SSRF protection is essential. If your API accepts user-provided URLs, protect against server-side request forgery from day one. Not later. Now.
Try It
Head over to ogimg.xyz and generate your first image. The free tier gives you 50 images/month with no credit card required. If you're integrating into a Next.js project, it's literally one fetch call:
const response = await fetch('https://ogimg.xyz/api/generate',?utm_source=devto&utm_medium=article&utm_campaign=wf5 {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OGIMG_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'My Page Title',
description: 'A brief description',
template: 'gradient',
backgroundColor: '#1e293b',
}),
});
const imageBuffer = await response.arrayBuffer();
// Save or serve the image
I'd love feedback on the API design, the templates, and anything else. You can find me on Twitter at @corbanware.
Top comments (0)