The idea
A small HTTP service that takes a title, subtitle, and a template name as query params and returns a rendered PNG social preview card. Point your static site generator's build step (or a serverless edge function) at og.yoursite.com/render?template=blog&title=... and every post gets a consistent, on-brand Open Graph image with zero manual design work. Templates are just HTML/CSS files with placeholder tokens, so designers can iterate without touching the rendering code.
Why build this
Every blog, docs site, and changelog needs decent OG images or links look broken and unclickable when shared on Slack, Twitter/X, or LinkedIn. Today's options are mostly manual (Figma export per post), expensive SaaS (Bannerbear, Vercel OG-as-a-paid-feature), or a pile of bespoke Puppeteer scripts every team reinvents from scratch. Headless Chrome rendering has gotten fast and cheap enough to run this as a stateless service instead of a build-time script, which means it also works for dynamically generated pages (user profiles, product listings, forum threads) that a static build never touches. Teams running their own infra — self-hosted docs, internal wikis, small SaaS marketing sites — want this without adopting a $50/mo third-party dependency for something that's fundamentally "render HTML, screenshot it."
Stack sketch
- Rendering:
@vercel/og(Satori + resvg, no headless browser needed) for the common case; fall back to Playwright/Chromium for templates that need real CSS features Satori doesn't support (grid, complex flexbox, web fonts with ligatures) - API: a thin Fastify or Hono service exposing
GET /renderandPOST /render(for longer payloads), returnsimage/pngwith long-livedCache-Controlheaders - Templates: JSX or plain HTML files with
{{token}}placeholders, stored in atemplates/directory or pulled from a git repo at startup - Caching: render once per unique query-param combination, cache the PNG on disk or in S3/R2, keyed by a hash of the params — repeat requests (social crawlers re-fetching) hit cache instead of re-rendering
- Auth: a simple API key or HMAC-signed URL scheme so people can't spam your render endpoint and run up compute
- Deploy: single Docker container, works fine behind Traefik/nginx like any other internal service; horizontally scalable since it's stateless
Scope for v1
- One rendering path (Satori-based, no headless browser) to keep it fast and lightweight
- 2–3 built-in templates (blog post, docs page, generic title/subtitle) plus support for a custom template file
- Query-param based API, PNG output only (no SVG/WebP variants yet)
- File-based render cache, no CDN integration
- API key auth, no per-user rate limiting or multi-tenancy
- Out of scope for v1: a template editor UI, font upload management, animated/video previews, screenshot-of-live-URL mode
Where it could go
The natural next step is a "screenshot mode" that renders an actual URL instead of a template — useful for auto-generating previews of dashboards, PR diffs, or user-generated pages where you don't control the markup, which pushes you toward the Playwright fallback becoming the primary path instead of the exception. From there, a template marketplace or gallery (shareable JSON+CSS template definitions) would let teams bootstrap a good-looking card in minutes instead of designing from scratch, similar to how Shields.io badges became a shared visual language. A hosted multi-tenant version with usage-based billing is the obvious commercial path if the self-hosted version gets traction — same core renderer, add auth scoping, a dashboard, and CDN-backed caching.
Watch out for
Font licensing is the sharp edge: bundling non-free fonts into a public rendering service can violate their license terms, so stick to open-license fonts (Google Fonts, system fonts) unless you're running it purely internally. Also budget real CPU for the Playwright fallback path — headless Chrome renders are an order of magnitude slower and heavier than Satori, so route to it only when a template actually needs it.