
The problem
Most studio websites are one of two things. Either a WordPress theme, which means plugins, patches, and a site that gets slower every year, or a page builder, which is fast to launch but rents you your own website forever. Stop paying and it disappears.
We spend a lot of time telling clients to own their code and keep their stack small. It would have been strange to publish that argument on a site that ignored it.
There was a practical constraint too. This had to be maintainable by one person alongside client work. That rules out anything with a build pipeline to babysit, a framework to keep current, or a hundred packages that each need watching. The biggest challenge wasn't "can I build this?" It was "will I still want to touch this in two years?"
How we approached it
There is no framework, and no build step, the site you are reading is a single JavaScript file and a folder of static assets which handles all: the pages, blog, admin panel, forms, media, sitemap.
The admin panel manages leads, blog posts, case studies, client reviews, products, the about page and newsletter subscribers. Authentication is an identity check rather than a password in a database, with an IP allowlist behind it as a second layer.
Public forms get Turnstile, a honeypot field and KV-backed rate limiting. Every database query is parameterised. The Content-Security-Policy is strict, and the admin panel runs under a separate, stricter one.
A few decisions that only look small until you skip them. The sitemap is generated from the database on request, so publishing a post puts it in the sitemap immediately, therefore, no file to edit, no deploy. Navigation is defined once and injected into the static pages at the edge by a streaming HTML rewriter, so it cannot drift between templates. Structured data is emitted per page type so search engines and AI crawlers get an accurate picture rather than guessing.
Data handling got the same treatment as the code. IP addresses are truncated at the point of collection rather than stored in full. Enquiries are deleted automatically after 24 months by a scheduled job and the retention period in the privacy policy is enforced in software, not left to memory.
Lighthouse [97] mobile · [0.4]s LCP · [100] desktop · $0/month hosting · zero dependencies
What the visitor sees
What the owner sees
Built with
What I'd do differently
Three things, all of which cost me time later.
The navigation started as markup copy-pasted into six files. It drifted, exactly as you would expect. It is now defined once and injected at the edge, which is where it should have started. Any content that appears on every page needs a single source from day one, not once you notice the copies disagreeing.
The sitemap was a static XML file. Blog posts live in the database, so the file could not possibly know about them, therefore, every publish needed a manual edit I had to remember. Obvious in hindsight. It is now generated from the database on request and cannot fall behind.
The one that actually cost something: I turned on "Block AI training crawlers" rule, assuming it separated training crawlers from search crawlers. It does not, for Google, Google-Extended is a robots.txt signal riding on Googlebot's own infrastructure, so blocking it blocks Googlebot. My sitemap silently stopped being readable and crawl discovery fell to six per cent before I found it in the security logs. The fix took ten minutes; noticing took considerably longer. Security rules that match on crawler identity need verifying against real traffic, not assumed to behave as labelled.