Skip to content

Iris

Iris is a site for a local photographer who shoots a bit of everything: weddings, portraits, families, newborns, events, headshots, products and pets, from a small studio or on location. It is built like a darkroom and a light table: at night (the default, following the visitor’s system) a warm near-black room under a red safelight, by day a warm white light table, with fine film grain over both. Headings are set in a high-contrast serif with its italic for marked words, camera data in spaced capitals. The demo is a fictional studio, Nora Vale Photography.

Iris 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.

Page Edit it in
Home Pages → Home page (a list of sections)
Sessions, journal, 404 Pages → the page’s own entry
About (the approach, a photo; the people come from Team) Pages → About page
Pricing (packages, how booking works, booking, deposits and gift vouchers, questions) Pages → Pricing page
Studio (hours, before you come, map, details, contact form, questions) Pages → Studio page
Sessions (line, summary, how long, price, delivery, included, how it goes, questions, cover, gallery) Content → Sessions
Team (role, what they shoot with, biography, portrait) Content → Team
Journal posts Content → Journal
Name, contact, area covered, navigation, header button, profiles Pages → Site settings
Buttons, labels, lightbox, chat, emails, game texts Content → Interface texts
Privacy, terms, image use Pages → Legal

In headlines, words wrapped in *stars* are set in italics and underlined in the accent, as in “Every kind of *day*”.

The home page opens with a viewfinder. The photo sits behind an aperture whose blades open when the page loads; corner brackets, a frame counter and the camera settings sit on top, and focus brackets follow the pointer. Press the shutter button (or wait a few seconds) and the blades snap shut and open on the next photo, while the word in the headline changes with it: “Photographs of your weddings”, then newborns, families… Each photo is a slide in Home → Through the lens with its word, its description and its settings. Only the first photo is part of the first paint; the others load after the page. With reduced motion the blades stay open and photos change only on the button.

Every session has a page: the cover across the window, how long it takes, the price and what you deliver, the description and what’s included, how it goes, the gallery and questions. Galleries take photos in any shape and open in a lightbox (previous and next with the buttons, the arrow keys or a swipe; Escape closes it). Each photo is also a plain link to its full-size file, so it works without script. The home page’s film strip lists the sessions on a strip of film that slides as you scroll; its contact sheet takes frames in turn from every gallery, with a loupe under the pointer.

The pricing page holds up to four packages (mark one as the favourite), how booking works in three steps, booking a call with Cal.com, and what you sell with Stripe (the demo: a session deposit, a wedding deposit and a gift voucher), each with a one-time Price id. Without keys, the booking shows a preview of the calendar and the payment buttons explain how to switch them on.

The “Find the studio” map is drawn from OpenStreetMap in your colors: streets, green spaces, water and buildings round the studio, a pin with your name, your address and directions links. It is part of the page (no embed, no cookies). Put the studio’s coordinates in map.config.json and run pnpm map -- fetch.

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

Feature Where Service
Book a call Pricing page, loads on click; a preview of the calendar until connected Cal.com
Deposits and gift vouchers Pricing page (one-time Price ids) Stripe
Newsletter Home and journal pages, with a welcome email Supabase
Contact form Studio page; messages go to the email in Site settings Resend
AI chat On every page, answers about sessions, prices, booking and the studio; never promises a date Claude
Search ⌘K or the search button: sessions, people, journal, pricing, studio built in, or Algolia
Analytics Every page; Google Analytics only after consent Plausible, Umami, Google Analytics

The home page is built from these sections, in any order: Through the lens (the opening), Sessions on a film strip, From shoot to delivery (prints developing in a tray as they scroll in), Contact sheet, Meet the photographer, Numbers, How to find the studio (the map), Journal, Newsletter and Closing call (a wide photo in viewfinder corners).

src/content/singletons/brand.json (Pages → Brand in Keystatic) holds three colors: the accent (the safelight: buttons, marks, the record light), the second (film amber: frame numbers and light fills) and the third (negative cyan: focus brackets). Buttons are darkened until their white text is readable and every pair is checked; a color that can’t get there stops the build with a message. Brand also sets the fonts (headings and text) and the corners (square like a print, or rounded).

Iris is ready for more languages: see Languages.

A small game, Rule of thirds: a balloon drifts across a scene. Steer the viewfinder (pointer, drag or the arrow keys) so it sits on one of the four thirds points and press the shutter (a click, Space or the button): up to 100 points by how close it sits, nothing if it’s outside the frame. Five frames, each drifting faster, printed on a little contact strip; 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, sessions, people and journal for AI assistants, in every language.
  • Pages carry JSON-LD: the studio as a ProfessionalService with its packages as offers, sessions as Services with their price, the people as Persons, booking as a HowTo, questions as a FAQPage, and each journal post as a BlogPosting.
Terminal window
pnpm dev # local site, editor at /keystatic
pnpm build # production build
pnpm map -- fetch # draw the map round map.config.json from OpenStreetMap
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).