Your self-hosted Bannerbear. Design social-card templates visually, then render them programmatically — dynamic OG images, X cards, podcast episode art, quote cards — from a single GET request. Pay once. Own it forever. No subscription.
Bannerbear charges $49/month for this. Placid charges $19+/month. OG Image Studio is a $39 one-time purchase (or free — it's MIT, build it yourself below) that runs on your laptop or a $5 VPS.
Don't want to clone, build, and configure? Grab the packaged installer with everything pre-wired:
→ https://whop.com/onetime-suite
- Visual template editor — canvas with drag positioning + snap guides, layers panel, live preview that matches server output pixel-for-pixel (same HTML renderer on both sides)
- Layer types — text (bundled fonts, weight, color, align, max-width wrap, auto-shrink to fit), images (upload or URL, cover/contain/fill, border-radius, opacity), shapes (rect/pill with opacity), backgrounds (solid, gradient, image)
- Sizes for every network — OG/blog 1200×630, X large card 1600×900, Instagram square 1080×1080, Instagram story 1080×1920, LinkedIn 1200×627 (plus fully custom dimensions)
{{variables}}— type{{title}}in any text layer (or image URL) and the template instantly becomes an API endpoint with that parameter- Render API —
GET /api/render/:templateId?title=...&key=...returns a PNG. Deterministic, server-side, rendered with bundled Chromium (Puppeteer). Drop the URL straight into yourog:imagemeta tag. - Bulk CSV → zip — upload a CSV, get one PNG per row. Batch 500 episode cards for your podcast back-catalog in one request.
- Gallery — every API render is logged and browsable, with the exact params used
- 5 polished starter templates — Blog Post, Podcast Episode, Product Launch, Quote Card, Event
- 100% local — your data never leaves your machine. SQLite + filesystem. No telemetry.
git clone https://github.com/bensblueprints/og-image-studio
cd og-image-studio
npm i # installs deps incl. bundled Chromium (~150 MB, one time)
npm run build # build the frontend
npm start # → http://localhost:5309 (password: admin)Copy .env.example to .env to set PORT, ADMIN_PASSWORD, and API_KEY. If you don't set an API key, one is generated on first run — it's shown on the API page and in the server logs.
Run it as a desktop app, or deploy to a $5 VPS when you need it public:
npm run desktopA native window opens, auto-logged-in as admin, with data stored in your OS profile. Same code, same features — the Electron wrapper just boots the local server for you. (Desktop mode uses your installed Node.js to run the server process.) npm run dist builds a Windows NSIS installer via electron-builder.
docker compose up -dShips with Chromium and all render dependencies baked in. Generated images and the SQLite database live in the og_data volume.
This is the headline feature: every template you design is instantly a REST endpoint.
GET /api/render/:templateId?title=...&author=...&key=YOUR_API_KEY
Any {{variable}} used in the template becomes a query parameter. Auth via ?key= or the X-Api-Key header. Returns image/png.
curl -o card.png "http://localhost:5309/api/render/JA4fNNDHkaUn?title=My%20Post&author=Ben&key=ogs_..."// Node / serverless — generate an OG image for every blog post
const params = new URLSearchParams({ title: post.title, author: post.author });
const res = await fetch(`https://og.yourdomain.com/api/render/${TEMPLATE_ID}?${params}`, {
headers: { 'X-Api-Key': process.env.OG_API_KEY },
});
const png = Buffer.from(await res.arrayBuffer());<!-- Or point your meta tags straight at it — images render on demand -->
<meta property="og:image"
content="https://og.yourdomain.com/api/render/TEMPLATE_ID?title=Dynamic%20Title&key=KEY" />
<meta name="twitter:card" content="summary_large_image" />Extras: &download=1 forces a download disposition, &nosave=1 skips the gallery log.
POST /api/render/:templateId/bulk?key=YOUR_API_KEY
Send CSV as the raw body (Content-Type: text/csv) or as a multipart file field named csv. Column headers map to template variables; an optional _filename column names each file. Returns a zip of PNGs (max 500 rows).
curl -X POST "http://localhost:5309/api/render/TEMPLATE_ID/bulk?key=KEY" \
-H "Content-Type: text/csv" \
--data-binary @episodes.csv -o cards.ziptitle,author,_filename
"How We Cut Our Cloud Bill",Ben,post-001
"Shipping in Public",Sarah,post-002| Method | Path | What |
|---|---|---|
GET |
/api/templates |
List templates (+ detected variables) |
POST |
/api/templates |
Create ({name, data}) |
PUT |
/api/templates/:id |
Update |
POST |
/api/templates/:id/duplicate |
Duplicate |
DELETE |
/api/templates/:id |
Delete |
GET |
/api/gallery |
Generated image log |
| OG Image Studio | Bannerbear | |
|---|---|---|
| Price | $39 once (or free, MIT) | $49/month ($588/yr) |
| API renders | Unlimited | 1,000/mo on Starter |
| Templates | Unlimited | 10 on Starter |
| Data location | Your machine / your VPS | Their cloud |
| Rate limits | None | Yes |
| Works offline | Yes | No |
| Source code | Yours, MIT | Closed |
| Pays for itself in | — | 24 days |
- Server — Node 20+, Express, better-sqlite3, Puppeteer (bundled Chromium) for deterministic HTML→PNG rendering
- Frontend — React (Vite), Tailwind CSS, Framer Motion, Lucide, Zustand
- Fonts — Inter, Space Grotesk, Playfair Display, JetBrains Mono bundled via Fontsource and base64-embedded at render time (fully offline, deterministic output)
- Desktop — thin Electron wrapper around the identical Express server
- One renderer, everywhere — the editor preview and the render engine share the same
shared/renderHtml.js, so WYSIWYG actually means it
npm start # API server on :5309
npm run dev # Vite dev server on :5310 (proxies /api to :5309)
npm test # smoke test: boots server, renders PNGs, checks dims/hashes/auth/bulkMIT © 2026 Ben (bensblueprints)
