Back to projects

A set-and-forget tennis calendar on Cloudflare

How a missed Grand Slam on the TV turned into a self-updating iCalendar feed built on Cloudflare Workers, Workers KV and Hono, with the date-source and timezone gotchas that make it reliable.

I wasn't planning to build anything that night. I was on the couch catching up with my mum when a Roland-Garros ad came on the TV. The French Open had already started and neither of us had a clue! Two people who genuinely love their tennis. We'd missed the opening days of a Grand Slam just because nobody told us it was on. Gutted.

The Grand Slam Calendar homepage at grandslamcalendar.com

That's when it hit me. I've built Icals before. This would be the perfect tool to ensure we never miss a slam event again! Four tournaments a year, always on my phone, always up to date, no app to install and no newsletter to skim. I work with Cloudflare a lot and I love it, so I knew almost straight away that Workers KV and Hono would let me do exactly that. Something light and fast that I could genuinely set and forget.

The shape of set and forget

The whole thing rests on splitting the system into two paths that never touch each other. A calendar client polls a subscription URL a lot 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. 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, checks them, renders the calendar once and writes the result into KV. Every visitor after that just reads the cached output. The expensive, breakable work happens a handful of times a month. The cheap read happens however often clients ask.

Why Cloudflare

This is exactly the kind of problem Cloudflare's primitives are made for. It's a big part of why I reach for them so often. 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 that's tuned for this exact shape, read heavy and write rarely. Subscribers read constantly, but the data only changes a few times a year. Hono is a tiny, web-standard router that gives the Worker clean endpoints without dragging in a whole framework. The whole thing fits comfortably in the free tier and ships as a single deployment.

I stuck to web-standard APIs the whole way through, fetch for the network and crypto.subtle for hashing. That 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's what happens when the source of truth quietly changes on you. 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 only carry the year, with no actual start or end dates, even for tournaments that had already finished. Structured, queryable and missing the one field I actually needed!

The source that worked was the English Wikipedia infobox, read through the MediaWiki API rather than scraped from raw HTML. Those articles are heavily watched, the dates get published well in advance and the date field is structured enough to parse with confidence. There are a few quirks. The US Open article needs a disambiguator and leaves the year off its date line. Next year's articles don't exist until the dates are announced. But each of those is a known case I could handle, not a nasty surprise.

Refusing to serve bad data

Unattended scraping works right up until the source changes its markup. Then it happily serves broken dates with nobody watching. The robustness here doesn't come from the parse being clever. It comes from the harness around it. Every parsed result gets validated hard. Each event has to 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's 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 means 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. The system heals itself the moment the source recovers. A health endpoint and an optional webhook alert turn a silent break into one I can actually see.

The calendar details that quietly break feeds

iCalendar is unforgiving in small ways. Lines have to be joined with CRLF endings or clients just refuse to import the file. Each event needs a stable identifier so a changed date updates the existing entry instead of creating a duplicate, plus a sequence number that only ticks up 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 parked 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're 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 completely.

The result

I was stoked with the end result! It was short and sweet. A simple and easy to read TypeScript app that did the job no fuss. It's live now at grandslamcalendar.com where you subscribe once and the four Grand Slams turn up on your calendar on the right 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, nothing to maintain. Just four tournaments that quietly show up so neither my mum nor I miss the start of another one.

The four majors on grandslamcalendar.com, the Australian Open, Roland-Garros, Wimbledon and the US Open

The full source is on GitHub if you want to see how the pieces fit together. If you're thinking of building something similar, my one bit of advice would be to lean on your data validation harder than your parser. The parse will break eventually. The harness around it is what keeps the feed trustworthy when it does. Happy coding!

Stack

TypeScriptCloudflare WorkersWorkers KVHonoCron TriggersiCalendar

Timeline

May 2026