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.
Your city
Section titled “Your city”The template draws the map itself, so it can be any city:
- Set the area in
map.config.json:areawith 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. - 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 insrc/data/map/and draws the base map intopublic/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. - 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.
Pages and their content
Section titled “Pages and their content”| 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*”.
Races and results
Section titled “Races and results”- 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.
Integrations
Section titled “Integrations”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 |
Sections
Section titled “Sections”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).
Colors and brand
Section titled “Colors and brand”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.
Languages
Section titled “Languages”Stride is ready for more languages: see Languages.
The 404 page
Section titled “The 404 page”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.
AI assistants and search engines
Section titled “AI assistants and search engines”AGENTS.mddescribes the project for coding assistants (Claude Code reads it throughCLAUDE.md, Cursor through.cursor/rules/), including how to draw your city and courses./llms.txtlists your pages, races, results and news for AI assistants, in every language.- Pages carry JSON-LD: the organiser as a
SportsOrganization, races asSportsEvents with their start, the questions on the race day page, and each post.
Commands
Section titled “Commands”pnpm dev # local site, editor at /keystaticpnpm map # draw your city (map.config.json), with its heightspnpm build # production buildpnpm 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).