Skip to content

feat(workers): screenshot surfaces as PNG via /s/:id.png#126

Merged
benvinegar merged 7 commits into
mainfrom
elucid/png-rendering-in-cf
Jun 24, 2026
Merged

feat(workers): screenshot surfaces as PNG via /s/:id.png#126
benvinegar merged 7 commits into
mainfrom
elucid/png-rendering-in-cf

Conversation

@elucid

@elucid elucid commented Jun 24, 2026

Copy link
Copy Markdown
Member

What

Append .png to any surface URL to get a screenshot:

https://sideshow.kilodollar.ca/s/162e2c8c.png

Uses Cloudflare Browser Rendering's quickAction("screenshot") to render the surface page at 1200×630 and return a PNG.

How it works

  1. GET /s/:id.png is intercepted in the Workers entrypoint (before the DO)
  2. The request is forwarded to the DO as /s/:id?part=0 with the user's original headers/cookies — the app's auth middleware decides whether to allow it
  3. If the app returns non-200 (401, 404, etc.), that response is passed through unchanged
  4. Only on 200: Browser Rendering screenshots the page (authenticating with the server's own token) and returns the PNG

Auth is fully delegated to the app — the entrypoint has zero auth logic, so changes to auth rules are automatically respected.

Changes

  • wrangler.jsonc: add browser binding
  • workers/index.ts: ~25 lines in the entrypoint to intercept .png requests

Zero changes to server/ or the viewer.

Details

  • 1200×630 viewport (OG-image / social-preview friendly)
  • Cache-Control: public, max-age=300 for edge caching
  • networkidle0 wait ensures JS-rendered content is captured
  • Only works for surfaces with html parts (the /s/:id route only serves those)

Add Browser Rendering support to the CF entrypoint. GET /s/:id.png
screenshots the rendered surface page and returns a PNG image.

Auth is fully delegated to the app — the user's credentials are forwarded
to the DO, and the screenshot only proceeds if the app returns 200.
The browser rendering call authenticates with the server's own token.

- Add browser binding to wrangler.jsonc
- Intercept .png requests in the entrypoint before DO dispatch
- 1200×630 viewport (OG-image friendly)
- Cache-Control: public, max-age=300 for edge caching
@elucid elucid marked this pull request as draft June 24, 2026 14:01
elucid added 6 commits June 24, 2026 10:06
Forward ?theme= and ?mode= to the /s/:id page so screenshots match the
viewer's rendering. Without mode pinning, headless Chrome's OS default
produced mismatched colors.

- Default width 800px (was 1200), configurable via ?w= (clamped 320–1920)
- Mode defaults to light (headless Chrome's environment), ?mode=dark supported
- Theme falls back to board setting; ?theme= overrides
- Viewport height maintains 1200:630 aspect ratio
Use fullPage: true so the screenshot captures the entire surface content
instead of clipping to a 1200:630 viewport. Tall surfaces were being cut
off. The viewport height (800) is now just the initial window size.
Appending ?nocache to a .png URL bypasses the edge cache and returns
Cache-Control: no-store, ensuring a fresh browser render.
Browser Rendering's quickAction cache (default 5s) keys on the URL
path and ignores query params, so ?theme= overrides were returning
stale screenshots. Set cacheTTL: 0 to disable it — we handle caching
ourselves via Cache-Control headers.
The viewer now persists the resolved OS color-scheme in a
sideshow_mode cookie. The .png screenshot handler reads it as the
default mode, so screenshots match what the user sees without needing
an explicit ?mode= param. Explicit ?mode= still overrides.
@elucid elucid marked this pull request as ready for review June 24, 2026 14:32
@benvinegar benvinegar merged commit bd8df08 into main Jun 24, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants