marvin-mitterlehner.com 48.16°N 14.03°E · --:--:--
← back

Premodern Chronicles

A content pipeline for a Magic: The Gathering tournament format. A decklist is submitted from a chat window, and a fully built, search optimised deck page goes live without anyone opening an editor.

The problem

Premodern is Magic: The Gathering played with a card pool frozen at 2003: no new sets, no rotation, a closed universe. The competitive scene is a network of local tournaments with no central archive, so results scatter into chat logs and photos of paper and are unfindable a month later.

That makes it a derivation problem rather than an editorial one. The card pool is closed, decklists are structured data, prices are an API call. Nearly everything a reader wants can be computed from a decklist instead of written by hand.

Publishing is one action

A tournament result is submitted from a chat interface: deck, player, placement, event, date, and the list as plain card names and quantities. An authenticated submission service validates the payload, normalises the card names, writes the record and triggers the build.

Nobody opens an editor, touches HTML, or writes a title, a description or a link. Moments later the deck has a page, every previously unknown card in it has one too, the archive wide rankings are recounted, the link graph is rewired and the sitemap reflects it.

That single action is the point of the architecture. Everything downstream exists so publishing stays one step, no matter how large the site gets. The entire manual surface is those submitted records plus a set of hand written archetype guides.

What that one action sets off

A full rebuild, not an incremental one: the dataset fits in memory, and a deterministic rebuild beats an invalidation strategy.

Card resolution with a write-through cache. Unknown names resolve against the Scryfall API into the printing legal in the format, with type line, colour identity and marketplace links, then land in a local store. The network is touched only for genuinely new cards; every later stage reads the store, so a rebuild is offline and reproducible.

Image derivation. Artwork is fetched once and derived into full, mid, thumbnail and art crop variants, each as WebP with a JPEG fallback, so pages serve the smallest asset that fits instead of one image scaled by the browser.

Inverted index. Decklists are inverted from deck to card into card to deck. That one reverse map unlocks several page types at once: per card, every deck and archetype playing it, and across the archive an aggregate of play rate and average copies per deck, maindeck and sideboard counted separately.

Static rendering. The client is a single page application with History API routing, but every route is also emitted as a complete static document at build time. Roughly six hundred of them, readable with no hydration. The app layer is progressive enhancement over pages that already work.

Prices as a time series

A scheduled job appends the current euro and dollar price for every tracked card to a per card, append-only series stored column-wise. Movers are derived as relative change over a trailing window, which surfaces a deck gaining traction before any tournament result reflects it.

Search and machine readability

Every document carries JSON-LD: breadcrumbs, and ItemList structures on the ranked pages. Titles and descriptions are composed from each page's own data, so they contain real deck names and real counts, and cannot drift out of sync.

The sitemap does something less common: each page's content is hashed and the hash persisted between builds, so a last modified date only advances when the content actually changed. A rebuild that changes nothing reports nothing. An llms.txt is emitted alongside for AI crawlers.

The social pipeline

Instagram is a second render target on the same data, and the larger engineering surface of the two. A canvas engine composites finished graphics at 1080 by 1080 for feed posts and 1080 by 1920 for stories and reels, drawn at double resolution and downsampled.

Around twenty six post series are implemented from roughly seventy five slide templates: tournament results, decklist visualisations, matchup breakdowns, metagame snapshots, tier lists, most played and under-and-overplayed analyses, card of the month with price history, archetype deep dives, combo explainers, mulligan guides, artist spotlights, price movers and several reel formats.

The leverage is in the templates. A series is a layout plus a data binding, not a design file. Card fans are composed programmatically with per card rotation, offset and shadow. Headlines measure themselves and step the font size down until they fit the safe area. Price charts are drawn into the canvas from the same series the website uses. Story layouts reserve the lower third by construction. A post becomes a data selection, not a design task.

Design decisions worth naming

No database. The data layer is structured JSON. With a card pool unchanged since 2003 and a dataset in the hundreds of records, a database adds an operational dependency and buys nothing. Everything is diffable and versionable.

No runtime rendering. Every reachable page exists as a file before anyone asks for it. Nothing to render on demand, no cache, no cold path.

Precompute over query. Play rates, movers, budget ranges and link graphs are computed at build time, which moves all the complexity into one deterministic step that can be inspected, diffed and rerun.

Why it stays interesting

A new tournament every week means the archive is never finished, and every addition ripples through the whole derivation: new card pages, shifted rankings, moved budget ranges, a fresh set of social slides. A small system with a high multiplier, driven by a format that stopped changing more than twenty years ago.

// built with love, by hand, in Wels · no framework, no build step