[ Case Study ]

Paperboy

A daily news dashboard I built so I'd stop opening six apps every morning.

Shipped 2026-05 · In daily use
  • Next.js
  • React
  • Tailwind CSS
  • TypeScript
News landing — headlines, popular today, topic feedsMedia — poster galleries for movies, streaming, coming soonScores — game card box, leaders, injuriesScores - Full game detail view of NBA playoff game

[ The Problem ]

Six apps every morning. Google News for headlines, ESPN for scores, a podcast app for what dropped overnight, TMDB for what's new in theatres, a streaming service for what to watch tonight, a local outlet for what was happening near home. Each one was a tab away from the next, each one a different shape of "here's what's going on," and none of them knew what I actually cared about.

I wanted one place that pulled the actual signal from each source and laid it out in the shape my morning already has — a quick scan of headlines, then sports, then what's worth listening to or watching that night.

[ Architecture ]

Paperboy is two halves that don't talk to each other in real time.

The dashboard is built entirely on Loom, a design-system pipeline I built before Paperboy — its 55 generated atoms covering every interaction surface.

[ Decisions ]

Script-first, not server-first. Most aggregators I'd seen had a server polling, a database holding results, an API serving the dashboard. Paperboy has none of that. One script produces one JSON file; the dashboard reads the file. The interface between pipeline and frontend is the filesystem. No DevOps, no persistent state, no drift between "what the database has" and "what the user sees." If the digest is right, the dashboard is right.

Editorial sources over keyword search. Google News topic feeds, ESPN's structured RSS, podcast publisher feeds, TMDB's curated lists. No keyword search anywhere. Search noise is the easiest way to ruin a daily feed — one viral headline pollutes a topic for a week.

Graceful degradation as a baseline. Every external fetch is wrapped in Promise.allSettled with per-source isolation. When TMDB has a bad morning, news and scores still ship; entertainment degrades to empty with a warning. When ESPN goes down, you still get news and podcasts. One outage doesn't kill the day.

Team color accessibility built in. Every team color routes through an ensureContrast() utility that compares the brand color against the current theme's surface luminance and substitutes the alternate when contrast is insufficient. Light mode and dark mode both supported. The dashboard uses team brand colors aggressively — scoring breakdowns, F1 session tables, box-score row hovers — and the contrast logic keeps every one of them readable.

Audit scripts for every dataset I can't keep in my head. F1 driver rosters change every season and mid-season. Circuit timezones change as the calendar expands. So audit-f1 fetches the live ESPN season and reports drivers and circuits missing from the static maps, with ready-to-paste stub entries. Same pattern for the media bias dataset (scans past digests for coverage gaps), same pattern for endpoint health (every RSS feed, ESPN URL, and TMDB endpoint, status-checked in 1 second). When the daily digest emits a warning, there's a script that closes the loop.

[ By the numbers ]

6Apps replacedone tab every morning
80–145API calls / morningall in parallel
~3sDaily buildfetch to digest
~35Games enrichedbox scores, leaders, injuries

[ Under the Hood ]

JSON in, files out. The three slices below show the shape of the pipeline end to end — the digest artifact, the fetch wrapper that keeps it resilient, and the filter that turns raw RSS into clean signal. Pick one.

The artifact at the heart of the pipeline. One file per morning under digests/YYYY-MM-DD/ — the dashboard reads this directly, no server in between.

> digests/2026-05-26/digest.json
{
  "meta": {
    "date": "2026-05-26",
    "day_of_week": "Tuesday",
    "story_count": 142,
    "run_mode": "initial",
    "last_run": "2026-05-26T07:14:33.812Z"
  },
  "sections": {
    "popular_today": [ /* trending headlines */ ],
    "local":         { "locations": [ /* per-location story lists */ ] },
    "for_you":       [ /* topic feeds the reader follows */ ],
    "on_your_radar": [ /* topic feeds in rotation */ ],
    "scores": {
      "team_sports": { "recaps": [...], "schedule": [...], "standings": [...] },
      "ufc":         { "recaps": {...}, "schedule": {...} },
      "f1":          { "recaps": {...}, "schedule": {...} }
    },
    "entertainment": { "movies": [...], "streaming": [...], "upcoming": [...] },
    "podcasts":      [ /* episodes that dropped today */ ],
    "opinions":      [ /* opinion-section pieces */ ]
  },
  "deep_dives": []
}

[ More Case Studies ]

JAMIE

A persistent AI development partner — part JARVIS, part operating system for how I work.

[ read case study ]

Loom

I kept burning out building UI foundations, so I built a pipeline that generates them.

[ read case study ]

Party Wipe

A D&D roguelike — captures the scramble of a session going sideways, solo, without needing a DM.

[ read case study ]