← Statpro dev log

2026-08-16

News cards got faces, and the feed learned to survive bad data

webproductdatasolo-founder

News cards got faces, and the feed learned to survive bad data

Player news cards on Statpro used to be text-only: headline, byline, timestamp, done. Functional, but flat. You could not tell at a glance who the story was about without reading the headline. This week I shipped the two changes that fix that, and they both came from the same uncomfortable realization: a news feed is only as good as its ugliest edge case.

Player photos and team badges, verified before linking

Every news card now renders a player headshot with a small team logo badge in the corner. If the headshot fails to load, you get initials. If the team logo fails, you get the three-letter code. No broken images, ever.

// Verify every news entity before publishing a link.
// Items with unverified players get no headshot, no link.
const playerVerified =
  playerId !== null &&
  verified.players.has(targetKey(item.player.id, item.league, item.player.slug));

return {
  ...item,
  player: {
    ...item.player,
    href: playerVerified ? referencePath(item.league, "players", item.player.slug) : null,
    imageUrl: playerVerified ? safeMedieUrl(headshotUrl) : null,
  },
};

The decoration runs server-side and fails closed: if the database is unreachable, you get undecorated items with no links. Better to show plain text than to link to a 404.

Quarantine instead of crash

Here is the thing that bit me. The news feed pulls from multiple sources, and some of those sources occasionally serve malformed items. A bad field in one item used to throw an exception that killed the entire page parse. So one broken report meant you lost every item on that page, including the good ones.

The fix: the parser now catches failures per-item, collects them as structured failures, and quarantines just the bad ones. The rest of the page ships normally. Each quarantined item gets recorded with its error code, field, and selector so you can see exactly what broke:

// Per-item try/catch: one bad item no longer kills the page
for (const [itemIndex, node] of itemNodes.entries()) {
  try {
    items.push(parseItem($, $(node), { ...context, itemIndex }));
  } catch (error) {
    const failure = emptyAnalysisFailure(error, $(node));
    if (!failure) throw error; // unknown errors still throw
    itemFailures.push(failure);
  }
}

If the page has zero good items, it still throws (empty pages are a real error). But if 8 of 10 items parse fine, you get those 8. The two failures go to a quarantine log with enough detail to write a fix without reproducing the input.

The pattern I keep reaching for

Both changes share a shape: verify before you decorate, quarantine before you crash. I think this is the right default for any feed that aggregates from sources you do not control. You cannot prevent malformed input, but you can decide whether one bad row takes down the whole page. The answer should almost always be no.

this is the build log of statpro · statpro.io · all entries · essays