Something to Say
URL-based poster maker on Cloudflare Workers. paste text into the URL and get a styled image back.
Prerequisites
- Node.js 18+
- A Cloudflare account with Workers enabled
- (Optional) An Upstash Redis account for PNG caching
Setup
1. Install dependencies
npm install
2. Login to Wrangler (Cloudflare CLI)
npx wrangler login
A browser window opens — authorise the CLI to access your Cloudflare account.
3. Create the D1 database (request tracking)
npx wrangler d1 create image_cache_db
Copy the returned database_id into wrangler.toml under [[d1_databases]].
Then run the migration:
# local dev
npx wrangler d1 execute image_cache_db --local --file=./schema.sql
# production (after first deploy)
npx wrangler d1 execute image_cache_db --remote --file=./schema.sql
4. Set up Upstash Redis (optional, for PNG caching)
PNG generation is slow without caching. SVG is instant. Redis caches generated PNGs so repeat requests are fast.
- Go to https://upstash.com and sign up
- Create a Redis database (free tier is enough)
- Copy the REST URL and REST Token from the dashboard
For local dev: Create .dev.vars in the project root:
UPSTASH_REDIS_REST_URL=https://your-upstash-url.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-token
For production:
npx wrangler secret put UPSTASH_REDIS_REST_URL
# paste the URL, press Enter, Ctrl+D
npx wrangler secret put UPSTASH_REDIS_REST_TOKEN
# paste the token, press Enter, Ctrl+D
5. Configure a custom domain
Before you can route a worker to your own domain, the domain must exist in your Cloudflare account as a zone.
If your domain isn't in Cloudflare yet:
- Go to the Cloudflare dashboard → Add a Site
- Enter your domain (e.g.
mydomain.com), pick the free plan - Cloudflare will show two nameserver addresses (e.g.
ns1.cloudflare.com,ns2.cloudflare.com) - At your domain registrar, update the nameservers to those Cloudflare-provided ones
- Wait for propagation (minutes to hours — Cloudflare checks automatically)
- Your domain now appears under Websites in the dashboard
Once the domain is in Cloudflare, you have two options:
Note: Worker custom domains are managed under Workers & Pages in the Cloudflare dashboard (not the DNS section). The dashboard shows each custom domain's status and lets you remove or re-trigger provisioning.
Option A: automatic via custom_domain (recommended)
Add to wrangler.toml:
routes = [
{ pattern = "yourdomain.com", custom_domain = true }
]
On deploy (npx wrangler deploy), Cloudflare auto-provisions the DNS record and SSL certificate. No manual DNS changes needed.
Also update PUBLIC_BASE_URL so OG image URLs are correct:
[vars]
PUBLIC_BASE_URL = "https://yourdomain.com"
Option B: manual CNAME record
If custom_domain doesn't work (e.g. the zone isn't fully active yet), create the record manually:
- Cloudflare dashboard → your domain → DNS
- Add Record:
- Type:
CNAME - Name: the subdomain, e.g.
say(forsay.mydomain.com) - Target: your worker's
*.workers.devURL (shown afterwrangler deploy, e.g.my-worker.jeancnicolas.workers.dev) - Proxy: Proxied (orange cloud)
- Type:
- Remove the
routesblock fromwrangler.toml(the CNAME handles routing instead) or keep it — they can coexist.
Verify
After deploying with either option, visit your domain. If it doesn't resolve, check:
- Is the domain listed under Websites in your Cloudflare dashboard? If not, add it first (steps above).
- Did you redeploy after adding the route? Run
npx wrangler deployagain. - Check route status:
npx wrangler triggersshows the current routes for your worker. - DNS propagation:
dig say.mydomain.comor use an online DNS checker to see if the record resolves.
Local dev
npm run dev
Opens at http://localhost:8787.
Deploy
npx wrangler deploy
First deploy checklist
After the first deploy:
- D1 migration (if not done yet):
npx wrangler d1 execute image_cache_db --remote --file=./schema.sql - Redeploy to pick up any config changes:
npx wrangler deploy
How it works
/— interactive editor (drag sliders, pick themes, preview live)/say/hello/world?theme=neon-noir— styled poster as SVG (instant)/say/hello/world?theme=neon-noir&png— styled poster as PNG (slower, cached)?svg/?png— explicitly request format?preset=x— platform presets (twitter, linkedin, instagram, etc.)?vw=N— custom viewport width (px)?scale— fluid mode (lines fill the container width)?ir=0.8— inner content width ratio (0.3–1.0, default 1.0)- Inline markers:
**bold**,*italic*,==highlight==,~underline~,{accent:text},{color:text}
Updating secrets
To update an existing secret:
npx wrangler secret put UPSTASH_REDIS_REST_URL
Then redeploy. No code changes needed.
Viewing logs
npx wrangler tail
Cache headers
PNG responses include X-Cache: HIT or X-Cache: MISS so you can verify Redis caching in the Network tab.