Skip to content

Caldera

Caldera is a site for a thermal bath or spa: thermal baths, bathhouses, saunas, wellness centres. It is made like an Art Nouveau bathhouse. The home page opens on the baths’ floor plan in 3D on a turquoise mosaic: cream tiled floors, white walls raised. The pools start empty and the saunas cold. As you scroll, the camera goes to the way in, then to each bath in turn, and the bath comes alive: its pools fill with turquoise water, or the room glows warm, with a card beside it (its name, temperature and hours). At the end every bath is full. Turquoise, gold and terracotta on cream tiles, photos under arches like bathhouse windows, headings in a Secession display face. The demo is a fictional bath under a glass dome in Budapest, Opal Baths, with six baths.

Caldera comes in two variants, chosen when you create the site: files/Sanity (static pages, any host) and EmDash (--cms emdash, rendered on request on Cloudflare). The pages are the same in both.

The plan is drawn from data, so it can be any bath. Edit it in Pages → The floor plan:

  1. Set the width and height in metres.
  2. Add your areas as rectangles (x, y, w, h in metres, from the top-left corner). Link an area to a bath by the bath’s file name (e.g. hot-spring): it takes the bath’s colour, name and number. Leave it empty for the entrance, changing rooms, the café. Mark one area as the entrance. Keep areas edge to edge, so their walls line up.
  3. Add the doors: the middle of each door on a wall, its width, and whether that wall runs up and down.
  4. Add the pools and furniture as rectangles and circles. Mark pools as pool: they show empty until their bath fills, then fill with water.

A bath that doesn’t hold water (a sauna, a steam room: turn off water on the bath) glows warm instead. Baths fill in the order of the areas. The same plan shows flat on the baths page and on each bath’s page, with that bath full. With reduced motion every bath stands full and the cards are a list under the plan.

Page Edit it in
Home Pages → Home page (a list of sections; the floor plan’s texts are in its first section)
Baths (a filter by kind, the baths, the plan, the bathing rules) Pages → Baths page and Content → Baths
A bath (picture, kind, temperature, hours, good to know, story, close-up, the plan) Content → Baths
Passes (day passes and gift cards with checkout, how they work) Pages → Passes page
Treatments (treatments and prices, booking a time) Pages → Treatments page
Journal Pages → Journal page and Content → Journal
Visit (hours, the map, safety, getting here, questions, the contact form) Pages → Visit page
Name, contact, navigation Pages → Site settings
The floor plan Pages → The floor plan

In headlines, words wrapped in *stars* are set in turquoise over a gold ripple.

Each pass has a name, a price, a few words, a colour and a checkout. Turn on “Book online” and add a Stripe Price id (or a Lemon Squeezy variant id), and the button takes visitors to checkout; after paying they get an email to show at the turnstile and come back to the passes page with a message. Treatments list their prices and book a time with Cal.com.

Everything works without keys; each feature switches on with its variables in .env.example.

Feature Where Service
Passes and gift cards Passes page Stripe or Lemon Squeezy
Treatments Treatments page, loads on click Cal.com
Newsletter Home and journal pages, with a welcome email Supabase or Mailchimp
Contact form Visit page; messages go to the email in Site settings Resend
AI chat Every page; answers from your baths, passes, treatments, visit details and journal Claude or OpenAI
Search ⌘K or the search button: baths, passes, treatments, journal, visit built in, or Algolia
Analytics Every page; Google Analytics only after consent Plausible, Umami, Google Analytics, PostHog
Members-only posts Journal Clerk

The home page is built from these sections, in any order: The floor plan (the signature), Baths, How a visit works, Feature (e.g. treatments), Numbers, Journal, Newsletter and a closing call over a photo from edge to edge.

src/content/singletons/brand.json (Pages → Brand in Keystatic) holds three colors: the accent (turquoise: buttons, water, links, the night’s hue), the second (gold: the rims, the ripple under marked words, warm rooms) and the third (terracotta: saunas and fills). Every bath picks one of the three. Buttons are darkened until their white text reads; every color is adjusted for each theme, and one that can’t get there stops the build with a message. Brand also sets the fonts (text, headings, labels) and the corners (soft or arched). The site opens by day; night comes from the toggle.

The visit page shows a map of the streets around you, drawn from OpenStreetMap: set your entrance’s coordinates in map.config.json and run npm run map -- fetch.

Caldera is ready for more languages: see Languages.

A small game, Just right: the attendants’ old puzzle. With two or three jugs that hold only so many litres, measure exactly the litres the pool needs: fill a jug from the spring, empty it, or pour one into another until it is full. Three pools, each harder. The fewest moves is worked out for every pool, and each scores 100 × fewest ÷ yours. The best score is kept in the visitor’s browser.

  • AGENTS.md describes the project for coding assistants (Claude Code reads it through CLAUDE.md, Cursor through .cursor/rules/).
  • /llms.txt lists your pages and baths for AI assistants, in every language.
  • Pages carry JSON-LD: the baths as a PublicBath, each bath as a TouristAttraction, passes as an OfferCatalog, treatments as a Service, questions as a FAQPage, posts as BlogPostings.
Terminal window
pnpm dev # local site, editor at /keystatic
pnpm map # redraw the visit page's map (npm run map -- fetch)
pnpm build # production build
pnpm cms sanity schema|seed # move content to Sanity (see Content → Sanity)

In the EmDash variant: npm run dev, npm run deploy (Cloudflare) and npm run emdash:schema (after changing the content model).