Every race in America, in one search box.
Race information lives on five different booking platforms and none of them will show you the other four. So I pulled all of it into one index: 25,239 upcoming races, every state, rendered into more than 20,000 pages at the edge. The interesting engineering was in the merge, the render, and one silent failure mode that took months to surface.
The shape of it
Five sources that disagree
RunSignUp, UltraSignup, RaceRoster, Active.com and Athlinks. They disagree about names, dates, distances and locations, and none of them publishes a stable identifier the others recognise. Merging them is most of the work.
The weekly-series problem
Some events publish every occurrence as its own race. A summer series becomes 20 rows called "Day 1" through "Day 20," and they crowd out real races in every result set. The fix collapses a series back into a single event carrying its cadence and occurrence count, so it shows up once and still tells you it runs weekly.
Compressed records
A 25,000-race index that ships to the browser has to be small. Records use single-letter keys for the high-cardinality fields, which gets the payload down to about 299 bytes a race. Verbose JSON keys are free on your laptop and expensive over a phone connection.
Nightly, and it commits itself
The whole ingest runs overnight, writes the merged index, hands it to the site build, and commits only when the data actually changed. A pipeline that commits every night regardless produces a git history you can't read.
20,000 pages without 20,000 files
Writing a real HTML file per race would mean committing tens of thousands of files and re-uploading them nightly. Instead the race pages are rendered on demand at the edge from sharded data.
Sharded data, hashed twice
The builder splits races into 256 shards by a hash of the slug. A Cloudflare Function computes the same hash at request time, fetches only that shard, and renders the page. Which means the hash is implemented twice, in Python and in JavaScript, and the two must agree exactly. They're variants of djb2 and there's a comment in both files saying so, because that pairing is exactly the thing a future refactor breaks silently.
Static beats dynamic, on purpose
60 top races have hand-built pages with real photos and detail. But on Cloudflare Pages a Function claims the route before static files get a look in, so the dynamic renderer would have shadowed every one of them. The Function checks static assets first and steps aside if it finds one.
Dev and prod disagreed
Worse: the asset lookup behaves differently locally and in production. Wrangler's dev server applies pretty-URL mapping to asset fetches and production doesn't, so the premium pages worked on my machine and vanished once deployed. The Function now tries both forms. Classic case of a local environment being helpful enough to hide a bug.
Cached four hours
Race facts change at most once a night, so rendered pages sit in the edge cache for four hours. Cold renders are cheap enough that the worst case is still fast.
A silent truncation, and the fix
Around a dozen builders generate this site. One of them, the page
builder, rewrites sitemap.xml from scratch. Every other
builder appends to it.
Run them in the wrong order and the rewrite lands last, truncating the sitemap from roughly 500 URLs down to about 55. No error. No warning. The build succeeds and deploys, and the only symptom is a file quietly missing 90% of its contents.
The symptom surfaced in Search Console: /boston-qualifying-times, /first-marathon, /lottery-watch, /adventure-list and /race-day-checklist all sitting at "Discovered, currently not indexed". Real pages, real traffic intent, invisible because of a shell-script ordering problem.
Every builder's docstring already said "run after the page builder." Nothing enforced it. The fix was to stop relying on documentation and make one script the only sanctioned build path: fixed order, then a verification pass that checks the guides are present and the race shards are populated before anything ships.
The general principle: a pipeline step that can silently emit a smaller, correct-looking output is more dangerous than one that crashes. A crash pages you. A truncation waits for a search engine to mention it.
The open problems
Traffic is the constraint
The index is comprehensive and the technical SEO is clean. Neither of those generates visitors on their own. Distribution is the real problem and I haven't solved it.
Affiliate economics don't work alone
Race registration affiliate rates top out around $0.60 a signup. At that rate the arithmetic never reaches a real business, which means the revenue has to come from race directors and lodging instead. Good to learn from a spreadsheet before learning it from a year of effort.
Thin pages were the real SEO issue
The problem was never technical. It was programmatic pages with nothing on them, and a fifth of the index being duplicate series rows. Tuning crawl directives was never going to move it.
Live at runraceusa.com, rebuilt and redeployed nightly. Back to all build logs.