Skip to content

Stride

Stride is a site for city races: race organisers, running clubs, charity runs and cycling events. Its maps are the real city, drawn from OpenStreetMap by a command in the template, and every course follows the real streets: it is measured along the line, gets kilometre markers and an elevation profile from open elevation data. On the home page the course draws itself as you scroll, the camera following the runner while a race clock ticks. The demo is a 10K and a 5K in Lisbon. It runs from edge to edge of the window.

Stride 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). 3-5 km across fits a 10K.
  2. Run npm run map -- fetch. It downloads the area from OpenStreetMap (one request, up to a minute) and the heights from Open Topo Data (about a request a second, a few dozen requests), builds the map’s geometry in src/data/map/ and draws the base map into public/map/, by day and at night, saved as AVIF in every width the site uses (so building the site never re-encodes it). A sea or a wide river mapped as a coastline is filled in.
  3. Give each race its start and its course. The course is found along the real streets when the site builds, with its length, kilometre markers and profile.

The map’s colours are in map.config.json → colors; after changing them, npm run map -- render redraws the maps without downloading again. elevation.spacing sets how densely the heights are sampled.

Map data is © OpenStreetMap contributors, under the Open Database License; the heights come from Open Topo Data. Every map shows the OpenStreetMap attribution and every profile its elevation source; keep them. See the template’s LICENSES.md.

Page Edit it in
Home Pages → Home page (a list of sections)
Races, results, training, news, race day, 404 Pages → the page’s own entry
Map labels (districts, the river) and attributions Pages → The map
Races (course, start, entries, aid stations) Content → Races
Results (one file per race and year) Content → Results
Coaches Content → Coaches
News posts Content → News
Name, description, city, email, address, navigation, header button, profiles Pages → Site settings
Buttons, labels, chat, search, emails, game texts Content → Interface texts
Privacy, terms Pages → Legal

In headlines, words wrapped in *stars* turn to the accent on a volt bar, as in “Ten kilometres of Lisbon, *every one of them real*”.

  • A race has a start and a course: points with a latitude and longitude in the order the runners pass them, the last one being the finish. Name the points worth a chapter (they become the home page’s chapters, labels and the race page’s timeline); leave the rest unnamed to steer the course into a particular street. The site finds the way between them along the runnable streets (big roads included, steps only when there’s no other way), measures it and draws it with kilometre markers and the elevation profile. A race also has its distance as named (“10K”), date and start time, time limit, surface, price, categories, aid stations by kilometre, what’s included, a picture and a colour.
  • Entries: turn on “Sell entries with Stripe Checkout” and put the one-time Price id of one entry there; the race’s page gets a working button. Runners come back to the races page, and the Stripe webhook (/api/stripe/webhook) emails them where to pick up their bib.
  • Results: one file per race and year with every finisher (bib, name, category, club, time) in any order. Places and pace per kilometre come from the times. The results page has a finder by name, bib, club and category that works in the browser, without a server.

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

Feature Where Service
Entries Each race’s page Stripe
Call with a coach Training page, loads on click; a preview of the calendar until connected Cal.com
Newsletter, volunteers Home, news and race day pages, with a welcome email Supabase
Contact form Race day page; messages go to the email in Site settings Resend
AI chat On every page, answers from your races (with their courses and climbs), results, training, race day and news Claude
Search ⌘K or the search button: races, results, training, news, race day 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: The course (it draws itself as you scroll; pick the race and the clock’s pace), Races, What you get, Numbers, Latest results, Training, Partners, From the news, Newsletter sign-up and Closing call (a picture from edge to edge).

src/content/singletons/brand.json (Pages → Brand in Keystatic) holds three colors: the accent (volt: buttons and the course on the map), the second (signal: kilometre markers and warm fills) and the third (lane: cool fills). Text on all three is dark; each is lightened until it reads, and links in the accent’s hue are darkened by day; a color that can’t get there stops the build with a message. Brand also sets the fonts (text, headings, and the mono font for numbers and splits) and the corners.

Stride is ready for more languages: see Languages.

A small game, Negative split: ten kilometres with the hills of your first race’s real course. Before each kilometre pick Easy, Steady or Push (keys 1-3 or the buttons). Push is fast but costs energy, climbs cost more, Easy gets some back; run out and you walk. Points come from the finish time, and 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/), including how to draw your city and courses.
  • /llms.txt lists your pages, races, results and news for AI assistants, in every language.
  • Pages carry JSON-LD: the organiser as a SportsOrganization, races as SportsEvents with their start, the questions on the race day page, and each post.
Terminal window
pnpm dev # local site, editor at /keystatic
pnpm map # draw your city (map.config.json), with its heights
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).