I got tired of babysitting Playwright, so I turned it into a small render API
Every small product I ship ends up needing one of two things: an Open Graph image, or a PDF of some HTML I already have.
Self-hosting Playwright or Puppeteer works. It also:
- creeps up in memory until the box runs out
- leaves Chrome hanging when a page never finishes loading
- captures a GDPR banner over the thing you actually wanted
- turns a weekend feature into an ops job
So I put it behind one endpoint, and it's now live as FrameForge: HTML or a URL in, PNG, JPEG or PDF out.
The request
curl -X POST https://95-217-220-34.sslip.io/render \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"html":"<!doctype html><h1>Invoice 1042</h1>","format":"pdf"}' \
-o invoice.pdf
Swap html for url, or pdf for png / jpeg. width and height go up to 3840 and fullPage captures the whole page.
Design choices I made on purpose
- One browser, a new context per request. Sharing tabs is how cookies and memory leaks spread between jobs. Each render gets a clean context that is closed afterwards.
- Hard timeouts. 20 seconds to navigate, 30 for the whole render. A hung page must not hold one of only two slots.
- Cap concurrency at 2 on a 2 vCPU / 4 GB box. Chromium contexts aren't free. A queue of 8 is better than an OOM kill.
- Cache by hash. The request body is SHA-256 hashed. An identical request is served from disk in well under a second, which makes retries fast.
- Hide banners with CSS rather than clicking "Accept". Clicking consent on other people's sites is messy and fragile. Hiding the common OneTrust / Cookiebot / Quantcast / generic cookie nodes covers most cases. It's best effort, and shadow-DOM banners still get through.
- Treat every URL as hostile. Private and link-local addresses are refused, and the check runs again on every redirect and every subresource the page loads, so the renderer can't be used to probe internal networks.
What it runs on
Node, Playwright 1.56, Docker and Caddy for HTTPS on a single Hetzner CPX22 in Helsinki. The box costs about €17 a month, so one Starter customer pays for it.
Trying it
- Free trial: 100 renders over 14 days. Email serv**************@rightemp.co.uk and I'll send you a key.
- Starter £9/month for 3,000 renders, Studio £29/month for 20,000.
- Founding Member: £49 once for 12 months of Studio, limited to the first 100 people.
- 30-day refund on all of them.
Details and checkout: https://frameforge-efg.pages.dev (disclosure: this is my project).
What I'm still unsure about
Is a 100-render trial enough, or does an API like this need a permanent free tier? And is banner hiding a must-have or a nice-to-have for you?
If you've run Chrome in production, I'd like to hear what broke first.
Comments
No comments yet. Start the discussion.