Four Levels, 302 Categories: The Library Re-sorted
Photo: Unsplash
When you build a platform for stories, the first question you face is: how do you actually find something? A flat list of genres — fantasy, sci-fi, romance — is enough for a small collection. But what happens when the library grows? What happens when someone is specifically looking for "fan fiction about a particular anime," not "fan fiction in general"?
This week I'll explain how OutaStory's category system is built.
The problem with flat genre lists
Most writing platforms work with a simple genre list. You pick "fantasy" and maybe one or two tags. The problem: tags are unstructured. Every author tags differently. "High Fantasy," "Epic Fantasy," "High-Fantasy," and "Tolkien-like" might mean the same thing — or they might not.
We wanted something navigable. Not a search box that assumes you already know what you're looking for. Rather a structure you can click through — like a well-organized bookshelf.
The result is a self-referencing category hierarchy with four levels, 302 categories in total.
What the four levels look like
A concrete example is the fastest way to explain it:
Level 0 (root category): Fan Fiction Level 1: Anime & Manga Level 2: Shonen Level 3: Naruto
On the other side of the tree:
Level 0: Fantasy Level 1: Epic Fantasy Level 2: Dark Fantasy Level 3: Grimdark
Most stories land on level 2 or 3. Levels 0 and 1 exist for navigation — they help readers orient themselves in the library.
That also means: an author writing a Naruto fan fiction files it directly under "Naruto," not under "Fan Fiction." The parent categories are automatically pulled in for navigation.
Technical implementation: self-referencing in EF Core
In the database this is elegant: every category has a ParentCategoryId that points to another category — or null for level-0 categories. Entity Framework Core supports self-referencing relationships directly, with a few quirks around cascading deletes, which I disabled because categories need to be deletable independently of each other.
The seed system is particularly interesting: the 302 categories are upserted when the Initialization.Data service starts. New categories added to the YAML file automatically land in the database on the next boot, without any existing data being lost. Categories are never deleted — only added or renamed.
Why 21 root categories?
We didn't choose the 21 level-0 categories arbitrarily. Each one represents a genre that's relevant in the German- and English-speaking indie space:
Fantasy, Romantasy, Science Fiction, Dystopia, Urban Fantasy, Dark Fantasy, Historical Fiction, Fan Fiction, Fairy Tales & Myths, Thriller & Mystery, Horror, Adventure, Humor & Satire, Coming of Age, LGBTQ+, Superheroes, Steampunk, Cyberpunk, Space Opera, Military Fiction, Slice of Life.
Each of these 21 root categories has its own flagship story at launch — more on that next week.
Photo: Unsplash
Navigation in the UI: bookmarkable URLs across four levels
A category system is only as good as its navigation. We decided to map each level to its own URL:
/category— all root categories/category/fantasy— Fantasy and all subcategories/category/fantasy/epic-fantasy— Epic Fantasy/category/fantasy/epic-fantasy/dark-fantasy— Dark Fantasy
That means every drill-down step is bookmarkable and shareable. If someone wants to recommend "all Dark Fantasy" stories, they can just copy the link.
Technically we solve this with optional route parameters in Blazor. The category page responds to /category/{L0?}/{L1?}/{L2?} — it queries the component tree and shows the matching level. Clicking further down replaces the URL with replace: true, so the browser history doesn't fill up with intermediate steps.
What this means for authors
An author who submits her story picks a category in the publish wizard. We show her the full tree — but collapsed by default, so she isn't overwhelmed by 302 options. She navigates through the hierarchy until she finds the right level.
The chosen category is then used not just for navigation, but also as a signal for Azure AI Search — so stories filed under "Shonen" automatically get a stronger connection to other Shonen stories in search results.
Seed data and the YAML hash mechanism
I want to briefly touch on a technical decision that saved us a lot of work: the seed system for categories and stories works with hashes.
When we change a category in the YAML file, the system computes a new hash on the next startup and updates the database entry. If nothing has changed, nothing gets touched. That means we can run the initialization system on every deployment without worrying about accidentally overwriting data.
For categories that's less critical — for stories with several thousand words, it's essential.
What's next?
Next week: how the 42 flagship stories are structured, what went into writing them, and why we settled on eleven chapters per story.
