I run Inflation Record, a small static site that turns the US Consumer Price Index into calculators: what $100 in 1913 is worth today ($3,361.64, if you're curious), what a 2020 salary needs to be now, that kind of thing. The whole site is generated at build time from one BLS series — CPI-U, CUUR0000SA0, monthly values from January 1913 onward.
Building it was mostly uneventful. Then the data itself started doing interesting things. These are the four problems I actually had to solve, in the order I hit them.
- A month of official data simply doesn't exist There is no CPI value for October 2025. The BLS didn't publish one during the federal government shutdown, and no official value exists — not "delayed", not "estimated". Gone. My pipeline computes annual averages as the mean of the 12 monthly values. For 2025 it produced a number anyway, because code does what you tell it to: function annualAverage(months) { const present = months.filter(m => m.value != null); return present.reduce((a, m) => a + m.value, 0) / present.length; } The bug isn't the code, it's the silence. An 11-month mean (321.943 for 2025) is a legitimate statistic, but only if the page says that's what it is. The fix was mostly editorial, enforced in the build: Track monthsPresent next to every annual average. If it's under 12, the year page renders a visible note: which month is missing and why. Never interpolate. A calculator that quietly invents an official statistic is worse than one with a hole in it. The same applies in miniature to the current year: the 2026 average (332.243) covers January to September. In the by-year table it now sits there labelled "(partial year)" instead of pretending to be a finished year.
- JavaScript floats will embarrass you in production HTML My year pages ship their dataset to the browser as a JSON payload in the page. For months, anyone viewing source could read annual averages like this: 1913: 9.883333333333335 2026: 332.24255555555555 Perfectly accurate, completely unprofessional. The CPI is published to three decimal places; my averages should be too. The fix is one rounding step at serialization time in the build — not in the display layer, where it would only paint over the payload: const round3 = n => Math.round(n * 1000) / 1000; const annualData = Object.fromEntries( Object.entries(annualAverages).map(([y, v]) => [y, round3(v)]) ); Lesson I keep re-learning: whatever you serialize is a public interface, even if it's "just" view-source.
- If the answer needs JS to exist, it doesn't exist Each calculator page originally rendered its default result with JavaScript on load. Humans saw an answer; crawlers saw an empty box. For a site whose entire distribution strategy is search, that's a self-own. Now the build pre-renders the default result into the HTML — same wording, same numbers the script would produce — and the JS takes over from there when the visitor changes an input: // build time: compute the default exactly the way the page's script does const def = convert({ amount: 100, from: 1913, to: 2026 }); html = html.replace("<!--RESULT-->", renderResult(def)); The discipline this imposes is the valuable part: the static text and the script's output must agree to the cent, which means one shared calculation function and a build check that compares them.
- One series, four countries, zero frameworks The stack, for the curious: a Node build script, no framework, no database at runtime. Monthly series for the US, Canada, Australia and the UK are baked into 366 static pages (year pages, calculators, data tables), deployed to Cloudflare Pages, with an IndexNow push after each deploy so new pages get discovered quickly. Australia was the awkward one — its CPI history is three different products stitched across a century (quarterly, then a monthly "indicator", then full monthly CPI), and the build keeps those segments explicitly separate rather than smoothing them into a fake continuous line. Total moving parts: a fetch script, a build script, and the discipline to label every hole in the data instead of filling it. Data sources The site: https://inflationrecord.com/ Method, series IDs and the missing-month policy: https://inflationrecord.com/methodology/ Original data: U.S. Bureau of Labor Statistics, CPI-U (series CUUR0000SA0): https://data.bls.gov/timeseries/CUUR0000SA0 If you've built on top of government statistics, I'd genuinely like to hear how you handle revisions and gaps — the missing month can't be the strangest thing out there.
Top comments (0)