An interactive map showing cycling group rides in Boulder, Colorado. It aggregates Strava club events and displays upcoming rides by day. It also lists nearby races and events sourced from BikeReg.
- Primary site: https://boulderrides.cc
- GitHub Pages: https://rbergua.github.io/boulderrides/
- 7-day calendar — Quickly see which days have scheduled rides; the first day with rides loads automatically
- Interactive map — Routes are drawn as color-coded polylines using MapLibre GL JS with MapTiler vector tiles and an automatic fallback to Stadia Maps
- Metric/Imperial units — Bottom-left "Metric" button switches between miles/feet ↔ km/meters; updates tooltips and mobile sheets instantly (imperial by default)
- Paved vs unpaved surfaces — Routes are drawn as solid lines on paved surfaces and dashed lines on unpaved surfaces
- Start pin markers — A colored pin marks the starting location of each ride
- Desktop — Hover over a route or its start pin to highlight it and dim the others to gray, showing the club name, ride title, start time, and whether the ride is women-only; click to open the Strava event in a new tab
- Mobile — Tap a route or its start pin to see its details in a bottom sheet; tap "View on Strava" to open the event; swipe the panel down (or tap the map background) to dismiss it and restore all routes
- Auto-fit bounds — The map zooms to fit all routes for the selected day
- Races & Events — Dedicated page listing upcoming races and events
- Progressive Web App (PWA) — Installable on Android and iOS home screens for a full-screen, app-like experience with custom splash screens for all device sizes
No build tools required — this is a plain HTML/JS app. You just need a local HTTP server.
# Using Python
python -m http.server 8000Then open http://localhost:8000 in your browser.
Tip: Hard refresh (
Ctrl+Shift+Ron Windows/Linux,Cmd+Shift+Ron Mac) forces the browser to re-download all files instead of using cached versions.
Tip: To emulate a mobile device, open browser DevTools (
F12) and toggle the device toolbar.
Ride data is automatically fetched from the Strava API by a backend process that runs twice a day (12 PM & 1 AM) and keeps club_rides.json up to date. Each entry in the array represents one ride:
[
{
"club_id": 575042,
"club_name": "Rapha Boulder",
"title": "Social Ride",
"date": "2026-03-08 10:00",
"url": "https://example.com/ride",
"starting_location": [
40.016888,
-105.285529
],
"women_only": false,
"route_id": 3471700207861648642,
"distance": 24.8,
"elevation_gain": 1247,
"route": [
[
40.01962,
-105.27153,
"paved"
],
[
40.0195,
-105.27212,
"unpaved"
],
...
]
}
]| Field | Type | Description |
|---|---|---|
club_id |
number | Strava club ID |
club_name |
string | Name of the organizing club |
title |
string | Ride name |
date |
string | "YYYY-MM-DD HH:MM" in 24-hour format; displayed as 12-hour (AM/PM) in the frontend |
url |
string | Link to the Strava ride event |
women_only |
boolean | If true, shown in the description |
starting_location |
[lat, lng] |
Start marker position (optional; if not available, it uses the first point in route) |
route_id |
number | Strava route ID |
distance |
number | Route distance in miles (1 decimal); null if unavailable |
elevation_gain |
number | Total elevation gain in feet (integer); null if unavailable |
route |
[[lat, lng, surface], ...] |
Array of latitude and longitude coordinates defining the route; each point includes a surface tag ("paved" or "unpaved"), or null if the OpenStreetMap surface classification failed |
Race and event data is stored in races.json. Data is refreshed weekly from the BikeReg API (Mondays at 3 AM). Some major events not listed on BikeReg (e.g., Triple Bypass, Mt. Blue Sky Hill Climb) are hardcoded in the backend. Each entry in the array represents one race or event:
[
{
"name": "Koppenberg Road Race",
"url": "http://www.withoutlimits.co",
"reg_link": "http://www.BikeReg.com/73713",
"location": "Superior",
"latitude": 39.9527634,
"longitude": -105.1685977,
"type": ["Road Race", "American Cycling Association"],
"date": "2026-05-03"
},
{
"name": "Superville Stage Race",
"url": "http://www.withoutlimits.co",
"reg_link": "http://www.BikeReg.com/74224",
"location": "Superior",
"latitude": 39.9083424,
"longitude": -105.1699298,
"type": ["Road Race"],
"date": ["2026-05-16", "2026-05-17"]
}
]| Field | Type | Description |
|---|---|---|
name |
string | Display name of the race or event |
url |
string | Event website URL |
reg_link |
string | Registration link (typically a BikeReg URL) |
location |
string | City or town where the event takes place |
latitude |
number | Latitude of the event location |
longitude |
number | Longitude of the event location |
type |
array of strings | One or more event type tags (e.g. "Road Race", "Criterium", "Time Trial", "Mountain Bike", "Special Event") and/or sanctioning body (e.g. "American Cycling Association", "Colorado Bicycle Racing Association (CBRA)") |
date |
string or array of strings | Event date in YYYY-MM-DD format; use a plain string for single-day events, or an array of strings for multi-day events (e.g. a stage race) |
├── index.html # Main app (map + calendar)
└── club_rides.json # Ride data, auto-updated by the backend process (Strava API). The file is only committed when its contents change
└── races.json # Race and event data, auto-updated by the backend process (BikeReg API). Major events not on BikeReg are hardcoded
- Days shown — Change the
7ingenerateCalendar()to show more or fewer days. Note that a longer window of time could be misleading. Recurring weekly rides only appear once their previous occurrence has passed, so looking further ahead would result in missing entries - Default map center — Update
defaultCenterinindex.html(currently set to downtown Boulder) - Default zoom — Update
defaultZoom(currently12) - Route colors — Edit the
colorspalette array inindex.htmlto change the cycle of colors assigned to routes - Map tiles — The app loads Stadia Maps immediately (no key required) so the map is instantly visible, then silently upgrades to MapTiler (outdoor/terrain) if the key is valid and quota is available. The MapTiler API key is locked to requests from
boulderrides.cc
- MapLibre GL JS — Interactive maps with vector tile rendering
- MapTiler — Outdoor/terrain map tiles (primary)
- Stadia Maps — Outdoor/terrain vector map tiles (fallback if MapTiler quota is exceeded or unavailable). Free, no API key required
- Service Worker — Extends MapTiler tile cache from the deault 8 hours to 120 days, saving API requests and making the map load faster for returning visitors (tiles served from disk instead of the network)
- OpenStreetMap — Road surface data source used for paved/unpaved classification, queried via the Overpass API by the backend
- Strava API — Source of group ride data, fetched by the backend
- BikeReg API — Source of race and event data, fetched by the backend
- GoatCounter — Privacy-friendly analytics
If you find this project useful, you can also buy me a coffee (donate a small amount) with the link below:
