Skip to content

Pace

Pace is a site for a running event in a real city: a city race, a night run, a charity run, a running club’s race weekend. Your city is drawn from OpenStreetMap in a vivid 3D style (pastel walls and terracotta roofs by day, electric violet and neon windows at night), and on the home page the course draws itself along the real streets as you scroll: the camera follows a lime runner from landmark to landmark and an odometer counts the kilometres. Hot magenta, electric violet and lime, headings in a condensed athletic face. The demo is a fictional night run through Lisbon, the Luz Night Run.

Pace 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-3 km across works best.
  2. Run npm run map -- fetch. It downloads the area from OpenStreetMap (one request, up to a minute), builds the map’s geometry 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 night, saved as AVIF in every width the site uses. A city on the sea or an estuary is mapped only by its coastline; the map command closes it into water.
  3. Give your landmarks their latitude and longitude, your races their course (the landmarks in order, plus waypoints to steer it) and start line. Courses are found along the real streets when the site builds, and every distance is measured along them, so name your races by what they measure.

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)
Races (every course on the city, the races, teams and clubs) Pages → Races page and Content → Races
A race (the course on the plan, landmark by landmark with the kilometre, the facts, entry) Content → Races
The course (every landmark on the map, a filter by kind) Pages → Course page and Content → Landmarks
A landmark (what it is, what runners find there on race night, nearby on the map) Content → Landmarks
Pacers Pages → Pacers page and Content → Pacers
Journal Pages → Journal page and Content → Journal
Race day (the start lines, bib pick-up, getting there, the contact form, questions) Pages → Race day page
Name, city, contact, navigation Pages → Site settings
District and river names on the map Pages → The map

In headlines, words wrapped in *stars* sit on a band of lime, like the tape at the finish.

Each race has a summary, its course, a time limit, the start date and time, start waves, places, the entry fee and a note, the start line, highlights, a picture and a colour. Its page shows the course on the plan with each landmark at its kilometre. Turn on “Sell entries online” and add a Stripe Price id (or a Lemon Squeezy variant id) and the race’s button takes runners to checkout; after paying they get an email with the start line. Teams and clubs book a call with Cal.com.

Landmarks have a kind (the course page filters by it without JavaScript), a fact and what runners find there on race night (water, music, a cheer zone). Pacers have the time they hold.

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

Feature Where Service
Entries Each race’s page Stripe or Lemon Squeezy
Teams and clubs Races page, loads on click Cal.com
Newsletter (when entries open) Home and journal pages, with a welcome email Supabase or Mailchimp
Contact form Race day page; messages go to the email in Site settings Resend
AI chat Every page; answers from your races (with their courses), landmarks, pacers, race day and journal Claude or OpenAI
Search ⌘K or the search button: races, the course, pacers, journal, race day 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 course (the signature: the course draws itself on the 3D city as you scroll, the runner leads it and the odometer counts), Races, On the course (landmarks as cards), Pacers, 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 (hot magenta: buttons, pins and the course), the second (electric violet: fills and the night) and the third (lime: the finish-line band, the runner, highlights). 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 night, unless the visitor picks one with the toggle; the maps switch with it.

Pace is ready for more languages: see Languages.

A small game, Measure the course: a grid of streets, each with its length; mark out a loop from the start line and back that measures exactly the distance asked for, using no street twice (tap the corners one after another, or arrows and Enter; U undoes). The loop of that exact length with the fewest turns is found for every course, and you score 100 × the fewest turns ÷ yours. Three courses, each grid bigger. 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, races and landmarks for AI assistants, in every language.
  • Pages carry JSON-LD: the organiser as a SportsOrganization, races as SportsEvents with their entry fee, landmarks as TouristAttractions, questions as a FAQPage, posts as BlogPostings.
Terminal window
pnpm dev # local site, editor at /keystatic
pnpm map # draw your city (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).