Build Log

Building in public

I've been building my own websites for 25 years — more than a dozen of them, from event sites to whatever the moment needed. These are the first I've built with AI agents (Claude Code, mostly) with me directing, and the difference isn't how fast they ship — it's what they can do: real data, real pipelines, real research. This journal tracks all of it — PainBeacon, BestRaces, Rich Shea Butter, this site, and whatever comes next. Real milestones, real numbers, and the things that broke along the way. The mistakes are a team effort.

New entries roughly twice a month. Newest first.

PainBeacon starts charging — and builds the thing worth paying for

Seven weeks since the last entry. The cadence slipped; the shipping didn't. PainBeacon spent them crossing the line every directory eventually has to cross: it stopped being only a free index of every pain practice in the country and started asking some of them for money.

Four live payment links now sit on the practices page — Enhanced at $50/month, Featured at $500/month, each with a launch offer of four months for the price of three. They were created by a script instead of four passes through the Stripe dashboard, and the amounts are read from the site's own config, so a payment link cannot charge a price the page doesn't advertise. Every link asks for practice name and NPI, which is what makes a payment identifiable. Fulfillment stays deliberately manual: money lands in Stripe and nothing changes until a human sets the tier.

PainBeacon paid listing tiers — Enhanced at $50 a month and Featured at $500 a month
Two paid tiers, four payment links, and a launch offer priced from the same config the page reads.
PainBeacon metro page for the Washington, DC area
Metro pages ignore state lines: 449 clinics within 40 miles of downtown DC, across three jurisdictions.

Shipped

  • Paid listings turned on: two tiers, four Stripe payment links, prepaid terms, and a link checker that fails the build if a button points at a retired or duplicated link
  • The Practice Kit — five front-desk forms (listing audit, Google Business Profile checklist, review protocol, new-patient phone script, insurance sheet), free but gated behind a claimed-and-verified listing. The kit is the reason to claim; the claim is what makes the listing worth selling.
  • Metro landing pages that cross state lines — one Washington, DC page holding DC, Northern Virginia and the Maryland suburbs, assigned from coordinates at build time so they can't drift from the zone names
  • The Pain Care Deserts study became infrastructure: a JSON endpoint and an embeddable choropleth, because an embed carries a link back. Publishable only because every input is federal public domain — the map's own clinic points are not, and the scripts now say so where someone extending them would look.
  • Outreach aimed at the clinics AI assistants already cite us for: Bing's AI Performance report lists the queries where Copilot used PainBeacon as a source, and a matcher sorts them into branded (a practice to contact) and category (nobody to contact) by word rarity rather than a keyword list — 4 branded out of 17 on the October 1 report, no false positives
  • Weekly articles kept shipping throughout: epidural steroid injections, sciatica, non-opioid pain management, spinal cord stimulators, and what to ask before agreeing to any procedure

What broke

  • Articles were dated ahead of their publication day, so the social automation never saw them as new. Now they're dated the day they merge.
  • The WebSub ping that tells search engines a new article exists was reporting a rejection as a success — it had been failing quietly and claiming otherwise.
  • The mobile home hero photo covered the entire hero and buried the lead card. Three passes to get the band short enough.
  • The outreach pipeline wouldn't run at all: it couldn't find the Supabase key under the name it had been given, and couldn't read a .env file written by Notepad. Both now handled.
Takeaway: the warmest outreach list we had was already sitting in Bing Webmaster Tools — the practices an AI assistant had cited us for by name. A citation isn't a backlink, but it tells you exactly who already treats you as a source, and the pitch is checkable by the recipient in ten seconds.

BestRaces: deleting races to make the rankings true

Most of this stretch on BestRaces made the index smaller. We deleted 99 listings that were never races — tournaments, meetings, camps, volunteer sign-ups, an art workshop — and kept them out with a denylist that records a reason for each one. That scan only became possible after finding out why the catalog looked the way it did: RunSignup's race search is sorted by name and stops at 5,000 results, so every sync we had ever run only saw names from the start of the alphabet to about "Fantasy". Searching state by state instead found 16,101 upcoming U.S. races.

Then the certifications. A course certified by USA Track & Field is a quarter of a race's score, and the field had been measured on 41 races out of 5,456 — both syncs created every race with the value empty and nothing ever filled it. Matching all 15,451 active USATF road courses against the eligible races produced 357 certifications, plus 365 near-misses that no rule could decide. Those were worked by hand: 50 accepted, 26 held for a policy call, 290 rejected, with 27 of them checked against the actual USATF certificate PDFs. Races carrying a composite score went from 427 to 632, and no flagship ranking moved.

BestRaces search page with distance, surface and date filters
Search grew filters — distance, surface and when — all kept in the URL, so a filtered view is a shareable link.
Race-day weather panel on a BestRaces race page
Race-day weather on 6,081 race pages, from public-domain NOAA records — naming the station and its distance.

