BACK TO BLOG

dnd.tools: building the compendium and the experience around it

A single narrative from compendium plumbing through filters, admin ingestion, and the OpenAI-assisted creation workflow.

Note: This post was entirely written by AI. While creating dnd.tools with codex, I used an automation to update this post to reflect changes as I went. The following post is the result of that automation. Posting as an example of how this might work. Honestly, the results are pretty terrible.

The promise

I started dnd.tools with one promise: make a compendium that feels native to the browser, where every filter is shareable and every route behaves exactly as you expect when you hit Back. That meant the earliest work was less about polish and more about building a foundation I could confidently extend.

Laying the data spine

The first obstacle was data integrity. Spells and monsters needed to be typed, queryable, and consistent across the UI, so I defined the domain schemas, API routes, and query hooks, then wired up a global store with sanity tests to keep everything honest. Once that spine existed, I built a Firestore seed flow to load real compendium data so I could see how the system behaved under actual content, not mocks.

Key outcomes from this phase:

  • Queryable schemas that stay consistent across the app.

  • Seeded data that mirrors real compendium content.

  • Early tests to catch schema drift before it hit the UI.

Building the shell

With real data flowing, I moved to the UI shell. I defined semantic design tokens in global styles, wrapped the app in shared navigation, and established route-based pages for spells and monsters. That let me focus on the core UX principle: route-first, shareable state. I added transition scaffolding so the home widgets could hand off into their pages without breaking browser history or losing user intent.

Making browsing worth it

The first pass of spells and monsters pages was functional, but it quickly exposed the next problem: the UI could filter, yet the results were too shallow for real browsing. I solved that by rendering full detail cards for both spells and monsters, including structured text sections and formatting helpers, so the list itself became the primary browsing surface.

Filters as a system

Once the browsing surface felt solid, filters became the proving ground. Spells moved from a single school filter to a matrix of filter groups for casting time, range, duration, components, concentration, ritual, class, level, and source. I hardened URL parsing and legacy query compatibility so old links kept working, and I built horizontal-scroll rows for dense groups so the page could stay compact without losing breadth.

That raised a more subtle issue: as filter coverage expanded, the logic needed to be explicit. I introduced multi-select behavior for key groups, added per-group selection modes, and built a FilterLogicPopover so you can decide whether groups combine with AND or OR and how multi-select groups should match within themselves. That same logic is now shared across spells and monsters, which finally made the filter system feel deliberate rather than a pile of independent chips.

Monsters catch up

Monsters then caught up with a deeper taxonomy. I expanded their filters across size, type, alignments, senses, sources, and damage or condition immunities, then added numeric range controls for challenge rating, armor class, hit points, and speed. Those range groups are collapsible and stateful, matching the rest of the filter UI so the page stays manageable even as it grows in power.

As the monster data matured, I added clearer presentation for proficiency bonus and skills, including tighter UI so those details read like real stat block output instead of raw fields.

Home as the front door

After the filtering engine was in place, I tightened the home experience. The home widgets now expose quick filters and live search counts, and filter group expansion persists across sessions so the UI remembers what you care about. When a user jumps from a widget, the destination page can expand the right filter group and keep the interaction context intact, preserving the promise that navigation should feel seamless.

Admin ingestion and editing

That momentum surfaced a different problem: it was still too hard to add new content. I built a hidden admin ingestion flow for spells and monsters, with a structured manual form and an optional parser that turns pasted stat blocks into JSON drafts. The draft step runs through OpenAI in a constrained JSON-only mode, then normalizes ids, name tokens, and schema versioning before validating against the same write schemas the public API uses. I added dedicated admin routes (/admin/spells/new and /admin/monsters/new), draft API handlers, and a shared ingest helper so the OpenAI request stays centralized with predictable error handling. I also tightened the parser to capture source labels reliably, then codified that behavior in the ingest helper and tests so the draft output stays consistent. The creation pages let me switch between parse and manual modes, preview the payload, and get validation feedback before writing.

With that creation path working, I extended it into edit flows so existing spells and monsters can be updated without losing the same validation guarantees.

Recent refinements

These were small changes, but they tie the experience together:

  • Linked monster spell names directly to a spells view filtered by that spell, so cross-references feel instant.

  • Tweaked filter group styling to reduce visual noise and make dense rows easier to scan.

  • Polished monster skills and proficiency presentation so the card reads like a finished compendium entry.

Where it stands

The result is a working vertical slice that matches the architecture goals in AGENTS.md: route-driven UX, URL-canonical filter state, admin ingestion that respects the same schema contracts, and modular page-local logic that can become shared only when it earns it. More importantly, the app now feels like a cohesive product, not just a set of screens.

What I am doing next

Next up, I am finishing the home page widget experience so the spells and monsters cards are the primary entry point, polishing focus and filter-intent handoff during transitions, expanding monster-side filtering depth to match spells, and hardening the admin ingestion path into a full editorial workflow.