I wasn't planning to build anything that night. I was on the couch with my mum catching up when an ad for Roland-Garros came on the TV. The French Open was on, and neither of us, two people who genuinely love our tennis, had any idea it had already started. We had missed the opening days of a Grand Slam simply because nobody told us it was happening.
The fix I wanted was boring in the best way: a calendar I could subscribe to once and then never think about again. Four tournaments a year, always on my phone, always current, with no app to install and no newsletter to skim. I work with Cloudflare a lot, and I knew almost immediately that Workers KV and Hono would let me build exactly that: something light, fast, and genuinely set-and-forget.
The shape of set and forget
The whole design rests on splitting the system into two paths that never touch each other. A calendar client polls a subscription URL often and unpredictably, so the path that answers those requests has to be cheap and boring. A separate path does the slow, fragile work of going out to the internet to figure out the actual dates, and it runs on its own schedule, not on the visitor's.
In Cloudflare terms, the serve path is the Worker's fetch handler: a subscriber hits /slams.ics and it returns a pre-rendered calendar string straight from Workers KV. No parsing, no outbound calls, just a key read at the edge. The refresh path is the Worker's scheduled handler, driven by a weekly Cron Trigger. It fetches the dates, validates them, renders the calendar once, and writes the result into KV. Every visitor after that reads the cached output. The expensive, breakable work happens four times a month; the cheap read happens however often clients ask.
Why Cloudflare
This is the kind of problem Cloudflare's primitives are made for. Workers run at the edge with no server to keep alive and no cold start to wait on. Workers KV is a globally replicated key-value store tuned for exactly this read-heavy, write-rarely shape: subscribers read constantly, but the data only changes a handful of times a year. Hono is a tiny, web-standard router that gives the Worker clean endpoints without dragging in a framework. The whole thing fits comfortably in the free tier and ships as a single deployment unit.
Sticking to web-standard APIs (fetch for the network and crypto.subtle for hashing) meant no Node compatibility flags and a smaller, faster Worker. Fewer moving parts is the entire point of something you don't want to maintain.
The data source rabbit hole
The hard part of any set-and-forget system is never the happy path; it is what happens when the source of truth quietly changes. My first instinct was Wikidata, which exposes structured start and end dates through a query endpoint. It turned out to be a dead end: the tournament edition records carry only the year, with no actual start or end dates, even for tournaments that had already finished. Structured, queryable, and missing the one field I needed.
The reliable source was the English Wikipedia infobox, read through the MediaWiki API rather than scraped from raw HTML. Those articles are heavily watched, the dates are published well in advance, and the date field is structured enough to parse confidently. There are quirks. The US Open article needs a disambiguator and omits the year from its date line, and next year's articles do not exist until the dates are announced, but each of those is a known, handled case rather than a surprise.
Refusing to serve bad data
Unattended scraping works right up until the source changes its markup, and then it serves broken dates with nobody watching. The robustness here does not come from the parse being clever; it comes from a harness around it. Every parsed result is validated hard: each event must have a sane date range, a duration in the right ballpark for a Grand Slam, and a start month in the expected window for that specific tournament. That month check is the strongest guard against silently parsing the wrong field.
If a value fails, it is never allowed to overwrite good data. The refresh merges per event: for each tournament it prefers the fresh value from Wikipedia, falls back to the last-known-good value held in KV, and falls back again to a small seed dataset bundled into the Worker. That seed guarantees the feed is never empty, even on the very first request before any refresh has run. A genuinely broken source degrades gracefully instead of corrupting the calendar, and the system heals itself the moment the source recovers. A health endpoint and an optional webhook alert turn a silent break into a visible one.
The calendar details that quietly break feeds
iCalendar is unforgiving in small ways. Lines have to be joined with CRLF endings or clients refuse to import the file. Each event needs a stable identifier so a changed date updates the existing entry instead of creating a duplicate, and a sequence number that increments only when the details actually change. I computed a content hash per event to decide when that bump is warranted.
The detail I cared about most, given my mum and I are both in front of Australian TV, was timezones. A Grand Slam is an all-day, multi-day block, so each event uses a date-only value rather than a timestamp. By the spec, a date-only value has no timezone at all. It represents the same calendar day everywhere. The Australian Open lands on the 18th of January whether you are in Sydney, London, or New York. Reaching for a midnight-UTC timestamp instead is the classic mistake that slides all-day events onto the wrong day in eastern timezones; using the right value type avoids the problem entirely.
The result
The finished thing is exactly as dull to live with as I wanted. You subscribe to one URL, and the four Grand Slams appear on your calendar on the correct days. A weekly cron keeps them current, picks up next year's dates the moment Wikipedia publishes them, and falls back safely if anything upstream breaks. No app, no account, no maintenance. Just four tournaments that quietly show up so neither my mum nor I miss the start of another one.
The full source is on GitHub if you want to see how the pieces fit together.