Feature Deep-Dive Β· AFL Homepage Hero System

Twenty-four teams. Two conferences.
One hero that knows which is yours.

The AFL homepage hero is not a banner on a rotation. A calendar decides which phase the league is in, a casting rule decides which player’s face belongs on it, and β€” because this league rosters the same NFL player in both conferences β€” a third rule decides whose colours the card is painted in. That last one is the hard part, and it is the reason this page exists.

Astro SSR Β· Conference-aware casting Β· Franchise-colour accents Β· Shared composite shell Β· Light + dark
01

The Premise

Why a real player is on the hero at all β€” and the rule that governs which one

Identifiable NFL action photography is rights-managed β€” Getty, AP, Imagn. A fantasy site can’t ship it, and the usual fallback is a gradient with a headline on top.

Every NFL player, though, has a free, transparent headshot on ESPN’s CDN. Layer that cutout over a team-colour gradient with some typography and a glow and you get a branded, on-topic hero for any player, automatically, with zero licensing. The constraint became the design language β€” and the same one TheLeague uses, on the same components.

What it unlocks is the rule the whole system exists to serve:

Casting Rule β€” binding
β€œEvery hero casts a semantically relevant player β€” your keeper cornerstone in the keeper window, the best available on draft day, your starter in the first game you play. Never a random star for decoration.”
02

The Duplicate-Player Problem

The AFL rosters the same NFL player twice, which makes β€œwhose colours?” a real question

src/utils/hero-franchise-accent.ts
// Ownership is a LIST here, never one franchise.
const owners = ownersByPlayer.get(playerId);

// Scope to the viewer's own conference BEFORE picking.
// Inside one conference a player has at most one owner,
// so the ambiguity disappears instead of being broken
// by a tiebreak nobody can predict.
const mine = owners.filter(
  (id) => conferenceOf(id) === viewerConferenceId);

// Anything but exactly one β†’ his NFL team's colour,
// which is what every hero did before this existed.
if (mine.length !== 1) return fallback;

The AFL runs 24 franchises as duplicate-player conferences: the same NFL player can be rostered by a team in the American League and a team in the National League at the same time. Sixty of the AL’s eighty-four keepers are kept in the NL too. So β€œthe franchise that rosters this player” is a list, and a hero that picks one of them paints a stranger’s colours on your homepage.

The rule that resolves it is to scope ownership to the viewer’s own conference. Inside a single conference a player has at most one owner, so the ambiguity is removed rather than guessed at. An AL owner sees the AL club’s colours; the NL owner of the same player is not their story.

Everything that is not that falls back to the player’s NFL team colour β€” a signed-out visitor, a free agent, a franchise with no usable brand colour. That fallback is the behaviour every hero had before the accent existed, so the feature can only ever add.

ownership is a list, never a franchiseId ===scoped to the signed-in franchise, not ?myteam=ambiguous β†’ NFL colournear-white brand β†’ floored for contrast
03

The Image Pipeline

How a name becomes a composite β€” and the gate that keeps it from breaking

z-0
Navy or gold gradient + ghost wordmark
A diagonal gradient on the AFL’s navy, or trophy gold in the keeper window, with an oversized barely-there wordmark β€” KEEPERS, AL DRAFT, NL DRAFT β€” bled off the top edge.
z-1
Radial glow, tinted by whoever owns the story
The cast player’s NFL team primary, or your franchise’s colour when your conference rosters him. hexToRgba(tint, 0.42) in dark, 0.22 in light.
z-2
Transparent ESPN headshot cutout
Anchored bottom-right with a soft drop-shadow, loading="eager" above the fold, decorative alt="". A 404 hides it and reveals the AFL crest silhouette instead of an empty flank.
z-3
Frosted caption + editorial copy
His name on a blurred pill bottom-right β€” reading his NFL club, even when the glow is your fantasy one β€” and the phase’s copy bottom-left with explicit colours so global heading rules can’t restyle it.

Two failure modes are gated before anything renders. Transparency is load-bearing: the ESPN headshot has an alpha channel, MFL’s fallback JPG has a baked-in background that ruins the composite, so the gate requires an ESPN CDN URL β€” matched on the parsed host, never a substring, so a lookalike domain can’t slip a foreign image into an <img src>. And a team defense isn’t a person: position === β€˜DEF’ is excluded, because a "DEF player" is a logo, not a face.

04

Two Conferences, Two Drafts

The split TheLeague’s single draft hero cannot express

American League Β· live draft

The AL meets and picks in MFL’s live-draft applet, Saturday at 12:30pm PT. On draft day the CTA becomes the room itself and the draft order is demoted to a secondary link β€” before that, the order is the useful destination, because the board is empty slots until picks land.

wordmark AL DRAFT Β· buildMflLiveDraftUrl
National League Β· email draft

The NL runs a slow email draft off MFL’s email-draft page, Sunday at 9am PT β€” a page that never opens the live applet. Sending either conference to the other’s URL is the bug this split exists to prevent.

wordmark NL DRAFT Β· MFL_EMAIL_DRAFT_OPTION

Because the hero leads with the viewer’s own conference, an NL owner would otherwise have no route to the AL board while the AL is actually drafting. So a live sibling conference attaches its board as a secondary link β€” resolved after the lead event is picked, because a view builder only sees its own event and cannot know the sibling’s live state.

