Pacific Coast Bike Tour

The "LA to LA" ride: Los Altos to Los Angeles.

Colophon

How this site was built

Eight GPX tracks go in; eight day pages come out, each with an elevation profile, a map, live conditions and a list of places to stop. Everything below is either free, keyless, or running on a public token. This page exists so that anyone wondering where a number came from can go and check it.

The shape of it

It is a static site with a hand-rolled generator: two Python scripts, some Jinja templates, and one dependency. build_data.py reads the GPX tracks and writes JSON; generate_pages.py renders that JSON through the templates into plain HTML. There is no framework, no bundler, and no package.json. The published site is HTML, CSS, about 640 lines of vanilla JavaScript, and a single PHP file.

That last file is the only server-side code, and it exists for one specific reason, explained below.

Routes

Most days come from GPX tracks drawn by hand in Ride with GPS or exported from Garmin Connect. Days 3–5 were re-routed in August 2026 when a fire closed the Big Sur coast; those replacements were generated with BRouter, an open routing engine that runs on OpenStreetMap data and lets you pick a cycling profile rather than accepting whatever a car router thinks a bike wants. The original coast tracks are kept rather than deleted, in case the road reopens.

The profile choice matters more than it sounds. Routing Day 5 with a general touring profile produced a line with an unpaved section of Santa Rita Road; a road-cycling profile avoided it and found Highway 41 instead, for a third of a mile more and 280 ft less climbing.

Distance, elevation and climbs

All computed from the GPX in pure Python, no library:

Every elevation chart is drawn on the same vertical scale, so a flat day looks flat. Charts that auto-fit their own range make every day look equally mountainous, which is worse than useless for comparing them.

Maps

Leaflet with raster tiles from Mapbox. Three basemaps, because the two things this route is judged on are climbing and shade: Elevation (contours and hillshade, the default), Vegetation (satellite, where tree cover is directly visible), and Plain for reading road names.

Clicking any point hands that coordinate to Google Maps, Apple Maps or Street View. Neither Google nor Apple lets a third-party site embed an interactive map without a paid, billed key, so handing the coordinate off is the honest substitute.

Stops, water and towns

Everything in the Stops along the way lists comes from OpenStreetMap, queried through the Overpass API and written into the GPX files as waypoints so they also appear on a head unit.

Two things had to be handled carefully. A Garmin course has a capped course-point budget shared between turn cues and waypoints, and going over truncates it silently — losing turn prompts, which matter more than a café. So POIs are thinned per category, with priorities: a supermarket survives over the fast-food place next door, and water is never thinned.

And amenity=toilets says a toilet exists, not that a passing cyclist may use it. Auditing the route turned up private units, permit-only beach facilities, and six separate toilet nodes inside a college campus. Restricted access and school, church or club operators are now filtered out; customers-only stops are kept and labelled, because buying a coffee is a fair trade and a café is in practice the most reliable restroom on any of these roads.

Where OpenStreetMap simply has nothing — Los Olivos has well-known places to eat and none of them are tagged as anything the query matches — a short hand-maintained list fills the gap. Those carry a star on the day page and say so in the waypoint comment on the device, because an OSM-derived point may be stale or mistagged while a hand-picked one was chosen deliberately. The star is about where the stop came from, not about the food: nothing here has been checked on the ground.

Hand-picked stops also skip the offset screen every scraped stop passes, so one can sit further off the line than anything around it. Those print the detour under the name rather than sitting in the list looking like somewhere you ride past.

That screen is a quarter mile on most days and 1.5 mi on Days 3 and 4. A quarter mile is right where there is something every few miles — a stop further off than that is noise nobody would ride to. It is wrong where there is nothing, because it then reports an empty route as an empty valley: Day 3 runs the west side of the Salinas Valley with every town off to the east, and at a quarter mile the page showed four stops in sixty-five miles while a corridor search found shops every few miles. Widening it took the day from 10 stops to 28. It stops at 1.5 mi — a three-mile round trip — on purpose: 3 mi would close the gap on paper, but only by assuming somebody rides five and a half miles for a filling station.

