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/elsewhose 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 separatechapter_start_oddboolean 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.