My Earthquake Site Said 8 Quakes in 24 Hours. There Were 3.
`I built DidItShake to answer the question people actually type after the ground moves: did it shake near me — and how often does this happen here? It's a static site generated from the USGS earthquake catalog: 150,880 e
`I built DidItShake to answer the question people actually type after the ground moves: did it shake near me — and how often does this happen here? It's a static site generated from the USGS earthquake catalog: 150,880 events in a local SQLite database, matched against 17,343 US towns, producing a page for each of the 6,124 towns that have real seismic history within 100 km — recent quakes in local time, the biggest on record since 1980, and how busy the last year has been compared with the previous decade.
The data pipeline behaved. The bugs were all in how I counted — and every one of them shipped a confident, wrong number to a page that looked completely finished. These are the four that taught me the most.
1. The frozen clock
During development I pinned the build's notion of "now" to a constant, so test builds would be reproducible:
python
NOW = datetime(2026, 11, 3, 0, 0, tzinfo=timezone.utc) # "just for testing"
You already know how this ends. The constant survived into the real build. Every windowed count on every city page — last 24 hours, 7 days, 30 days, the 90-day map, the activity rate — was computed as of midnight on the pinned day, while the "latest earthquakes" list came from the live feed. Two clocks, one page.
San Francisco's page said 8 earthquakes in the last 24 hours. The real number was 3. The 8 wasn't random: it was the count for a full UTC day two days earlier, which is exactly the kind of plausible-looking wrong answer that nobody questions.
The fix is one line (use the actual build time), but the detection is the lesson: I only caught it by picking one number off one finished page and re-deriving it from the raw source with the same filters. Which leads directly to bug number two — mine, in the checker.
2. My verification query was wrong, not the site
Re-counting San Francisco's 30-day and 365-day totals independently, I got 48 and 804. The site said 45 and 774. Tense moment — until I found the difference: 30 events in the catalog near San Francisco are quarry blasts, which USGS records alongside earthquakes with type='quarry blast'. The site correctly filtered to type='earthquake'. My "independent" recount hadn't.
A verification that doesn't carry every filter of the original query — event type, magnitude floor, radius, time window, anchor point — doesn't verify anything. It just manufactures a second, different wrong number and a false alarm. When the filters matched exactly, everything agreed to the event: 24 h = 3, 7 d = 22, 30 d = 45, 365 d = 774, and spot checks in other cities (Los Angeles: 198 decade-count events in my database, 198 in a fresh catalog query) matched too.
3. Two honest numbers can still contradict each other on one page
San Francisco's yearly chart used a rolling 10-year window. Its activity-rate baseline — "the last 12 months vs. the 2016–2025 average of 12.9 per year" — used calendar years. Both were computed correctly. But the rolling window silently ate most of 2016, so the chart's 2016 bar didn't add up to the printed average, and to any reader who checked, the site simply looked like it couldn't count.
Same family of bug, twice more: the map originally drew the top 5 historic quakes while the table listed the top 8; and one event that belonged to both "last 90 days" and "historic top 8" got painted with the recent colour, so the map showed one fewer historic marker than the table had rows.
The rule I now build by: everything on a page that comes from the same dataset is cut from one slice — one window definition, one filtered list, shared by the chart, the map, the badges and the text. Not five queries that happen to be similar.
4. So I made the build check its own arithmetic
Checking pages by eye doesn't scale to 6,124 of them, so the build now ends with an automated audit: 128 sampled cities (every large metro in the launch set plus a random spread) × 11 checks = 1,408 assertions. Page KPIs against the database, the badge rate against its printed derivation, distribution totals against the 365-day count, historic table rows against map markers, structure and JSON-LD. The hourly rebuild runs fetch → rebuild → audit → deploy, and a failed audit blocks the deploy; the old version just stays live.
The audit earned its keep on its first run: it caught the Albuquerque map/table mismatch from bug #3, which had survived two rounds of human review. (Its four other "failures" that day were bugs in my test expectations — cities with zero small quakes in a year legitimately render no distribution module. The checker needs checking too, see bug #2.)
The shape of the thing
For the curious: Python build scripts, SQLite as the only database, zero runtime queries to USGS — the catalog is sliced locally by magnitude and era (last year at M1.5+, last decade at M3.0+, historic layers back to 1976), everything pre-rendered to static HTML on Cloudflare Pages, city pages around 30 KB including their map data. Times are stored in UTC and rendered in each town's own timezone, which sounds trivial until you remember the entire point of the page is "that shake woke me at 11:38 pm", not "07:38 UTC".
Only 400 of the 6,124 city pages are in the launch sitemap. The rest exist, staged in per-state sitemaps, and get released in steps while I watch how the first batch gets crawled and indexed. Generating pages is the easy part; earning their way into the index is the actual product problem.
Data sources
- The site: https://diditshake.com/
- A city page, with the pace rate, biggest-on-record and local times: https://diditshake.com/ca/san-francisco/
- How every number is defined, including what the catalog can't tell you: https://diditshake.com/methodology/
- Original data: USGS Earthquake Catalog (ComCat): https://earthquake.usgs.gov/earthquakes/search/
If you publish numbers derived from someone else's dataset, I'd like to hear how you verify them — especially the failures. Mine all passed code review. They failed arithmetic review.
`
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.