Skip to content

Locale

Locale is a site for a new residential building: a developer’s project, an estate agent’s new release, a housing co-operative. The home page opens on the building’s front, a photograph with every flat in the release lit in its own windows by its status: free flats glow warm, as if someone were home, reserved ones in lilac, sold ones stay dark. Point at a flat and its card appears; click it and you are at its card on the apartments page, with a floor plan drawn from its rooms and area. Around the building, your neighbourhood is drawn from OpenStreetMap in a vivid 3D style, and every school, park, market and stop shows its minutes on foot from the front door, measured along the streets. Emerald, tangerine and lilac on a cream site plan, headings in a high-contrast serif. The demo is a fictional building of 48 flats in Vienna’s Leopoldstadt, Kastanienhof.

Locale 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 template draws the map itself, so it can be any city:

  1. Set the area in map.config.json: area with its south, west, north and east edges in degrees (right-click on openstreetmap.org → “Show address” gives you a point’s latitude and longitude). About 2 km across works best, with your building near the middle.
  2. Run npm run map -- fetch. It downloads the area from OpenStreetMap (one request, up to a minute), builds the map’s geometry and the walkable streets in src/data/map/ and draws four base maps into public/map/: the city in 3D and as a plan, each by day and at dusk, saved as AVIF in every width the site uses.
  3. Put your building in Pages → The map (building: its name, latitude and longitude). Give your places their latitude and longitude and your walks their route (the places in order, plus waypoints to steer it). The minutes on foot are measured along the real streets when the site builds, at 80 metres a minute, so they are never typed by hand.

The map’s colours are in map.config.json → colors; after changing them, npm run map -- render redraws the maps without downloading again. Map data is © OpenStreetMap contributors, under the Open Database License. Every map shows the attribution; keep it.

Page Edit it in
Home Pages → Home page (a list of sections)
Apartments (the lit front, a filter by rooms, the flats with their status, price and floor plan, reservations, viewings) Pages → Apartments page and Content → Apartments
Neighbourhood (every place on the map, a filter by kind, the minutes from home) Pages → Neighbourhood page and Content → Places
A place (what it is, what it means for the people who live here, nearby on the map) Content → Places
Walks (every walk on the city) and a walk (on the plan, place by place with its minute) Pages → Walks page and Content → Walks
Journal Pages → Journal page and Content → Journal
Visit (the building and the nearest stops on the plan, directions, details, the contact form, questions) Pages → Visit page
Name, city, contact, navigation Pages → Site settings
Your building, district names on the map Pages → The map

In headlines, words wrapped in *stars* are set in emerald on a tangerine underline.

The front pictures (by day, and at dusk for the dark theme, same size and framing) are in Pages → The map → Your building. src/data/front.json says where their windows are, floor by floor, in percent of the picture; each apartment names its floor and its first and last window (Content → Apartments → On the building front). With your own pictures, measure their windows once and the flats light up in the right places.

Each apartment has its rooms, area, outdoor space, floor, where it faces, price, status (available, reserved or sold), a few words and a picture. The apartments page filters them by rooms without JavaScript. Turn on “Reserve online” on an available apartment and add a Stripe Price id (or a Lemon Squeezy variant id) for the reservation fee, and its button takes buyers to checkout; after paying they get an email and come back to the apartments page with a message. Viewings of the show flat are booked with Cal.com.

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

Feature Where Service
Reservations Apartments page Stripe or Lemon Squeezy
Viewings Apartments and walks pages, loads on click Cal.com
Newsletter (new homes and building news) 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 apartments, places (with their minutes), walks, visit details and journal Claude or OpenAI
Search ⌘K or the search button: apartments, places, walks, 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 building (the signature: the front with every flat lit by its status), Apartments (the free ones first), Numbers, Around the corner (places as cards with their minutes), Walks, 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 (emerald: buttons, pins and the walks), the second (tangerine: marked words, the band on panels and your building on the map, free flats’ windows) and the third (lilac: soft fills, reserved flats). 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. The theme follows the system, day or evening, unless the visitor picks one with the toggle; the maps switch with it.

Locale is ready for more languages: see Languages.

A small game, Best address: a 7 × 7 plot with blocks of houses, empty lots and a few places (a school, the tram, a market, a park, a café), each visited so many times a week. Build your home on the lot where the walking adds up to the fewest minutes a week (tap a lot, or arrows and Enter); streets go round the blocks. The best lot is found by measuring every lot from every place, and you score 100 × the best ÷ yours. Three plots, each with more places. 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, walks and places for AI assistants, in every language.
  • Pages carry JSON-LD: the building as an ApartmentComplex with its Apartments and their prices and availability, the sales office as a RealEstateAgent, walks as TouristTrips, places as TouristAttractions, questions as a FAQPage, posts as BlogPostings.
Terminal window
pnpm dev # local site, editor at /keystatic
pnpm map # draw your neighbourhood (npm run map -- fetch | build | render)
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).