FakeRocket renders pixel-perfect platform screenshots — tweets, iMessages, Instagram posts, WhatsApp chats, Discord messages, iOS notifications, YouTube comments, news chyrons — entirely in the browser. No server processes your input. No image gets uploaded anywhere. The PNG you download is rendered locally and exists only on your machine after the click.
Here's how it's built, why every choice was made, and what we learned shipping it.
The architecture, end-to-end
FakeRocket is a Next.js 16 App Router app, statically prerendered to flat HTML at build time, served from Cloudflare Workers Static Assets. Every route is a (Static) prerender, including the per-tool and per-blog-post Open Graph images. There are 50+ routes and zero of them require runtime compute.
That choice — going fully static — was load-bearing. We wanted three things at the same time: global TTFB under 50ms, no user data ever touching a server, and ad-free dev previews for every PR. All three follow from "everything prerenders at build time."
The rendering loop
Each generator is a client component with two halves: form on the left, live preview on the right. State lives in React. The preview is just JSX rendered with the user's state — no canvas, no offscreen rendering, no Web Worker. The same DOM you see in the browser is the DOM that becomes the PNG.
The PNG export uses html-to-image — a single call to toPng(ref.current, { pixelRatio: 2 }) rasterizes the preview at 2× density and returns a data URL. We trigger a download via a hidden anchor tag and the file lands in the user's downloads folder. Total client-side execution time: usually under 200ms.
Why html-to-image specifically
The alternatives we considered:
- html2canvas — wider adoption but bigger bundle, slower, and notoriously bad at modern CSS (backdrop-filter, grid, custom properties).
- dom-to-image— the original, but maintenance has slowed and it doesn't handle cross-origin images well.
- Server-side rendering with Puppeteer or Satori — would have required user input to leave the browser. Non-starter for the privacy story.
- HTML <canvas> rebuild from scratch — rewriting bubble math and font kerning in canvas for eight different tools would have taken months.
html-to-image won because it's small (~30KB gzipped), maintained, handles modern CSS, and works with cross-origin images via a cacheBust: true option that we actually need for user-uploaded avatars.
Bubble math, status bars, and the things that look easy
The hardest part of building a fake-screenshot tool isn't the rendering. It's the platform-specific math that nobody documents. Two examples:
- iMessage bubble corners.Apple uses asymmetric corner radii on the last bubble of a sender's run — 18px on most corners, 4px on the corner facing the tail. The middle bubbles of a run get 18px on every corner. The visual reads correctly to iOS users only if this is right; reads as fake if it isn't. We compute these per-bubble in React based on the previous and next bubble in the message array.
- WhatsApp tail position.The little triangular tail on the first bubble of a sender's run sits at the top corner facing outward. The corners on the opposite side of the bubble are not affected. iMessage and WhatsApp do this differently — iMessage tails are on the LAST bubble of a run, WhatsApp on the FIRST. Getting either wrong fails the visual.
These details live in inline CSS-in-JS computed at render time, not in static stylesheets. The cost is that you have to read the code carefully to understand the visual; the win is that the bubble math stays correct as users add, remove, and reorder messages live.
The watermark
Every PNG export embeds a small fakerocket.com watermark in a corner of the rendered preview. The watermark is part of the same DOM tree as the rest of the preview, so html-to-image rasterizes it along with everything else. There's no post-processing step that adds it; the watermark is just a styled <div> with a backdrop-blur and small uppercase text.
That matters because users can't remove the watermark client-side without modifying the page DOM, and even if they do, the watermark is back the next time the page reloads. We've put the case for why this matters in a separate post — short version: the watermark is provenance for the audience that sees the screenshot downstream of the original creator.
Deploy: Cloudflare Workers via OpenNext
We started on Cloudflare Pages with @cloudflare/next-on-pages, hit Next 16 compatibility walls, and switched to @opennextjs/cloudflare — the modern adapter that compiles Next.js to a Workers bundle. Every push to main triggers Workers Builds: clone, install with pnpm, run opennextjs-cloudflare build, run wrangler deploy. Total time from push to production: ~90 seconds.
Static assets — including 18 prerendered OG image PNGs and the assets manifest of every JS chunk — get uploaded to Workers Static Assets and served from the global edge. The worker itself does almost nothing; it's a thin shim that hands requests to the static asset bucket.
What we'd do differently
- Custom fonts in OG images.The OG image renderer uses Satori, which needs fonts loaded explicitly. We're currently using system-font fallbacks, which is fine but loses brand consistency between the on-page render and the OG card. Loading Archivo Black and Space Grotesk into Satori would fix it.
- Shareable tool state via URL params.A natural next step — let creators share a pre-filled tool state link so others can remix. The architecture supports it (state lives in React, encoding it as URL params is straightforward), it's just not built yet.
- Group chat support in the iMessage generator. The current generator handles 1-on-1 chats well. Group chat support (with per-sender avatars, names above bubbles, group title in the header) is the highest-asked feature in our inbox.
The stack, in one paragraph
Next.js 16 App Router, React 19, TypeScript, Tailwind v4, Lucide icons + inline-SVG brand marks, html-to-image for the PNG export, next/og + Satori for OG image generation, @opennextjs/cloudflare for the build, Cloudflare Workers Static Assets for serving, Cloudflare Workers Builds for CI/CD on every push, Cloudflare for DNS and the www → apex 301 redirect rule. No database, no API routes, no server functions. Everything that takes user input runs on the user's machine.
If any of this was useful, the easiest way to send the post anywhere is our RSS feed — new pieces drop about once a week.