A simple Cloudflare Worker that acts as a secure proxy for the ScreenshotOne API, specifically for taking screenshots of Stagetimer.io URLs.
This worker wraps the ScreenshotOne API to:
- Hide API credentials from client-side code
- Restrict screenshots to stagetimer.io domains only
- Return screenshot images directly to clients
- Apply consistent screenshot settings across the platform
npm installCreate a .dev.vars file in the project root with your API keys:
SCREENSHOTONE_ACCESS_KEY=your_access_key_here
SCREENSHOTONE_SECRET_KEY=your_secret_key_here
CACHE_KEY=v1This file is already in .gitignore and will not be committed.
Set your ScreenshotOne API credentials as Cloudflare secrets:
wrangler secret put SCREENSHOTONE_ACCESS_KEY
wrangler secret put SCREENSHOTONE_SECRET_KEYYou'll be prompted to enter each value. These are stored securely in Cloudflare.
The CACHE_KEY is configured in wrangler.jsonc and can be changed to invalidate all cached screenshots on the next deployment. Simply update the value (e.g., from v1 to v2) and redeploy.
Run the worker locally:
npm run devThis starts a local server at http://localhost:8787
Deploy to Cloudflare Workers:
npm run deployThe worker will be available at https://stagetimer-screenshotone-proxy.<your-subdomain>.workers.dev
The worker supports three URL formats:
Path-based encoded (recommended - clean filenames):
GET /{domain__{path}__{parts}}.jpg
Use __ (double underscore) to replace slashes.
Path-based literal (for manual testing):
GET /{domain}/{path/with/slashes}.jpg
Query parameter (legacy):
GET /?url=<target-url>
Encoded format (clean filename):
# https://stagetimer.io/output/688cbdf38490dac6f25c0eba/
curl "https://stagetimer-screenshotone-proxy.workers.dev/stagetimer.io__output__688cbdf38490dac6f25c0eba.jpg"
# → filename: stagetimer.io__output__688cbdf38490dac6f25c0eba.jpgLiteral format (for testing):
curl "https://stagetimer-screenshotone-proxy.workers.dev/stagetimer.io/output/688cbdf38490dac6f25c0eba/.jpg"
# → filename: 688cbdf38490dac6f25c0eba.jpg or .jpgQuery parameter format:
curl "https://stagetimer-screenshotone-proxy.workers.dev/?url=https://stagetimer.io/pricing"Use the included script to convert any Stagetimer URL to the worker format:
node scripts/convert-url.js "https://stagetimer.io/output/688cbdf38490dac6f25c0eba/?v=2&signature=..."Output:
📸 Screenshot Worker URL:
https://stagetimer-screenshotone-proxy.workers.dev/stagetimer.io__output__688cbdf38490dac6f25c0eba.jpg?v=2&signature=...
📁 Filename:
stagetimer.io__output__688cbdf38490dac6f25c0eba.jpg
// Encoded format (recommended - clean filename)
// Replace slashes with __ (double underscore)
const url = 'https://stagetimer.io/output/688cbdf38490dac6f25c0eba/'
const encodedPath = url.replace('https://', '').replace(/\//g, '__').replace(/__$/, '') // Remove trailing __
const screenshotUrl = `https://stagetimer-screenshotone-proxy.workers.dev/${encodedPath}.jpg`
// → stagetimer.io__output__688cbdf38490dac6f25c0eba.jpg
// Or for simple paths without slashes
const screenshotUrl = 'https://stagetimer-screenshotone-proxy.workers.dev/stagetimer.io__pricing.jpg'
// Query parameter (legacy)
const screenshotUrl = `https://stagetimer-screenshotone-proxy.workers.dev/?url=${encodeURIComponent('https://stagetimer.io/pricing')}`
// Use in an img tag
<img src={screenshotUrl} alt="Screenshot" />
// Or fetch directly
const response = await fetch(screenshotUrl)
const blob = await response.blob()URLs with #anchor fragments automatically scroll to that element before taking the screenshot, using ScreenshotOne's scroll_into_view parameter. Since browsers don't send # fragments to servers, encode # as %23 in path-based URLs:
# Scroll to #picture-in-picture on the page
curl "https://stagetimer-screenshotone-proxy.workers.dev/stagetimer.io__output__abc%23picture-in-picture.jpg"
# Query parameter format (encode # as %23 in the url value)
curl "https://stagetimer-screenshotone-proxy.workers.dev/?url=https://stagetimer.io/output/abc/%23picture-in-picture"Priority: explicit scroll_into_view override > URL anchor > default main.
The worker applies the following settings to all screenshots:
- Format: JPEG
- Viewport: 1200x627px
- Device scale: 1x
- Blocks: Ads, cookie banners, trackers
- Cache: 30 days (controlled by
CACHE_KEYenv var) - Scroll target:
mainelement (or#anchorif present in URL)
These match the settings used in the current Stagetimer landing page implementation.
The worker includes helpful metadata in response headers:
Content-Disposition: Provides the filename (e.g.,inline; filename="stagetimer.io__output__123.jpg")X-Image-Width: Image width in pixels (1200)X-Image-Height: Image height in pixels (627)Content-Type: Alwaysimage/jpegCache-Control: Set to 30 days
These headers are exposed via CORS, so you can access them from JavaScript:
const response = await fetch(screenshotUrl)
const filename = response.headers.get('content-disposition')?.match(/filename="(.+)"/)?.[1]
const width = response.headers.get('x-image-width')
const height = response.headers.get('x-image-height')
console.log(`${filename}: ${width}x${height}`) // stagetimer.io__output__123.jpg: 1200x627All screenshots use the same cache key from the CACHE_KEY environment variable. To invalidate all cached screenshots (e.g., after a major design change), simply update the CACHE_KEY in wrangler.jsonc (e.g., from v1 to v2) and redeploy.
- Only allows GET requests
- Restricts screenshots to
*.stagetimer.iodomains - API credentials are stored as Cloudflare secrets (never in code)
- CORS enabled for cross-origin requests
View real-time logs:
npm run tailOr view in the Cloudflare dashboard: https://dash.cloudflare.com
400 Bad Request: Missing or invalid URL parameter403 Forbidden: URL is not a stagetimer.io domain405 Method Not Allowed: Non-GET request500 Internal Server Error: Screenshot service error or missing API keys