PlotWeaver #publishing #ux #epub

Shipping EPUB at four days old — twice in one day

PlotTwist.ai is four days old and it can now hand you a book. Not a text dump — an EPUB with a cover, a navigable table of contents, accessibility metadata, and chapters that open on the correct page of a two-page spread. Getting there took two attempts, both of them today.

10:28: use a library. 20:26: don't.

The morning commit did the sensible thing: epub-gen-memory, a 326-line renderer, done. By evening the library was gone from package.json and in its place: archiver, a 163-line zip builder, and 237 lines of XML templates. EPUB export is now hand-rolled.

This wasn't purity. An EPUB is a zip file with rules, and the rules are where the quality lives: the library made the container easy and the rules inaccessible. The one everyone learns first, now enshrined in a comment:

// mimetype MUST be first entry, stored without compression
archive.append('application/epub+zip', { name: 'mimetype', store: true });

Owning the templates meant the OPF could carry proper EPUB 3 accessibility metadata (schema:accessMode, accessibilityFeature structural navigation, alt-text declarations), chapters could be wrapped in semantic <section epub:type="chapter" role="doc-chapter">, and the nav document could include the hidden landmarks nav that reading systems actually use. A library gives you an EPUB; the templates give you the EPUB you meant. Backing it all: a 1,041-line best-practices spec document committed alongside, researched before writing a line. The four-day-old app has a paper trail.

The paginator: 870 lines of book physics

Until today, "preview" meant a placeholder that printed a file path. Now there's a real book viewer: a custom paginator, not epub.js. Chapter HTML gets injected into an iframe via srcdoc and paginated with CSS multi-columns (column-width: 420px), the sub-page count measured as scrollWidth / BOOK_WIDTH. Every chapter is measured in a hidden off-screen iframe parked at left: -9999px, rendering it for real, out of sight, because the only way to know where page seven ends is to typeset pages one through seven.

The reason for all this ceremony is the spread. The old viewer treated a chapter as one page, which wrecked verso/recto pairing the moment a chapter ran long. The new model builds a flat list of physical pages and enforces book conventions:

// Artwork always goes on verso (odd/left) so chapter opens on recto (right)
if (result.length % 2 === 0) result.push({ type: 'blank', label: 'Blank' });

Deliberately inserted blank pages — the most print-brained line of code I've written this year, and the moment the preview started feeling like a book instead of a scrolling div. There's a 3D page-flip transition on top (rotateY(-180deg) over 0.6s), which is pure theatre and stays.

Settings without whiplash

The preview settings panel grew up today too: eleven trade page sizes (A-Format through Crown Quarto, defaulting to B-Format), three serif font families, drop caps, running headers, scene-break styles, and a whole strip of EPUB metadata (publisher, ISBN, copyright, twelve languages), stored as new columns on books.

Two details I'm keeping. First, the Paper theme: page background #c0bdb8 with grain generated by an inline SVG feTurbulence filter — procedural paper, no texture asset, skipped on cover pages where grain would lie about the artwork. Second, position preservation. Changing a display setting used to rebuild the preview and dump you back at page one. The fix is a fetch key that simply excludes the display-only settings:

const { page_size: _, preview_theme: _t, spread_view: _s, ...content } = settings;
return JSON.stringify(content);

Change the theme, keep your place; change the font size, re-typeset. The viewer stays mounted through refetches with a spinner overlay, so even a real rebuild doesn't feel like a teardown.

The copyright spec, or: research as a commit

Also committed today: specs/epub_copyright.md, 33 lines of notes on what belongs in front and back matter: half-title, copyright page with its "10 9 8..." edition line, dedication, "Also By". Zero implementing code. The one finding worth repeating: ebooks put the copyright page immediately after the title page, not at the back, "to prevent it being skipped in sample previews". Retailer previews show the first N pages, and publishers learned to front-load the legal furniture. The spec sits in the repo as a promissory note.

Honest ledger, day four

  • The artwork-to-chapter attachment logic runs twice: a first pass followed by a full re-scan whose comment admits the first pass gets the ordering wrong. It works; it is not proud.
  • The scene-break code contains an if/else whose two branches are identical. A decision was scaffolded and never made.
  • PDF preview is still the 17-line placeholder printing a file path. EPUB overtook PDF because EPUB was more interesting, which is not the same as more important.
  • chapter_start: 'new_page' | 'next_odd' and a separate chapter_start_odd boolean both exist. One of them is lying.
  • The 1,041-line spec calls for EPUBCheck validation; the pipeline doesn't run it yet.

The lesson from the double-shipped day: reach for the library to learn the shape of the problem, then decide whether the shape is your product. For a writing app, the book is the product — the file format where everything the writer did either survives or doesn't. Ten hours of library-assisted apprenticeship, then the templates came home.