Anything admitted past a quarter mile also has to be reachable without crossing US‑101, tested against the freeway's own geometry rather than guessed at from which side of the map it falls on. That is what keeps Soledad's shops off Day 3 while keeping the Mission, which sits west of the freeway even though the town does not. The test is scoped to the far stops deliberately: Day 7 rides the 101 shoulder through Gaviota, and applying it there would discard legitimate roadside stops as unreachable.

Tasting rooms and bars are listed only on Days 5 and 6, the two days built around them. Elsewhere they crowded out the water and toilets somebody is actually looking for, and on a hot inland day they are the wrong thing to steer a rider toward.

One caution about all of this, learned the hard way. The Overpass query used to keep its own list of shop and amenity tags, separate from the table that says what each tag means. The two drifted: shop=general was in the table but never fetched, so Lockwood Store and Diner — 0.03 mi off the road at mi 25 of Day 4, the only resupply in the middle of the emptiest day on the trip — simply never appeared, and the day's notes concluded there was nothing there. The query is now generated from the table, so the two cannot disagree again.

Lodging

The hotel for each night is written down once per town, since it ends one day and starts the next. Its coordinate is measured against the last point of that day's track: most days were drawn to the door, and the few that stop short say how far short, because the last half mile of a sixty-mile day is the one worth knowing about in advance.

Shade

The most involved calculation, and the one that settled an actual decision. Two mechanisms, measured separately:

Sun position comes from the NOAA solar algorithm, implemented directly; ground elevation from Open-Meteo's elevation API, which is free and needs no key.

At 3 pm in early September the sun is still about 49° up, so terrain shade comes out at zero almost everywhere — a real result, not a broken model. Sweeping the hour confirms it behaves: hold the route still and drop the sun, and terrain shade on Day 3 climbs steadily through the evening. During riding hours on these routes, though, tree cover is the only shade on offer.

Why the day pages no longer print a shade figure

They used to, and the numbers did not survive being checked. Three faults, all pushing the same way:

Under-mapping cuts the other way — OSM woodland is thin inland, so real shade can read as none — but the errors do not cancel, and the model is least trustworthy exactly where it matters most: the hot inland days. What survives is the warning on a near-shadeless day, which no error found here would overturn. A bare day is bare under every correction. build_shade.py is still in the repo, and would earn its place again for an October trip or a dawn start, when a lower sun makes terrain shade real.

Live conditions

Two feeds, fetched differently for one reason: whether a key is involved.

Weather comes straight from the National Weather Service API. No key, CORS open, so the browser can call it directly. It takes three hops: the grid point for the coordinate, that grid's forecast, then the nearest station's latest observation — the forecast payload carries no humidity.

Air quality comes from AirNow, and cannot be fetched the same way. Unlike a Mapbox public token, an AirNow key cannot be restricted to a domain, and its rate limit is enforced per key. Publish it and anyone can drain the hourly quota, after which AirNow returns nothing until the next hour — the panel would go blank, plausibly right when someone on the road is checking for smoke. So the key stays on the server behind a small PHP proxy, which caches for ten minutes, refuses coordinates outside this route so it cannot be used as a free general-purpose proxy, and serves a stale-but-labelled reading rather than nothing when the upstream is rate-limiting.

Every air quality reading names the reporting station and its distance. That is deliberate: the network is sparse inland — Monterey has no station within 75 miles — and smoke varies sharply over short distances. A bare number would imply a precision the reading does not have.

What this does not tell you

OpenStreetMap is volunteer-mapped, and thin in rural California. A day showing no water means nobody has mapped any, not that there is none; Day 4 comes back with zero mapped water across 62 miles, which is a reason to load the support car rather than a survey result. The same caveat applies to canopy: comparing two routes in the same region is far more trustworthy than any single absolute figure.

The route notes mix measured figures with rider knowledge. Distances, gradients and percentages are all traceable to the data above. Judgements about traffic and road surface are not — OpenStreetMap has no shoulder, speed limit or width tag on most of these roads — so those come from people who have ridden them, and are marked as unchecked where nobody has.

Source

All of it is on GitHub: scottgruber/pacific-coast-2026. The interesting parts, if you want to check the working:

The two algorithms worth reading up on rather than taking on trust: Ramer–Douglas–Peucker for line simplification, and solar position for the shade model — the implementation here follows NOAA’s solar calculator equations.

Credits