AL owner, AL draft day
Wordmark reads AL DRAFT, card flips to urgency red
CTA is the live draft room, not our board
Draft order demoted to a secondary link
NL owner, same day
Same event, but it is not their draft
CTA watches the AL board instead of entering a room
Their own NL card leads again the next morning
05

The Calendar Decides

Phases resolve by date; the regular season subdivides into six daily slots

Which hero shows is not a toggle β€” it is resolved from the calendar by resolveLeagueHeroState, on a priority ladder from P0 (an active event: draft day, the trade deadline, championship week) down to P5 (the default). Three phases have an identity of their own and render as composites; the rest render through the AFL’s branded promo card, which is also the fallback whenever no player can be cast. A missing feed degrades to the card that always worked, never to an empty flank.

01
new-season-starts
New AFL league year created on MFL
02
keeper-deadline
Jun 15 β†’ Jul 15 @ 8:45pm PT
03
al-draft
Saturday before Labor Day weekend, 12:30pm PT
04
nl-draft
Sunday before Labor Day weekend, 9am PT
05
season-start
NFL kickoff
06
regular-season
Kickoff β†’ end of the regular season
07
trade-deadline
Deadline day (24h override)
08
playoffs
Conference brackets
09
championship
Championship week matchup card
10
champion-crowned
Championship decided β†’ +7 days
11
default
Any gap not covered above

Phases with a shipped composite hero today

Daily slots Β· regular season
live-scoringTNF, Sunday, MNF game windows
standingsMonday pre-game
recapTuesday AM
waiver-wireTuesday PM β†’ Wednesday 8pm
articleSchefter articles
game-day-previewSaturday, Sunday pre-game
06

Casting, by Phase

Each phase has a purpose-built picker β€” all deterministic, all shared with TheLeague

FunctionPhaseWho it picks
castRosterModelKeeper windowA headliner from YOUR franchise β€” your keeper cornerstone. Guests get a league-wide daily pick.
castTopFreeAgentModelAL / NL draftThe best available by dynasty ADP, from the players nobody in the league rosters yet.
castRandomStarterModelSeason startRotates among your likely starters in the first game you play β€” not merely the week’s earliest.
castBestScoredModelRecap slotThe week’s highest actual score, with the rostering franchise named in the copy.
castRookieModelNew seasonThe newest draft class β€” rookies represent β€œnew”.
castFeaturedModelFresh What’s New entryExactly the player the entry names, and nobody when it names none.

Every picker is deterministic per Pacific-Time calendar day: the page is server-rendered, so Math.random() at request time would make two loads disagree and the cache useless. A hash of the PT day-key plus a per-hero seed indexes into the sorted candidate pool β€” stable all day, rotating at midnight PT so one face doesn’t own the window. ?testDate=YYYY-MM-DD drives both the phase and the rotation, so any day’s hero can be previewed on demand.

07

One Shell, Two Leagues

The AFL’s composites are the same components TheLeague ships

The treatment is not reimplemented here. Seven forked hero components β€” five spotlights and two panel boards, about three thousand lines under seven CSS prefixes β€” were unified into one spotlight shell and one panel board. Every dimension that genuinely differed between them became a custom property; every palette became a named accent. The AFL’s keeper and draft heroes are configurations of that shell, which is why they arrived with light mode, dark mode, mobile behaviour and a 404 fallback already correct.

The accents are named per hero rather than per colour, and that is deliberate: three of TheLeague’s five were shades of blue that differ in accent, pill and dark surface, and folding them together on the way in restyled live heroes. The AFL adds two more β€” navy and gold β€” plus the shared red urgency tone that the cut watch, the keeper deadline and a live draft all reach for.

LeagueCompositeHero Β· keepers
castRosterModel

Trophy gold, KEEPERS wordmark, your keeper cornerstone. Flips to urgency red on the deadline itself.

LeagueCompositeHero Β· AL draft
castTopFreeAgentModel

Navy, AL DRAFT wordmark, best available on the board. Red while the room is live; the CTA becomes the room itself for an AL owner.

LeagueCompositeHero Β· NL draft
castTopFreeAgentModel

The same card, one page different β€” the NL drafts by email, so its CTA opens MFL’s email-draft page rather than the live applet.

AflEventHero
every other phase

The branded promo card still renders the calendar events, the daily slot rotation and fresh What’s New entries β€” and is the fallback whenever a composite can’t cast anyone.

Honest scope
not composites

The playoff and championship heroes are a bracket and a matchup card; the trade-deadline hero is a live countdown. Those do jobs a player composite cannot, so they keep their bespoke components.

The Bottom Line

A licensing constraint turned into a rule β€” every hero means something β€” and the rule turned into a handful of small deterministic pickers plugged into a calendar that already knew what day it was. The AFL’s twist is that identity here is genuinely ambiguous: two conferences, twenty-four teams, and the same player on two rosters at once. Answering β€œwhose colours?” honestly is one filter and one fallback, and it is the difference between a hero that is about your team and one that is about somebody else’s.

It renders on the server, stays stable for a Pacific-Time day, degrades to a card that always worked when a feed is missing, and looks right in light and dark. No stock photos. No licensed action shots.

Key files
  • src/utils/league-hero/resolver.ts
  • src/utils/league-hero/casting.ts
  • src/utils/hero-franchise-accent.ts
  • src/components/shared/LeagueCompositeHero.astro
  • src/components/shared/CompositeHero.astro