Guerrilla Public Service Project: MPB Radio Schedule

Regrets, dear reader, but I'd like to talk to you about public radio.

And mostly, when we see these things, we grumble on the inside and then do nothing. There are all sorts of reasons for our inertia. We don’t know how to fix it. It’s not ours to fix. We could get in trouble. You might notice these little design flaws for years, silently fuming, until one day.

David Weinberg

99 Percent Invisible: Guerrilla Public Service Redux

Before the pandemic, my consumption of public radio was NPR in the car on the way to the office. When that drive disappeared, NPR ended up playing an even bigger role in my routine--starting my day scrambling eggs and along to 1 A and winding down for the night with Fresh Air. The whole time, though, I was using my XM subscription, overlooking MPB, my local station. Truthfully, it wasn't until a friend said he was calling into Felder Rushing's Gestalt Gardener in the last couple of months that I even looked at their online schedule.

That schedule is my personal version of Richard Ankrom's 110 freeway sign highlighted in 99 Percent Invisible. Ankrom missed his turn on 110 in LA once because there was no sign indicating when to turn off. He was a trained sign painter, and after a few years of stewing on the inadequate signage, he decided to change add a turn-off indicator on his own. The MPB schedule never made me late for work, but that schedule has irked me every time I've used it:

  1. It's unwieldy. No matter the viewport, I find it difficult to know the time for a show the farther right in the table the column is.
  2. It shows me a lot of information I don't need. No matter what day it is, I see all of the programming for that week. So on Friday, I'm still seeing the past Monday's lineup.
  3. It isn't even linked on the MPB Radio landing page, which tells me it's likely not anyone's worry. The landing page does have an embedded player on it that tells you what's on the air by default, but nothing about what's coming up. But that player is not why we're here today.
  4. It not being linked on the landing page means I had to use Google to get to it, which meant I occasionally had to think about which search result was the right one. This is a serious inconvenience. And I will not take questions about why I didn't just bookmark the correct page.

It's just not helpful to me. I don't want to say it, but reader, I had my sign.

I wanted a version of this schedule that showed me up front what was currently on the air. Then what was coming up, then what was happening in the next days. With my hunch about the page being orphaned, and not wanting to be That Guy who demanded to know why something someone else made didn't meet my extremely specific requirements, I started thinking about what I would need to build my own.

First, I'd need a data source. There's no point in manually maintaining the station's schedule, because that could change or it could just be me verifying that, yep, it's the same this week again. Not a good use of my time. But looking at the table, there are buttons to move to older or upcoming schedules, too, which told me there was a feed of some kind somewhere. Taking the URL from the network tab in dev tools, I fiddled with the query string until I got a week's schedule in JSON. Had my data source.

Second, I'd need a way to render the JSON. You might think I'd go for a single-page application. But the relevant information changes every 30 minutes, that isn't a screaming use-case for something like Vue or even a web component at this point, in my opinion. It is, however, a definite use-case for my favorite static-site generator, Eleventy. Next!

If I'm not going to use client-side code to update the page, I'll need a way to republish the page on a schedule. This was where I fought my inner need for complexity, but do not think this is a hero's story. I looked at DigitalOcean Functions, CircleCI, and GitHub Actions. They all did more or less the same thing--read jobs written in YAML, take some inputs, run some code, push some output--but just not what I needed.

Primarily, I needed a job to run on a set schedule and push files to a static site. DO Functions are in beta, and has some limitations around S3-based hosting, so nope. CircleCI and GitHub Actions are wonderful, but their scheduling isn't what I want--Circle flat out says that a cron schedule of "every thirty minutes" will actually run twice an hour, and GitHub's documentation claims to hold to the cron schedule in your workflow, but it too cannot guarantee an exact run time. And this totally makes sense, everyone probably defaults to wanting their jobs run on the hour which expands their workload and would grind their hardware to a halt.

This left me exactly where I said I didn't want to be, staring a man named Jenkins in the face. I was reluctant to self-host Jenkins because in my heart I knew there'd be some catch, some config setting I couldn't get right, but no. I used Portainer's one-click installer, setup some DNS, and it just worked. And better than that, I was able to negotiate with its GUI to get my scheduled build happening on the half hour1.

With the build tool in place, I wrote a little deployment script or three, and my guerrilla public service project lives: What's on MPB Radio.

The thing about guerrilla public service, as 99PI talks about, is that it's often met with resistance from Actual Public Service people. In the case of Ankrom's sign, there were serious public safety concerns, but with my little web page, I'm not misdirecting people or potentially dropping large pieces of metal on to passing cars.

Just makin' stuff to make daily life a little better, hopefully.


  1. When you tell Jenkins, "Hey, run this every hour," it will suggest you rewrite it so that it runs once an hour instead, to keep the health of the server in good territory. The joy of self-hosting is being able to say "Nah, it's the only build running, maybe next time, Butler Boi." Let's see if I come to regret this decision at some point.