Generate OG images in milliseconds. No browser. No Puppeteer. No BS.
Server-side image generation API powered by Canvas-based text measurement.
Drop-in replacement for Puppeteer/Playwright image pipelines.
Quick Start • API Reference • Self-Host • Benchmarks • Templates • Roadmap
Every time you generate an OG image with Puppeteer, you're spinning up a full Chromium instance to render some text on a rectangle. That's 500MB of RAM and ~129ms (warm) to ~658ms (cold) of latency — for a PNG.
OG Engine measures text and renders images using server-side Canvas. No DOM, no browser, no headless anything.
| Puppeteer | OG Engine | |
|---|---|---|
| Render time | ~129ms (warm) / ~658ms (cold) | ~22ms |
| Memory per render | ~200-500MB | ~10MB |
| Infrastructure | Chrome binary, Xvfb, sandboxing | Node.js process |
| Concurrency | ~5-10 per instance | ~500+ per instance |
| Cold start | ~2-5s | ~50ms |
| CJK / Arabic / Emoji | Yes (full browser) | Yes (native Unicode support) |
curl -X POST http://localhost:3000/render \
-H "Content-Type: application/json" \
-d '{"title": "Hello, OG Engine", "format": "og"}' \
--output card.png# Clone & install
git clone https://github.com/Atypical-Consulting/og-engine.git
cd og-engine
bun install
# Download fonts (required on first run)
bun run fonts:download
# Start the server
bun run dev
# → Server running at http://localhost:3000docker build -t og-engine .
docker run -p 3000:3000 og-engineGenerate an image from text + configuration.
{
"format": "og",
"title": "Server-Side Text Layout Without a Browser",
"description": "Pure JS text measurement replaces Puppeteer.",
"author": "OG Engine",
"tag": "Open Source",
"variables": {
"read_time": "4 min read",
"category": "Engineering"
},
"images": {
"avatar": "https://example.com/author.png"
},
"style": {
"accent": "#38ef7d",
"layout": "left",
"font": "Outfit",
"titleSize": 48,
"descSize": 22,
"gradient": "void",
"overlayOpacity": 0.65
},
"output": {
"format": "png",
"quality": 90
}
}Response: Binary PNG with performance headers:
Content-Type: image/png
X-Render-Time-Ms: 2.34
X-Title-Lines: 2
X-Desc-Lines: 3
X-Layout-Overflow: false
Check if text fits without generating an image. Ultra-fast.
{
"format": "og",
"title": "Some headline",
"description": "Some body text",
"font": "Outfit",
"titleSize": 48,
"maxTitleLines": 3
}{
"fits": true,
"title": { "lines": 2, "maxLines": 3, "overflow": false },
"description": { "lines": 1, "maxLines": 4, "overflow": false },
"computeTimeMs": 0.12
}Zero-config image generation — OG Engine fetches the page at url, extracts its Open Graph tags, and renders a card automatically.
{
"url": "https://myblog.com/posts/my-article",
"format": "og",
"style": { "gradient": "deep-sea" }
}Optional overrides lets you override specific fields (e.g. tag) while keeping the scraped title and description.
{
"status": "ok",
"fonts": ["Outfit", "Inter", "Playfair Display", "Sora", "Space Grotesk", "JetBrains Mono", "Noto Sans JP", "Noto Sans AR"],
"formats": ["og", "twitter", "square", "linkedin", "story"],
"templates": ["default", "social-card", "blog-hero", "email-banner", "product-card", "event", "testimonial", "github-repo", "news-article", "pricing", "profile-card", "announcement"],
"version": "0.1.0"
}| Format | Dimensions | Use case |
|---|---|---|
og |
1200 × 630 | Open Graph / Facebook |
twitter |
1200 × 675 | Twitter/X cards |
square |
1080 × 1080 | Instagram / general social |
linkedin |
1200 × 627 | LinkedIn posts |
story |
1080 × 1920 | Instagram/TikTok stories |
8 fonts included out of the box, with full Unicode coverage:
| Font | Weights | Script support |
|---|---|---|
| Outfit | 400, 700, 800 | Latin |
| Inter | 400, 700, 800 | Latin |
| Playfair Display | 400, 700, 800 | Latin |
| Sora | 400, 700, 800 | Latin |
| Space Grotesk | 400, 700 | Latin |
| JetBrains Mono | 400, 700 | Latin (monospace) |
| Noto Sans JP | 400, 700 | Japanese / CJK |
| Noto Sans Arabic | 400, 700 | Arabic / RTL |
6 built-in gradients: void deep-sea ember forest plum slate
3 layout modes: left center bottom
Full control over: accent color, font, title/description size, overlay opacity, tag pill, author line.
| Template | Description |
|---|---|
default |
Accent bar, grid background, tag pill — the classic OG card |
social-card |
Large centered title, minimal and clean |
blog-hero |
Background image focused with text overlay |
email-banner |
Horizontal CTA-style for email campaigns |
product-card |
Product name, price, and image highlight |
event |
Date, venue, and event title prominent |
testimonial |
Quote, author, and avatar layout |
github-repo |
Repo name, description, and stats |
news-article |
Publication, headline, and category badge |
pricing |
Plan name, price, and key features |
profile-card |
Avatar, name, title, and social handles |
announcement |
Large headline with accent, ideal for launches |
// app/api/og/[slug]/route.ts
export async function GET(req: Request, { params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
const res = await fetch('http://localhost:3000/render', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
format: 'og',
title: post.title,
description: post.excerpt,
tag: post.category,
style: { accent: '#38ef7d', font: 'Outfit' }
})
})
return new Response(res.body, {
headers: { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=86400' }
})
}import express from 'express'
const app = express()
app.get('/og/:slug', async (req, res) => {
const image = await fetch('http://localhost:3000/render', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
format: 'og',
title: `My Blog — ${req.params.slug}`,
style: { gradient: 'deep-sea' }
})
})
res.set('Content-Type', 'image/png')
res.send(Buffer.from(await image.arrayBuffer()))
})# Generate an OG image
curl -X POST http://localhost:3000/render \
-H "Content-Type: application/json" \
-d '{
"title": "How We Killed Puppeteer",
"description": "And saved 500MB of RAM per render.",
"format": "og",
"tag": "Engineering",
"style": { "accent": "#ff6b6b", "gradient": "ember", "font": "Playfair Display" }
}' \
--output og-card.png
# Check if text fits before rendering
curl -X POST http://localhost:3000/validate \
-H "Content-Type: application/json" \
-d '{"title": "Will this headline fit?", "format": "og", "titleSize": 48}'Measured on Apple M2, 8 cores, 24 GB RAM, Bun 1.3.11. 1,000 iterations per scenario with 50 warmup (discarded). Full report: benchmarks/results/2026-04-03-report.md.
| Scenario | Text Measure (P50) | Canvas Draw (P50) | PNG Encode (P50) | Full Pipeline (P50) | Full Pipeline (P95) |
|---|---|---|---|---|---|
| Baseline (og, 1 line, Outfit) | 114µs | 50µs | 21.39ms | 21.57ms | 22.79ms |
| Long text (og, overflow, Outfit) | 390µs | 78µs | 24.34ms | 24.83ms | 26.41ms |
| Story format (1080×1920, Outfit) | 426µs | 98µs | 59.37ms | 59.96ms | 65.02ms |
| CJK (og, Noto Sans JP) | 126µs | 79µs | 24.12ms | 24.34ms | 26.92ms |
| Scenario | OG Engine (P50) | Puppeteer Warm (P50) | Puppeteer Cold (P50) | Speedup (warm) |
|---|---|---|---|---|
| Baseline | 21.57ms | 128.75ms | 657.55ms | 6x |
| Long text | 24.83ms | 132.14ms | 634.03ms | 5x |
bun run bench # OG Engine only
bun run bench:full # Includes Puppeteer comparisonOG Engine is designed to self-host. It's a single Bun/Node.js process with no external dependencies.
Requirements:
- Bun 1.0+ (recommended) or Node.js 20+
- ~50MB disk (fonts + binary)
- ~10MB RAM per concurrent render
# Production
bun run start
# Or with Node.js
npx tsx src/index.ts| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
Server port |
HOST |
0.0.0.0 |
Bind address |
| Component | Technology | Why |
|---|---|---|
| Runtime | Bun | Native TypeScript, fast startup |
| HTTP | Hono | Ultra-light, runs everywhere |
| Canvas | @napi-rs/canvas | Fastest server-side Canvas for Node/Bun |
| Validation | Zod | Type-safe request validation |
| Fonts | Google Fonts (TTF) | Downloaded locally, zero runtime fetching |
og-engine/
├── src/
│ ├── index.ts # Hono server
│ ├── api/
│ │ ├── render.ts # POST /render — image generation
│ │ ├── validate.ts # POST /validate — text fit check
│ │ └── health.ts # GET /health — discovery
│ ├── engine/
│ │ ├── text-measure.ts # Line breaking & text measurement
│ │ ├── renderer.ts # Canvas compositing & rendering
│ │ ├── fonts.ts # Font loading & registration
│ │ ├── formats.ts # Format definitions (og, twitter, etc.)
│ │ └── gradients.ts # Gradient presets
│ └── schemas/
│ └── request.ts # Zod request schemas
├── fonts/ # TTF files (downloaded at build time)
├── benchmarks/ # Performance benchmark suite
└── tests/ # Vitest test suite
-
Phase 1 — Core API (complete)
- Hono server with render, validate, health endpoints
- Canvas-based text measurement engine
- 5 formats, 6 gradients, 8 fonts
- Zod validation, CORS, error handling
- Benchmark suite with statistical analysis
-
Phase 2 — Production Features
- All 12 templates (social-card, blog-hero, email-banner, product-card, event, testimonial, github-repo, news-article, pricing, profile-card, announcement)
variablesandimagesfields for template-level dynamic contentPOST /render/from-url— zero-config rendering from a URL's OG tags- Background image upload (multipart)
- WebP output
- LRU text cache, batch endpoint, rate limiting
-
Phase 3 — Scale & Polish
- API key authentication & usage tracking
- Redis cache layer
- OpenAPI documentation
- TypeScript SDK (npm)
- Docker + Fly.io deployment
-
Phase 4 — Growth
- Custom template builder (JSON DSL)
- AI text fitting (auto font-size adjustment)
- Edge deployment (Cloudflare Workers)
- PDF output, webhook triggers
| Feature | OG Engine | @vercel/og | Puppeteer | Cloudinary |
|---|---|---|---|---|
| Render speed | ~22ms | ~50-200ms | ~129ms (warm) / ~658ms (cold) | ~500ms |
| Self-hostable | Yes | Vercel only | Yes | No |
| No browser needed | Yes | Yes (Satori) | No | N/A |
| CJK/Arabic/Emoji | Yes | Partial | Yes | Yes |
| Multiple formats | 5 | 1 | Any | Many |
| Custom fonts | 8 built-in | Manual setup | Any | Limited |
| Text validation | Yes | No | No | No |
| Batch rendering | Planned | No | Manual | Yes |
| Open source | Yes | Yes | Yes | No |
Contributions are welcome! Here's how to get started:
git clone https://github.com/Atypical-Consulting/og-engine.git
cd og-engine
bun install
bun run fonts:download
bun run test
bun run devAreas where help is needed:
- Additional templates and gradient presets
- Font coverage (more scripts/languages)
- Integration examples (Astro, Nuxt, SvelteKit, etc.)
- Performance optimizations in the rendering pipeline
- Visual regression test suite
OG Engine's server (src/ and everything at the repo root) is licensed under
the Functional Source License, Version 1.1, Apache 2.0 Future License
(FSL-1.1-Apache-2.0). You can read, modify, and self-host it for any purpose
except making it available to third parties as a hosted service or
embedding it in a commercial product you distribute. Every release
automatically converts to Apache-2.0 two years after
its release date — see LICENSE-HISTORY.md.
The SDK (sdk/) is licensed under Apache-2.0 — use it freely
in any project, commercial or not.
Using OG Engine inside a commercial product or SaaS?
See COMMERCIAL-LICENSE.md or email
philippe@atypical.consulting.
If OG Engine saves you from running Puppeteer, consider giving it a star.
It helps others discover the project.