Shipped

  • USATF certification audit and review: 357 written by rule, 365 near-misses decided one at a time, every decision recorded with its reason
  • A triathlon sanctioning audit, and the article that came out of it — USA Triathlon issues sanction numbers but publishes no list we're allowed to read, so we read all 98 race listings instead and found 16 that will say. The piece owns our own error first: two duathlons had ranked 12th and 14th in New York because a running formula scored a bike leg like a mile.
  • Race-day weather on 6,081 race pages — five years of highs, lows and rainfall at the nearest NOAA station, from GHCN-Daily because the convenient free weather APIs forbid commercial use
  • Runner ratings live on the 3,824 race pages with evidence of a past edition, with averages withheld until five confirmed ratings and every rejection path tested against production before launch
  • Search filters by distance, surface and date; lodging measured for 1,533 races; the homepage's metro and state grid collapsed into a compact near-you strip
  • 1,746 dead race URLs now redirect to their state ranking page instead of 404ing — found because Google referred a runner to a race we deleted three days after it was run. Junk we deleted on purpose keeps its 404.
  • Race-entry giveaways with official rules and a verifiable draw, a race-director share link, and the first organizer reshare: Asbury Park put our New Jersey ranking in their stories, unprompted

What broke

  • Two days of deploy-failure alerts that weren't our fault at all — Cloudflare's initialize stage failing after 28 minutes on an already-live commit, before it ever reached our code. The alarm we built in August worked; the first thing it caught was the host.
  • The first Instagram Reel's numbers were partly our own: the shares and most of the likes turned out to be self-engagement. Corrected in the log the next day. The honest read — 404 viewers, 69% of them non-followers, zero profile visits, zero follows — was more useful than the flattering one.
  • 99 non-races, then 7 more, then an art workshop and a bootcamp class: the junk kept surfacing in ones and twos until the filter moved up to ingest.
Takeaway: deleting 99 listings, rejecting 290 certification near-misses and redirecting 1,746 dead URLs doesn't look like progress on any dashboard. It is the only kind that matters for a site whose promise is every ranking, with the receipts.

BestRaces: the race map, the crawl budget, and a newsletter engine

Two weeks of heavy shipping on BestRaces. The headline feature is the race map: 4,600 races plotted nationwide, with actual course routes traced on the map — seeded from OpenStreetMap data, with race directors able to upload their own GPX files. The course line even animates itself from start to finish. Then the infrastructure caught up: a newsletter engine with a twice-monthly digest, race-week automation, and branded social graphics generated for every article.

BestRaces homepage — Every ranking, with the receipts
The promise on the tin: 4,863 U.S. races scored, methodology public, rankings never sold.
The Rundown — BestRaces news and analysis section
The Rundown: data-desk analysis articles that feed the newsletter digest.

Shipped

  • Interactive race map with OSM-seeded course traces + director GPX upload — and an animated course line
  • Newsletter engine: twice-monthly digest, race-week automation, unsubscribe infrastructure, badge-stamped category photos
  • Email capture across the site: plan-by-email, race-page signup, calendar moments, race segmentation
  • Every dated race got a downloadable .ics calendar file; every race page got a lodging panel
  • Fixed the four indexing faults that were starving the crawl budget — then documented the SEO invariants so they stay fixed

What broke

  • An email API key rotation took five commits to debug — we ended up building a temporary workflow just to prove the alert path worked, then tore it out. The fix: surface the provider's actual error instead of guessing.
  • Races at the same coordinates were hiding each other's map pins. Now they cluster.
  • Non-US races were quietly leaking into the US rankings. Barred at the filter.
  • A race with a 250-character name broke the whole build. Slugs are now capped at 200.
Takeaway: when a deploy can fail silently, the first feature you owe yourself is the alarm. We now email on any failed Cloudflare deploy — the debugging saga was worth it.

PainBeacon publishes original research: Pain Care Deserts

PainBeacon graduated from directory to publisher. The Pain Care Deserts study analyzed all 12,487 clinic locations in the federal NPI registry against every U.S. county and found that 1,906 of 3,144 counties — 61% — have no pain management clinic at all. About 38 million Americans live in one of these deserts. The study shipped with a county-level choropleth map, and the finding is exactly the kind of original data that earns citations from journalists and AI answer engines alike.

Pain Care Deserts study with county choropleth map
1,906 counties, mapped: the darker the county, the farther its residents travel for pain care.

Shipped

  • Pain Care Deserts research page with county dataset and choropleth
  • Clinic map pins now use the logo and group co-located clinics
  • Google Places backfills for hours and websites, engineered to stay inside the API free tier
  • A stock-photo registry so no two pages can ever share the same image
  • Monthly outreach report that detects real listing changes, not just first-time data fills

What broke

  • The first cut of the study counted all of Connecticut as a pain care desert. Connecticut replaced its counties with planning regions in 2022 — our county join didn't know that. Fixed, re-verified.
  • Google confirmed our Places calls were billing under an Enterprise SKU — backfills got capped at 450/run and rebalanced the same day.
  • A snapshot comparison silently matched nothing because one side stored NPI as a number and the other as text. Coerce, then compare.
