01 · Projects / The Fan Fiction Library

A reading list for a site that never had one

Archive of Our Own hosts millions of stories and gives readers one flat list of bookmarks to manage them with. The Fan Fiction Library is a web app and browser extension that lets readers save a work in one click, sort it into collections, keep private notes, and track what they’re reading — without ever leaving the archive.

Role
Sole designer & developer
Timeline
2024 — ongoing
Status
Live · public beta
Platforms
Web, Chrome, Firefox (incl. Android)
Coming soonMedia placeholder — 01Hero shot: a collection page on desktop, populated with real work cards.16:9 · 1920×1080 · PNG or WebP

02 · The problem

Bookmarks aren’t a reading system

AO3 readers routinely track hundreds of works. The archive’s own bookmarks are a single chronological list: no shelves, no reading status, no way to note why you saved something. Readers work around it with spreadsheets, notes apps, and browser tabs left open for months.

The gap isn’t storage — it’s organisation and re-entry. The design goal was a library that fits into the reading habit already in place, rather than asking anyone to move house.

  • Save without a detour. One click, from the page you’re already on.
  • Shelves, not a stack. To be read, completed, dropped, plus your own.
  • Remember the context. Private notes and reading status per work.
  • Survive the archive. Works get updated, locked, or deleted — the library has to cope.

03 · How it’s built

Two views of the same product

The same feature looks different depending on where you stand. Switch between what the reader experiences and what runs underneath.

Next.js 14 · App RouterTypeScriptReact Server ComponentsTailwind CSSRadix UIWebExtensions MV3

Rendering model

Pages are server components that fetch their own data and stay thin — around 150 lines, with logic pushed into the components below them. Client components are used only where there’s genuine interaction: dialogs, menus, the search field, the theme toggle. Mutations run through server actions, so most interactions never ship a fetch handler to the browser.

Design system

A deep navy surface with warm gold accents, Lora italic for headings and Nunito Sans for everything else. Colour and type live entirely in named Tailwind tokens — no raw hex in component code — which is what makes the in-progress light mode a change at the token layer instead of a rewrite of every component.

Coming soonMedia placeholder — 02Design system strip: colour swatches with token names, the type scale, and three work cards in different states (unread / currently reading / completed).16:7 · exported from Figma or built as a static page

The work card

The card is the unit the whole product is made of. It carries title, author, fandom, word and chapter count, completion status, the user’s private note, and a menu for everything else. It appears in search results, in collections, and in the reading list — same component, different affordances.

Interaction details that took the longest

  • Dialogs inside menus. Dialog state is owned by the parent menu, not the button, so the dialog survives the dropdown closing.
  • Portals on mobile. Dialogs render through createPortal to escape the transform containing block Radix creates, which was clipping them on small screens.
  • Optimistic saves. Saving a work updates the UI immediately and reconciles when the server action returns.
  • Empty states as invitations. A new collection explains what to put in it and links to search, rather than showing a blank grid.
Coming soonMedia placeholder — 03Mobile: collection page, cards stacked.9:16 · device frame optional
Coming soonMedia placeholder — 04Mobile: work card menu open with the note dialog on top — the portal fix in action.9:16 · device frame optional

Sharing

A work card can be turned into an image and shared straight from the phone. The card is rendered to PNG in the browser with html-to-image and handed to the Web Share API where it exists, with a download fallback where it doesn’t.

Coming soonMedia placeholder — 05An exported share card, ideally shown next to the live card it was generated from.4:3 · PNG

The extension UI

The extension injects a save button into AO3’s own markup: on work pages, on listing blurbs, and on bookmark pages. It reads the site’s visual language rather than fighting it, and the collections dropdown matches the order used on the site, so the two surfaces feel like one product.

04 · Decision log

Four things that broke, and what fixed them

Sole-developer projects don’t have code review, so the useful record is the one you keep yourself. These are the decisions that changed how the rest of the app is written.

Collections went stale after you created one

Cause
The user's collections were serialised into the NextAuth session cookie. A new collection existed in the database but not in the cookie until the session refreshed.
Fix
Collections moved out of the session entirely, into a context provider fed by a server component in the layout.
Result
One source of truth, a smaller cookie, and new collections appear instantly everywhere.

Server action errors were blank in production

Cause
Next.js scrubs thrown error messages in production builds and replaces them with a digest, so users saw a generic failure where a specific one was intended.
Fix
Handled validation errors return a typed result object instead of throwing. Throwing is reserved for genuine faults.
Result
Form errors say what’s actually wrong, and the distinction between “invalid input” and “something broke” is enforced by types.

Dialogs were clipped on phones

Cause
Dialogs opened from a dropdown were rendered inside a transformed ancestor, which becomes the containing block for fixed positioning.
Fix
Portal the dialog to the document body, and move its open state up to the menu so it isn't unmounted with the dropdown.
Result
Same component, correct behaviour on every screen size — and the pattern is now the default for anything modal.

Locked works couldn't be saved at all

Cause
Restricted AO3 works are only visible to logged-in readers, so a server-side scrape gets a login page instead of metadata.
Fix
The extension extracts metadata from the DOM the reader is already viewing and sends it to the API, bypassing the scrape.
Result
Restricted works save like any other, with no credentials handled or stored on the server.

05 · Where it stands

Shipped and in use

The site is live, the Chrome extension is published on the Chrome Web Store, and the Firefox version is approved on Mozilla Add-ons — including Firefox for Android, which makes it one of the few ways to do this from a phone at all.

Coming soonMedia placeholder — 07Short screen recording: saving a work from an AO3 listing page and it appearing in a collection on the site. The most convincing asset on the page — worth doing well.16:7 · MP4 loop, muted, autoplay · ~10s

Next

SourcesFanFiction.net alongside AO3
ThemingLight mode across site and extension
OrganisationSorting, filtering, and bulk actions
ImportBulk import from AO3 bookmarks
InsightReading wrap — a period in numbers
AutomationSmart collections driven by status drift