Building Days Since Last

  • astro
  • typescript
  • side projects

I've been watching the NBA Finals with one eye on the series and the other on an idea that's been kicking around in my mind. Somewhere out there a Knicks fan is explaining to a group chat exactly how many days it has been since their last title. What if there were a site that just showed that number for every team across a variety of sports? A giant scoreboard counter for every championship drought, ticking up in real time. That idea turned into Days Since Last, and this post covers the decisions that went into building it.

The best API is no API

My first instinct was probably the same one you have right now, which was to go find a sports API and start comparing free tiers. Then I did the math on what the site actually needs. At the absolute minimum, a drought counter needs exactly one fact per team, the date of their last title-clinching game. That dataset changes once per year per league, and it changes on a night that everyone knows about well in advance.

Given that, there's no API. Instead, each league is a JSON file in the repo, and every date was pulled from box scores. The whole "backend" is a curated dataset, a static site generator, and a GitHub Action. There are no monthly costs and no rate limits, which is perfect for a little side project. When a champion is crowned in any of the leagues, updating the site is a simple one-line JSON edit. I cannot overstate how nice it is to maintain a "live" site that has no moving parts 🚀

Astro, because the counter is the only JavaScript

The site is almost entirely static content with exactly one interactive element, which is the ticking counter. That made the framework decision easy. Astro ships zero JavaScript by default, and the counter is a few KB of vanilla JS that computes the elapsed time from the clinching date on every tick. The number is always correct no matter when the site was last built. It's expected that most people will open these links from group chats on their phones, and that cold mobile load is exactly where shipping a tiny script instead of a framework runtime pays off.

The jumbotron

For the design I wanted an arena scoreboard, not an analytics dashboard. The counters use DSEG, a gorgeous open-source seven-segment font, with a dim ghost layer of 8s behind the digits so it reads like an LED panel with all of the segments faintly visible. Each team's page glows in its real colors. My favorite design problem in the whole project was light mode. A dozen teams have accent colors that simply vanish on a white page, like the Nets' white and the Lakers' gold. Each of those teams carries an alternate accent from its official palette that swaps in with the theme.

What is sports fandom without a little bit or a whole lot of pain and disappointment? With this in mind, I thought that it would be fun to include some tidbits of events that have happened since a team last won a championship. This helps to really drive home how long some team's droughts are at this point. Fortunately both the Red Sox and Cubs have won championships recently, otherwise the counter might not function properly 😬

Below each team's counter are the facts that I think will want to make people send the link to their friends, families, and coworkers. For example, let's take a look at the Sacramento Kings. The Kings last won a title before the Moon landing, the iPhone, and ChatGPT. Fourteen presidencies have started since then. When they last won, Michael Jordan wouldn't be born for another twelve years. None of that is hardcoded. It all falls out of one date and a small list of reference events.

Share cards with a freshness problem

Every team gets an Open Graph image rendered at build time with Satori in the same jumbotron style, so pasting a link into a chat unfurls into that team's number. Most people who see a shared link never click it, which means the unfurl is the site for them. Baking a day count into pixels also means the image goes stale at midnight. The fix is delightfully dumb. A GitHub Actions cron hits a deploy hook every night and rebuilds the whole site. The counters tick live in the browser, and the images never drift more than a day.

Trust, but verify every single date

The thing that makes or breaks a site like this is whether the numbers are right, and championship history is full of traps. Does Oklahoma City carry Seattle's 1979 title? Officially yes. Do the Vikings count their 1969 NFL Championship? They don't, because the Super Bowl decided that season, which is why Vikings fans are still waiting. Did today's Hornets exist before 2002? Officially yes, since the NBA retconned their history back to 1988 in a deal made in 2014.

I leaned on Claude Code agents for this in two passes. One set of agents built each league's dataset from box scores, and a second set was told to assume the first set made mistakes and to go find them. Across 193 teams and eight leagues, the adversarial pass found exactly one factual error. In the MLS, FC Dallas had been credited with a Supporters' Shield that D.C. United actually won. One error, in a footnote, across every clinching date in American sports history. I'll take it!

One JSON file per league

The site launched as an NBA-only board, but the architecture never assumed that. Leagues load from a glob over the data directory, so adding the NFL meant adding a file. The nav link, the league board, the team pages, and the share cards all appear on the next build. There are eight leagues on the board now, from the NFL down to the brand-new PWHL, whose Montréal Victoire lifted the Walter Cup just three weeks ago. The all-sports view surfaces the heavyweight, the Arizona Cardinals, whose last championship came in 1947. That counter has five digits 😬

As I write this, the Knicks and the Spurs are playing in the Finals, and the Hurricanes and the Golden Knights are tied in the Stanley Cup Final. Two of the longest waits on the site might reset to zero within the week. That's the thing I love most about this project. It's a static site with a built-in season finale.

Check it out at dayssincelast.app, and the code is on GitHub. If your team's counter hurts to look at, I'm sorry. The clock doesn't lie 😅