Takeaway: original research is the best link-earning and AI-citation asset a directory can build — but validate your geography joins. Somewhere, a state has always changed its counties.

The AEO sprint: teaching search and answer engines who we are

Four days spent making every site legible to Google, Bing, and the AI answer engines (ChatGPT Search, Perplexity, Claude). The centerpiece is a JSON-LD entity graph on richshea.com that formally connects me to every venture — so when an AI is asked about PainBeacon or BestRaces, the founder relationship is machine-readable fact, not inference. All sites got verified in Google Search Console as domain properties, imported into Bing Webmaster Tools in one OAuth step, and had their robots.txt policies deliberately set: AI crawlers welcomed everywhere we want citations.

{
  "@type": "Person",
  "name": "Richard Shea",
  "founder": [
    { "@id": "https://painbeacon.com/#org" },
    { "@id": "https://bestraces.com/#org" },
    { "@id": "https://richsheabutter.com/#org" }
  ],
  "worksFor": { "@id": "https://candorhealthcare.com/#org" }
}

Shipped

  • JSON-LD @graph entity graph: Person + WebSite + one Organization node per venture, cross-linked
  • Google Search Console domain properties + sitemaps for richshea.com and bestraces.com
  • Bing Webmaster Tools: all seven GSC properties imported in one step (Bing powers ChatGPT Search and Copilot)
  • Reciprocal dofollow footer links between the venture sites and richshea.com — a deliberate entity signal
  • AI-crawler policy set site by site: GPTBot, ClaudeBot, PerplexityBot all explicitly allowed where we want to be cited

What broke

  • BestRaces had no sitemap at all — /sitemap.xml was serving the homepage HTML. Adding the Astro sitemap integration produced a proper index with 5,014 URLs.
  • PainBeacon's IndexNow submission job kept failing in CI: Cloudflare's Bot Fight Mode was challenging our own GitHub runner. Fix: fetch via the raw pages.dev origin, submit the canonical URLs.
  • Cloudflare's managed robots.txt was silently blocking AI crawlers on bestraces.com — the opposite of the strategy. Two dashboard toggles later, policy and reality agree.
Takeaway: check what your CDN is doing in your name. Two of our three robots.txt problems were configuration we didn't know we had.

Rich Shea Butter: a fact-first directory in a weekend — then the hard part

The fastest launch yet: richsheabutter.com went from empty repo to live site in a day — a custom static-site generator (one build.js, no framework), then a full redesign two days later into a clean, clinical comparison system. Every shea butter sold in the US, compared on origin, refinement, certifications, and verified price per ounce. No sponsored rankings. The "Made with Shea" section followed: finished products — soaps, balms, masques — organized by category with honest picks.

Rich Shea Butter homepage — Every shea butter, compared on facts
15 products tracked, 5 certified organic, verified $0.67–$2.67 per ounce — no adjectives.

Shipped

  • Custom static generator with an image pipeline and per-product data heroes
  • Full "clean-clinical" redesign with self-hosted type, price-per-ounce chart with every bar labeled
  • "Made with Shea" finished-products section with category jump-menu
  • 301 redirects for every retired product URL, plus a de-dupe guard so near-identical entries can't slip in
  • Amazon affiliate integration and Cloudflare Email Routing on the domain

What broke

  • Stale CSS kept serving after deploys — fixed with edge revalidation on HTML.
  • Products kept disappearing from Amazon. Two delisted in July, one more in August, six out-of-stock items purged and replaced. A directory is a subscription to maintenance, not a launch.
Takeaway: the site took a weekend; keeping 15 product listings honest is the real product. Scheduled ASIN spot-checks are now part of the routine.

Losing the old site, rebuilding richshea.com in ten days

This site used to live on Adobe Portfolio — until the Adobe account that hosted it was gone, and the site with it. The rebuild became the excuse to do it right: a single self-contained HTML file (no framework, no build step), deployed as a Cloudflare Worker connected straight to GitHub. Push to main, live in about a minute. The DNS zone moved from Hostway to Cloudflare along the way — carefully, since the richshea.com email had to keep flowing through Hostway's mail servers the whole time.

richshea.com homepage hero
The rebuilt richshea.com — one HTML file, deployed by git push.

Shipped

  • Single-file static site: hero, three pillars, Candor Healthcare feature, nine project cards, contact form
  • Cloudflare Worker hosting, git-connected — push to main auto-deploys
  • DNS zone migrated to Cloudflare with Hostway email preserved (unproxied mail records)
  • Contact form via Web3Forms — submissions land straight in Gmail, no backend

What broke

  • Hostway's DNS panel takes up to 4 hours to publish changes — which looks exactly like you did it wrong. Patience is a debugging tool.
  • Mid-migration we discovered the domain's auto-renew was off. The kind of thing you only find when you're actually looking.
Takeaway: losing the hosted platform was the best thing that happened to this site. Owning the file, the repo, and the DNS means no platform can take it away again.