One fetch call from your App Router route handler. Full HTML & CSS support, deploys on Vercel, no Chromium binary in your bundle.
Every approach has a different tradeoff. Here's why teams end up looking for something better.
background-clip. Anything beyond a simple layout requires painful rewrites.clip-path — if your browser can render it, we can capture it at any resolution.Add a single file to your Next.js project. Works on Vercel, Netlify, or any Node.js host.
import { NextRequest } from 'next/server'
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url)
const title = searchParams.get('title') ?? 'Untitled'
const author = searchParams.get('author') ?? ''
const html = `
<div style="
width:1200px; height:630px;
background:linear-gradient(135deg,#0f172a 0%,#1e1b4b 100%);
display:flex; flex-direction:column;
justify-content:flex-end; padding:72px;
font-family:'Inter',sans-serif;
">
<div style="color:#818cf8;font-size:18px;margin-bottom:20px;font-weight:500">
myblog.com
</div>
<div style="color:#f8fafc;font-size:56px;font-weight:700;line-height:1.15;max-width:900px">
${title}
</div>
<div style="color:#94a3b8;font-size:20px;margin-top:28px">
${author}
</div>
</div>
`
const res = await fetch('https://api.renderpix.dev/v1/render', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.RENDERPIX_API_KEY}`,
},
body: JSON.stringify({ html, width: 1200, height: 630, format: 'png' }),
})
const { url } = await res.json()
const image = await fetch(url)
return new Response(image.body, {
headers: {
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=86400, immutable',
},
})
}
Use it in any page: <meta property="og:image" content="/og?title=My+Post&author=Jane" />
api.renderpix.dev/v1/render. Inject dynamic values (title, author, date) into the template before sending.Response with a long Cache-Control header. Vercel's CDN handles the rest — each unique image is generated once.How the three most common approaches stack up for Next.js projects.
| Tool | Full CSS support | Vercel compatible | HTML templates | Setup time | Cold start |
|---|---|---|---|---|---|
| RenderPix | Yes | Yes | Yes | < 5 min | ~200 ms |
| @vercel/og | Subset only | Yes | JSX only | ~30 min | < 50 ms |
| Satori (standalone) | Subset only | Yes | JSX only | ~1 hr | < 50 ms |
| Puppeteer in Lambda | Yes | No (250 MB limit) | Yes | ~4 hrs | 2–4 s |
Yes. Your route handler is a normal serverless function that makes an outbound fetch — no Chromium binary, well under the 250 MB limit. It works on Vercel Hobby, Pro, and Enterprise. The render happens on RenderPix infrastructure, not inside your function.
The route handler above uses the Node.js runtime (default for App Router). Edge runtime support is on the roadmap. If you need edge today, you can use the Node.js runtime for the /og route only — add export const runtime = 'nodejs' at the top of the file.
Add a Cache-Control: public, max-age=86400, immutable header to the response. Vercel's CDN will cache the image at the edge after the first request. For blog posts that never change, the OG image is generated once and served from cache forever. You can also pre-generate images at build time and store them in /public/og/.
Free tier: 100 renders per month, no credit card required. Starter plan is $9/month for 2,000 renders. Most blogs and marketing sites stay on the free tier indefinitely — OG images are generated once per post and cached. Check the pricing page for full details.
Yes. Reference any Google Font or self-hosted font in your HTML template with a standard <link> tag or @font-face declaration. The renderer waits for fonts to load before capturing, so your typography will match exactly what you see in a browser.
Free tier, no credit card. Full HTML and CSS support. Works on Vercel out of the box.