Skip to content

Spoke

Spoke is a site for a bike tour and bike hire shop: guided tours, bikes by the day, e-bikes, cargo bikes. The home page opens on the city from the saddle: the rider’s hands on the handlebars and the street ahead filling the window. As you scroll, one view gives way to the next, moving forward a little, and a blue cycle signpost says where you are. The tours show the real shape of their route and book online; the places along the way stand on a signpost, each sign with the kilometres and the minutes by bike from the shop. Signal red, cycle-lane blue and a sunny yellow on white, with faint dashed lane lines, headings in a heavy grotesque. The demo is a fictional shop by Nyhavn in Copenhagen, Rød Cykel.

Spoke 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 3 km across works best, with the shop 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 rideable 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 night. A city on the sea is mapped only by its coastline; the map command closes it into water.
  3. Put the shop in Pages → The map (building: its name, latitude and longitude). Give your places their latitude and longitude and your tours their route. Distances and minutes by bike are measured along the real streets when the site builds, at an easy 15 km/h, so they are never typed by hand.

Map data is © OpenStreetMap contributors, under the Open Database License. Every map shows the attribution; keep it.

The first section of the home page holds the views (frames): a picture of the street ahead, its description and the text on the signpost (e.g. “Nyhavn 1 km”), plus the handlebars, a PNG with a transparent background laid over every view. Use three to six pictures taken from the same height and angle, without a bike in them, so the handlebars sit right. Only the first view loads with the page; the others once the visitor scrolls or touches it. With reduced motion, the first view stands still.

Page Edit it in
Home Pages → Home page (a list of sections; the ride’s views are in its first section)
Tours (every route on the city, the tours with their length, duration and price, groups and events) Pages → Tours page and Content → Tours
A tour (on the plan, place by place with its minute, booking) Content → Tours
Bikes (the bikes with who rides them, price and features, booking, groups and events) Pages → Bikes page and Content → Bikes
Along the way (every place on the map, a filter by kind, the minutes from the shop) Pages → Places page and Content → Places
A place (what it is, the guides’ tip, nearby on the map) Content → Places
Journal Pages → Journal page and Content → Journal
Visit (the shop and the nearest places on the plan, directions, opening hours, the contact form, questions) Pages → Visit page
Name, city, contact, navigation Pages → Site settings
The shop and district names on the map Pages → The map

In headlines, words wrapped in *stars* sit on a band of cycle-lane blue.

Each tour has its route (places in order, plus waypoints to steer it), when it runs, how long it takes, a price and a note (e.g. “per rider, bike and helmet included”), who it is for, highlights and a picture. Each bike has its kind, who rides it, a price and a note (e.g. “a day, lock and helmet included”), features, a few words and a picture. Turn on “Book online” on a tour or a bike and add a Stripe Price id (or a Lemon Squeezy variant id), and its button takes riders to checkout; after paying they get an email and come back to the tour or the bikes page with a message. Groups and events book a call with Cal.com.

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

Feature Where Service
Booking Each tour’s page and the bikes page Stripe or Lemon Squeezy
Groups and events Tours, tour and bikes pages, loads on click Cal.com
Newsletter 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 tours, bikes, places (with their minutes by bike), visit details and journal Claude or OpenAI
Search ⌘K or the search button: tours, bikes, places, 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 ride (the signature: through the city from the saddle as you scroll), Tours, Bikes, Numbers, Along the way (the signpost), 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 (signal red: buttons, the stripe down the panels, the lane line over headings, routes and pins), the second (cycle-lane blue: the signs, the band under marked words, the route drawings) and the third (yellow: the start of a route, small 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.

Spoke is ready for more languages: see Languages.

A small game, Green wave: a street with a few traffic lights, drawn as a chart, the distance along the street across and the time going up, each light green where it is green. Pick one steady speed between 10 and 30 km/h (the slider, − and +) and ride: your ride is a straight line, and every light it meets in green counts. Each street scores 100 × your greens ÷ the most any speed can catch; three streets, each with more lights. 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, tours, bikes and places for AI assistants, in every language.
  • Pages carry JSON-LD: the shop as a BikeStore with its bikes as offers, tours as TouristTrips with their price, places 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).