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. Then I broke my own sitemap and Google told me about it.
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.
I deleted my own sitemap and didn't notice
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 bill came from 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 lesson I actually took: a pipeline step that can silently produce a smaller correct-looking output is more dangerous than one that crashes. Crashes page you. Truncation waits for Google to tell you, months later.
The unflattering part
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
I assumed the problem was technical. It wasn't. It was programmatic pages with nothing on them and a fifth of the index being duplicate series rows. Fixing crawl directives was never going to help.
Live at runraceusa.com, rebuilt and redeployed nightly. Back to all build logs.