A photography portfolio that never shows a blank page, even if the backend is down
A vanilla-JS gallery on Vercel, a Cloudflare Worker API over Turso and R2, and a data layer that degrades to bundled sample data instead of a blank page when the API is unreachable.
Project: Katie Monroe Photographyopen →Katie Monroe is an on-location photographer character in a Los Santos roleplay community. The site is a public portfolio plus a private console she uses to manage it herself. No CMS to log into somewhere else, no editing raw files for every new shoot — and if the API ever goes down, visitors still see a working gallery instead of an error page, which matters more than it sounds for a client-facing site.
Two halves, two hosts
The front end is vanilla JavaScript built with Vite and deployed on Vercel. No React, no framework — just ES modules for the gallery, lightbox, motion, and story routing. The API is a separate Cloudflare Worker with its own deploy, backed by Turso (libSQL) for data and R2 for images. Auth is a signed JWT.
Splitting it this way keeps the public site a cheap bundle of static assets and puts everything that needs secrets behind the Worker.
The data layer degrades instead of breaking
The one decision I'd point to: the front end never assumes the backend is there. loadPortfolio returns bundled sample photos when no Worker URL is configured, and only fetches live data when one is set.
A fresh clone runs and looks complete with zero setup. Wire in the Worker and the same UI switches to live content. The commission form takes the same stance — if there's no backend, it fails honestly instead of faking a success and dropping the message.
The Worker is a hand-rolled router
The API is one file matching method and path, with a gated() wrapper for anything that touches admin data. No framework, so the auth boundary is impossible to miss — if a route isn't wrapped, it isn't protected, and that's visible at a glance.
Rate limiting is a Turso-backed fixed window, so public endpoints like the commission form can't be hammered. The whole thing has 164 tests across the Worker, the front end, and the admin console.
What I'd improve
- The hand-rolled router is fine at 31 routes but is getting long; a small matcher would read better without pulling in a framework.
- Image handling uploads straight to R2 with no resizing step — I'd add derivative sizes before this grows.
- The admin console shares no code with the public site; a little shared rendering would cut duplication.
This "degrade instead of break" approach is something I try to build into every client site — see how I approach this on other projects or get in touch if you want it on yours.