Skip to content
Sean Swanson_
All work

Web / 2026 / Live

Halcyon Yoga

Twenty-one pages for an invented Edinburgh yoga studio, arranged around the one thing the site has to do: get a stranger booked into their first class.

Screenshot of Halcyon Yoga

What it is

A boutique studio in Stockbridge, Edinburgh, that does not exist. Fifteen public pages, five admin pages and a 404: three class descriptions, five teacher profiles, a timetable, pricing, a visitor guide and seven long-form journal essays. About fourteen thousand words, all written for this project.

A studio is a good category to test against because it is full of sites that look expensive and do nothing. Soft photography, a nice typeface, and then a phone number in the footer and no way to actually get into a class.

The one job

I made the funnel testable rather than aspirational. Three rules hold it together, and each one is enforced by a test that runs against the built pages.

Every page-level primary button leads to the booking flow. Before that rule existed, three pages had an email inquiry as their loudest action, which is exactly the failure the category is full of. No page is a dead end: every page, the 404 included, offers a route to booking from inside its own content and not just from the header. The 404 failed that check and had to be rebuilt as a recovery page.

And intent carries. Choosing a class opens the flow at step two with that class already selected, choosing a time from the timetable opens it at step three, and all thirty-one slots carry their own link. Nobody is asked the same question twice.

How the flow degrades

The four steps ship as a single document and JavaScript turns them into a stepper, so with scripting off you get one readable form instead of a blank page. It ends in a personalized confirmation and a calendar file.

Nothing is submitted anywhere, and the page says so at the point where a real one would take your details. A concept build that quietly pretends to collect a booking is worse than one that admits it.

The photography problem

Stock photography comes from a dozen photographers under a dozen lighting setups, and a page of it looks exactly like what it is.

So every source photograph runs through one shared grade in the build: saturation pulled down so loud props stop shouting, a per-channel warm push toward the studio color, a soft wash and a deterministic film grain. Framing is declared rather than automatic, and the width budget is measured against the crop so a tighter frame never quietly costs sharpness. The overhead shot became hands pressing into a wood floor, and the one frame with a mural in it was retired rather than carried.

The teachers are generated tiles and initials rather than photographs. They are invented people, and a real face would be a claim about somebody who never agreed to it.

What it measures

First contentful paint of 224 ms on the home page and cumulative layout shift of 0.000 on every page, in headless Chromium against the production headers. Ten requests for the home page, no render-blocking CSS, and about 17 kB of JavaScript across the public site, roughly half of it the booking flow and loaded only there.

The stylesheet is inlined into every page rather than fetched, which is why first paint never waits on a round trip. Every image ships with intrinsic dimensions and a blurred placeholder and every web font has a metric-matched local fallback, which is why the shift figure is a zero and not a small number.

Two things that would have shipped broken

The stylesheet was originally deferred with an inline onload handler. The site content security policy blocks inline script, so every page would have arrived in production completely unstyled. It was caught only because the local preview server replays the production headers instead of serving files plainly. A content security policy you never test against is decoration.

Adding a package.json flipped the Vercel project from a static deploy to a framework build, which then failed looking for an output directory that does not exist. The fix was to tell the host explicitly that there is no build step, because the generator writes finished HTML into the repository root. Nothing to configure on deploy is the whole point of building it that way.