Design: books on atproto via standard.site #
Status: architecture settled with standard.site team guidance, then
revised July 2026 for Leaflet-first interop: every text chunk carries
pub.leaflet.content (one linearDocument page per chapter; chunk = one
release for serialized books); our block types (blocks/toc,
blocks/verse) are injected into Leaflet's open page unions rather
than wrapped in our own content type. page.cavu.content and
blocks/chapterTitle are deleted. The lexicon surface is now just
lexicons/page/cavu/: book (publication extension), pages
(comic/fixed-layout content), blocks/toc, blocks/verse — codegen'd
types match. The no-backend reader (reader/, TanStack Start, paginated
- infinite scroll + two-page comic spreads) consumes the revised shapes,
and the studio (
/studio) is the only way to publish: the command-line publishers (publish-driftwood,publish-honeyguide-release,publish-release,publish-byline-book,update-cover,import-leaflet-comic) were removed in September 2026. "Implementation delta" at the end is kept as history.
Constraints #
- A book is split over multiple records. Sometimes a record holds one chapter, sometimes a subchapter, sometimes several chapters. Records are storage chunks sized for network limits, not units of meaning.
- A "chapter" is a TOC breakpoint, not a record. The start of a chapter exists at a specific point in a specific record.
- The TOC is multi-level; its leaf nodes are chapters. Inner nodes are groupings (parts, volumes, acts).
- Serialized writing and series must both be easy to subscribe to — adding chapters to a book, and adding books to a series.
- Two content formats from day one: EPUB-formatted text (a subset of EPUB XHTML) and comics (images for pages). Comics should consider multi-zoom-level images or image formats that support progressive zoom.
- Authors control layout and styling as far as possible — including odd and non-standard typesetting. Book design is part of the work.
Architecture (per standard.site team guidance) #
| Thing | Record | Notes |
|---|---|---|
| Book | site.standard.publication |
carries our extension object (spine, TOC, series link, cover) |
| Series | site.standard.publication |
plain publication; books point at it |
| Content chunk | site.standard.document |
one per release (a chunk may hold several chapters, one per page); main text in the content union; largely replicates book fields |
| Frontmatter | site.standard.document |
one per book: cover, human-readable TOC, acknowledgments — the renderable front door for generic standard.site apps |
The extension pattern #
The spine lives on the book publication as a pass-through property, not
a sidecar record. standard.site explicitly blesses additional properties
("shared baseline, not a constraint"), and the ecosystem idiom — observed on
real Leaflet and pckt.blog publications — is a bare key whose value is a
$type-tagged object from the platform's own lexicon, alongside the
standard fields kept populated as fallback (e.g. theme next to
basicTheme).
We follow it with one bundled key:
{
"$type": "site.standard.publication",
"name": "...", "url": "...", "icon": ..., // standard fields as fallback
"book": {
"$type": "page.cavu.book", // self-describing, validatable
"spine": [ // ordered chunk entries
{ "document": "at://...", "roles": ["frontmatter", "toc"] },
{ "document": "at://..." },
{ "document": "at://...", "roles": ["appendix"], "linear": false }
],
"series": { "publication": "at://...", "index": 2 }, // optional
"cover": { /* blob */ }, // portrait; same blob CID as
// frontmatter doc's coverImage
"completionStatus": "ongoing"
}
}
Spine entries are property bags, not bare AT-URIs — schema evolution
(string → object later would break every record) and EPUB precedent
(itemref attributes, manifest properties, landmarks). roles uses the
EPUB structural-semantics vocabulary (toc marks the document holding the
TOC block — readers find it with a free array scan of the record they
already fetched); linear: false (absent = true, as in EPUB) marks
supplementary material skipped in continuous reading.
- One key means one collision surface, one schema def, and
presence/absence of
bookdistinguishes book publications from series publications. $typeinside the value keeps us in the typed regime: our lexicon validates it, codegen types it, anyone can resolve the schema over DNS.- Size guardrail: the publication record (~1MB ceiling) is fetched by every subscriber and indexer. Spine entries are lean property bags (the TOC with its titles lives in a document, not here) — even thousand-chapter serials stay small.
- Blobs are CID-referenced, so the cover blob is shared with the
frontmatter document's
coverImageat no storage cost. Keep blobs within declared lexicon limits even though validation is lenient in the wild.
Spine vs TOC (constraints 1–3) #
The EPUB 3 split, exactly:
- Spine — ordered list of content-chunk entries ({document, roles, linear}) on the book publication object. Defines the continuous reading flow; chunk boundaries are invisible to the reader.
- TOC — a block type (
page.cavu.blocks.toc) inside a spine document, modeled on EPUB 3's navigation document: simultaneously machine-parseable structure and rendered content that sits at its natural place in the book (a printed TOC page). EPUB 2 kept the TOC as out-of-band metadata (NCX) and forced authors to duplicate a rendered TOC page; EPUB 3 unified them — navigation is content. The spine entry'stocrole plays EPUB'sproperties="nav"manifest flag: readers fetch publication + the toc-tagged document (two reads) for full navigation. - TOC entries are flat and
depth-annotated (flat-with-depth reconstructs any tree in one pass; no recursive lexicon refs). Leaf entries are chapters targeting (chunk AT-URI, page id) — thelinearDocumentpage's own (standard)idfield — with an optional finer block anchor; inner entries are groupings with optional targets. "Leaves must have targets" and "target pages must carry ids" are app-level validation.
Subscriptions (constraint 4) — solved natively #
site.standard.graph.subscription targets publications. Book = publication
and series = publication, so both are directly subscribable:
- New book in series → new book publication whose
book.seriespoints at the series publication. - New release → see below.
Chunk = release. standard.site documents are as much notification
units as storage units: subscribers should get exactly one new-document
event per release. So a serialized book releasing chapters in pairs puts
the pair in one document — chapters are pages inside it, not documents
of their own. The release ritual is one atomic
com.atproto.repo.applyWrites batch, three writes:
- create the release document (
pub.leaflet.content; onelinearDocumentpage per chapter, pages carrying ids likech-5;pages[0]is the release nav page with ablocks.pagecard per chapter when the release holds more than one). - put the frontmatter/TOC document with a new leaf entry per chapter
targeting
(release doc, page id)(plus a grouping entry if the release is a named part). - put the book publication with one new spine entry (and
completionStatusflipped on the final release).
Atomicity means no window where the spine points at missing documents or
new chapters are unreachable from navigation. Release-document title
metadata covers the whole release ("The Second Tide & Undertow —
Novella"); followers get one notification, and the document renders
natively in anything leaflet-aware.
Reading progress (page.cavu.progress) #
The reader-side complement to subscriptions: the logged-in home page
lists the user's site.standard.graph.subscription records, joined with
their reading progress, showing progress-vs-content ("8 of 10 chapters,
2 new since you last read"). Progress is a record in the reader's own
repo (the BookHive-shelf pattern: activity lives with the actor), one
per book, updated in place:
- Derived rkey.
key: any; the rkey is the book publication's AT-URI withat://stripped and/replaced by~(all legal rkey characters, reversible). Updates are idempotent putRecords with no read-before-write; one book's progress is a single getRecord; the whole shelf is one listRecords on the collection. - Position grammar, coarse to fine:
document(spine doc AT-URI) →page(page id — same field for linearDocument and comic pages) → optionalblockindex path + byteoffsetmirroringpub.leaflet.pages.linearDocument#position(Leaflet's own quote grammar). Fine fields are edit-fragile by nature; readers resolve what still exists and clamp the rest. positionvsfurthest: position is the resume point, last-writer-wins across devices; furthest never regresses (clients max-merge), covering re-reads and stale-device writes.- Denormalized
percent(integer 0–100, body-matter weighted — thebookProgresslogic inbook.ts), computed by the writing client, which has the book loaded. The home page renders progress bars from progress records + publication records alone; "N new chapters" = linear-spine entries afterposition.document— no spine-document fetches on the home page at all. startedAt/finishedAt/updatedAt— shelf semantics (finishedAt marks finished regardless of percent; updatedAt doubles as last-read-at for a "continue reading" sort).- Public, therefore opt-in. Repo records are world-readable; writing progress must be a visible user choice, not ambient telemetry. Readers who decline keep progress in local storage only.
- Why not
buzz.bookhive.bookas the progress record (checked before settling):hiveIdis required and Cavu books aren't in the hive catalog;key: tidmeans list-and-match lookups, read-modify-write updates, and cross-device duplicate races;bookIdentifiershas no AT-URI field, so the record can't even name our book without a pass-through extension; andbookProgress(percent/currentPage/currentChapter) is display data with no resume point. It also carries the user's review and stars — rewriting it at reading cadence races their app's edits. The seams do line up for mirroring (their percent/startedAt/finishedAt/status map losslessly from our fields), so: page.cavu.progress is source of truth at reading cadence; mirror into buzz.bookhive.book at milestone cadence (start, chapter boundaries, finish) once the book has a hiveId.
Content formats (constraint 5) #
Chunks are site.standard.document records; the format lives in the
document's content open union as our $type-tagged objects, mixable
within one spine:
- Text:
pub.leaflet.content— Leaflet's own wrapper, so every text document (frontmatter included) renders natively in leaflet-aware apps (see "Text content format" below); book-only blocks ride the open block unions.textContentgives indexers/previews plaintext. Target chunk size ~100–300KB. - Comics:
page.cavu.pages— pre-paginated image content, the fixed-layout counterpart to apub.leaflet.contenttext chunk (see "Comic content format" below). Zoom options:- Renditions per page — blobs at multiple widths. Simple, works everywhere; start here.
- Progressive formats — JPEG XL / progressive JPEG decode low-res from a byte prefix; needs range-request support on blob endpoints (not guaranteed).
- Tiling (deep-zoom / IIIF-style) — best zoom UX, most complexity.
Chunks-as-documents also restores per-chapter interop: timeline previews,
per-chunk bskyPostRef comment threads, and per-chunk labels.
Text content format (Leaflet-first) #
The ecosystem convention (confirmed on live Leaflet and pckt.blog records)
is that each platform puts its own $type-tagged content wrapper in
site.standard.document.content. We deliberately don't follow it for
body text: a page.cavu-only wrapper would make every chapter
unrenderable outside our reader. Body chunks carry pub.leaflet.content
itself, so any leaflet-aware app renders them natively and our reader
renders them with the same block components.
Structure conventions inside a body chunk:
- Chapter = page. The chunk's
pagesarray holds onepub.leaflet.pages.linearDocumentper chapter. The page's own (standard, optional)idfield is the chapter anchor — we require it by app-level rule (ch-5-style, human-legible). Page boundaries give our reader chapter boundaries with zero nonstandard vocabulary. A chunk never holds a partial chapter; an oversized chapter splits into more spine chunks, never intoblobPages(which we honor when consuming leaflet content — the lexicon says consumers MUST — but never emit). - Reachability in Leaflet: the release nav page. Leaflet renders
pages[0]and reaches other pages only throughpub.leaflet.blocks.pageportal blocks (Notion-style tree, no sequential next/prev — confirmed in theirPostPages.tsx). So a multi-chapter chunk's first page is a short release landing page — the release title and oneblocks.pagecard per chapter — and each chapter page is a portal target. Single-chapter chunks skip this; the chapter itself ispages[0]. Rule for our reader: pages targeted by TOC leaf entries are chapters in the reading flow; untargeted pages are auxiliary (nav) and skipped — our book TOC supersedes them. - Chapter title is content: the first block of a chapter page is a
pub.leaflet.blocks.headerwith the chapter title as rendered in the book. Chapter-opening treatment (fresh page in paginated flows, the chapter drop, generous separation in continuous flows) is TOC-driven: our reader applies it to pages targeted by TOC leaf entries. The formerpage.cavu.blocks.chapterTitleblock is dropped — a page boundary plus a TOC target carries the same information in standard vocabulary. Degradation in foreign renderers: an ordinary heading, which is fine. Ordinary in-chapter headings are alsoheader; the TOC, not the heading level, distinguishes them. - Book blocks ride the open union.
linearDocument's block union is open (noclosed: true; the lexicon spec makes unions open by default), and Leaflet's published-view renderer confirms the skip behavior: its block dispatch ends indefault: return null(PostContent.tsx). Sopage.cavu.blocks.verse(andtoc, below) sit inside leaflet pages; foreign renderers show a silent hole (a chapter minus its lullaby, no indication) — accepted because a mostly-readable chapter beats an unrenderable one. Keep the document'stextContentcomplete so skipped words still exist in previews and search. Corollary: never round-trip our documents through Leaflet's editor, which would drop unknown block types on save. - Sub-page anchors: standard
#idfacets. Leaflet's richtext facet lexicon includes an#idfeature ("used for linking to a segment") — an inline anchor on a byte range, in standard vocabulary. Finer-than- page targets (deep links, the futurepage.cavu.positionreading-position record) use (document, page id, facet id) — no nonstandard pass-through block ids needed. Not load-bearing today: chapters, the only current anchor consumers, are whole pages.
The frontmatter/TOC document is leaflet content too —
page.cavu.content is deleted; there is no page.cavu text wrapper left.
Its single linearDocument page is:
- a level-1
header— the book title (title page, not a chapter); - the injected
page.cavu.blocks.tocblock — flat depth-annotated entries targeting (document, page id[, facet anchor]) — which our reader parses for navigation UI and renders as the printed TOC page.
The TOC is deliberately a cavu-only structure with no in-page fallback:
Leaflet skips the block and shows a plain title page, which is fine —
leaflet-aware apps already navigate the publication through their own
chrome (chronological post feed, postsList, prev/next), so a
hand-maintained card list would just duplicate that. The document's
textContent carries a plaintext TOC and description a real blurb
for preview cards.
Should a work someday need blocks whose omission would misrepresent it beyond tolerance, the answer is a new content type added to the spine's open content union — additive, not a revival of the wrapper.
Book-only blocks, now three:
page.cavu.blocks.verse—plaintextwith significant newlines (\n= line break,\n\n= stanza break) + the same Leaflet facets. Mirrorspub.leaflet.blocks.text's shape so rich-text tooling carries over. Renderers keep stanzas together and style verse distinctly. (Survey basis: The Driftwood Library, 8 chapters, ~18k words — prose, quoted embedded tales, dialogue, and one lullaby in quatrains; verse was the only structure with no Leaflet block and no faithful markdown representation.)page.cavu.blocks.toc— the table of contents as content, per EPUB 3's navigation-document unification.page.cavu.blocks.screenplay— one screenplay element (scene heading, action, character, parenthetical, dialogue, lyric, transition, centered) asplaintext+ facets, with an optionalsceneNumberand adualflag for dual dialogue. A script is a flat run of these — the paragraph-type model of Fountain and Final Draft — inside ordinary chapter pages, one page per act. See "Screenplays" below.
Title vs heading convention: a chunk document's title is metadata
— self-contained for browser tabs, previews, feed cards, and subscription
notifications, covering the whole release ("The Second Tide & Undertow —
The Driftwood Library"). Chapter titles are content: header blocks
opening each chapter page. The frontmatter document opens with a level-1
header of the book title (title page, not a chapter).
Importer note: markdown sources don't mark verse explicitly — detection is heuristic (runs of short hard-broken lines) or author-annotated.
Screenplays #
A book whose page.cavu.book form is screenplay is a script, set to
the US industry standard: Courier Prime throughout, the standard element
indents (cue 2.2in in, parenthetical 1.6in, dialogue 1in and 3.5in wide,
transitions flush right), and in PDF the Letter script page exactly —
54 lines, scene numbers in both margins, page numbers top right,
(MORE)/(CONT'D) where a speech crosses a page. Screen and EPUB keep
the indents but fit them to the reading column. The source format is
Fountain; # sections become chapters (acts).
form sits beside styling rather than in it: it changes what the
importer reads and how every renderer sets the body, not a preference.
Under it, the faces, paragraph style, justification, chapter drop and
initial are fixed by convention; only page size and chapter break remain
the author's.
This deliberately stretches the tolerance rule above. A screenplay
rendered by a foreign Leaflet reader is a silent hole — act titles and
nothing else — which is past "mostly readable". The rule's answer would
be a new content type, but that buys nothing here: no foreign renderer
can set a script either way, and a new type would duplicate every
chapter-shaped path (TOC anchors, the reading route, bookState, exports,
KOReader XPointer mapping, highlights, the studio block editor) for no
gain in interop. textContent stays complete, so previews and search
still see the whole script.
Leaflet's pub.leaflet.blocks.html was considered and rejected as the
storage: it renders author HTML in a sandboxed iframe of fixed height
(16–1600px), which clips a scene, can't flow through paginated columns,
is opaque to highlights and KOReader positions, ignores the reader's
theme, and gives the PDF nothing to break (MORE)/(CONT'D) from. It
remains a possible additive Leaflet fallback — a per-scene HTML copy
the Cavu reader skips — without touching records already published.
Comic content format #
page.cavu.pages is the pre-paginated counterpart to a
pub.leaflet.content text chunk,
modeled on EPUB fixed-layout: an ordered array of page objects, each one
image blob + aspectRatio (reusing pub.leaflet.blocks.image#aspectRatio)
alt(ideally a panel/dialogue transcript) + optionalid+ optionalspreadhint. Same chunking rules as text — a chunk is usually a chapter or issue, boundaries invisible to the reader — and both content types mix freely in one spine (a prose foreword before comic chapters).
Survey of a real Leaflet comic (aavia-comic.leaflet.pub, 18 pages) found
one site.standard.document per page with a single
pub.leaflet.blocks.image block (~2.5MB webp, 2480×3508) — fine for a
feed-style site, but no spine order, no spreads, no reading direction.
Importing that shape means converting it into chapter-sized
page.cavu.pages chunks (grouping "Prologue p.N"-style titles into
chapters, re-uploading blobs); a one-off script once did this, and a
studio comic import would have to do the same.
Two-page spreads. Print-comic conventions, EPUB's vocabulary:
pageProgression: ltr | rtlonpage.cavu.book(EPUB's spine-levelpage-progression-direction; absent = ltr,rtlfor manga) decides which slot of a spread comes first in reading order and which way pages turn.- Per-page
spread: left | right | centermirrors EPUB'spage-spread-*properties.left/rightpin a page to a physical side — renderers insert a blank slot when the natural flow disagrees, exactly as print does (e.g.righton page 1 makes it open alone like a proper recto);centergives a page the whole spread. Absent means pages fill spreads in reading order. Pages wider than tall are center regardless — a double-page spread drawn as one image.
Anchor grammar (resolves the former open question): the page id is
the anchor — TOC entries and intra-book links target
(document AT-URI, page id), the same grammar as linearDocument page
ids in text chunks. One grammar for both content types.
Zoom, deferred: single full-resolution blob per page for now
(maxSize 20MB; the wild runs ~2.5MB/page). The lexicon can grow an
optional per-page renditions array additively when needed.
Ecosystem: byline.pub and BookHive #
byline.pub publishes CC0, venue-neutral pub.byline.* lexicons (vendored
in lexicons/pub/byline/) for the catalog layer: book is
author-signed listing metadata — title/subtitle/tagline, tiered
descriptions, cover + coverAlt, genres/tags, publication status
(draft/forthcoming/published/outOfPrint as token defs), editions (format,
ISBN-10/13, retailer links, audiobook duration), external identifiers
(ASIN, Goodreads, and hiveBookUri — an AT-URI into BookHive's
buzz.bookhive.catalogBook, revealing a third ecosystem player for reader
tracking/reviews), plus series, profile, and comment (reader comments
in the commenter's own repo, strongRef to the book, threaded via parent).
No content anywhere — no spine, no text. Byline answers "what is this book and where do you get it"; we answer "here is the book, read it." The integration seam:
- Don't duplicate the catalog. page.cavu.book stays a reading-format object; editions/retailers/genres belong in a companion pub.byline.book record. A Cavu book can be its own byline listing where the "edition" is natively readable on atproto.
- Correlate, both directions. Their
identifierspattern (hiveBookUri) is the template: an identifier/link pointing at our book publication is the natural ask to byline; alistingAT-URI on page.cavu.book pointing at the pub.byline.book record is ours. - Validation: their series design matches ours exactly ("Book points
at the series; the series record never points back"), with two ideas
worth stealing: denormalized series
nameon the membership object (render without a fetch) and theircontributordef (name required, DID optional — friendlier than site.standard's DID-required contributor for crediting off-protocol co-authors/narrators). - Social layer for free: pub.byline.comment (book-level, threaded) +
standard.site's per-chunk
bskyPostRefcover book and chapter discussion without us minting a comment lexicon.
Adopted into page.cavu.book (baked-in byline dependency, deliberate):
contributors reuses pub.byline.defs#contributor; listing AT-URI
points at the companion pub.byline.book catalog record; coverAlt added;
seriesRef.name denormalized.
BookHive (buzz.bookhive.*, vendored in lexicons/buzz/bookhive/) is
the reader-shelf layer: book is a record in the reader's repo —
status tokens (wantToRead/reading/finished/abandoned), 1–10 stars, review
text, started/finished dates, and a bookProgress object (percent /
currentPage / currentChapter) — correlated to the catalog via
bookIdentifiers and hiveBookUri. buzz is their threaded comment
record; catalogBook is their scraped canonical catalog (in the
bookhive.buzz repo, keyed by hive id). Implications for our reader:
- Write
buzz.bookhive.bookrecords on the user's behalf (status, stars, review, coarse progress) so Cavu reading activity shows up on BookHive shelves — interop instead of a competing shelf lexicon. - Their
bookProgressis page-granularity; our fine-grained record ispage.cavu.progress(see "Reading progress"), with coarse sync mirrored into bookProgress. - Getting Cavu books into their catalog (so readers can shelve them) goes through their hive-id pipeline — same conversation as the byline identifier ask.
Author styling (constraint 6) #
Semantics are the floor; styling is progressive enhancement. Records must render legibly with zero author styling (other apps, screen readers, reader-mode); above that floor, the author owns presentation in escalating layers:
- Book theme —
page.cavu.theme-style object on the publication (thetheme/basicThemepattern): typography with embedded font blobs (EPUB's book-identity feature), palette, page background, paragraph style (indent vs spaced), justification, chapter-drop depth, scene-break ornament. Covers the design identity of most books entirely. - Block attributes — declarative, enumerable hints on the block wrapper: alignment (Leaflet precedent), inset, size variant. Safely ignorable by simple renderers.
- Author stylesheet — a CSS blob on the publication + a
classhook on block wrappers. The "odd typesetting" unlock. Safer for us than publisher CSS ever was for EPUB reading systems: atproto structurally forbids external fetches (fonts/images are blobs), and our reader renders into a container we control — sanitize with a property allowlist and let authors go wild. - Freeform layout content types — for concrete poetry and
House-of-Leaves pages: a positioned-canvas content type
(
pub.leaflet.pages.canvasis precedent) or SVG pages withtextContentfallback, mixable into the spine chapter by chapter via the open content union.
Reader-side policy, exactly one: reader-mode always available — user accessibility overrides author art on demand (EPUB's "publisher defaults" toggle, done deliberately). Author styling is on by default.
Presentation defaults documented in block descriptions (e.g. the chapter drop) are what the absence of author styling produces — the floor, not a ceiling.
URL resolution & hosting (standard.site verification) #
How https URLs and records point at each other, per the standard.site verification spec and Bluesky's shipped integration (researched July 2026):
- Documents: per-page
<link rel="site.standard.document" href="at://…">in the page head. Fully path-scoped. There is also arel="site.standard.publication"link tag, but the spec marks it "a hint only — always confirm via the .well-known endpoint". The reader emits both. - Publications:
/.well-known/site.standard.publicationreturns the publication's AT-URI as plaintext — and it is path-capable ("Non-root Publications"): a publication whoseurlishttps://example.com/books/fooverifies athttps://example.com/.well-known/site.standard.publication/books/foo. Live precedent: atproto.com/blog. The publicationurlfield isformat: uri; nothing forbids a path, and Leaflet's own resolver longest-prefix-matches path-based publication urls. - So: no subdomain-per-book. One reader domain can host many books at
paths. Leaflet mints a subdomain per publication, but that's their
product choice (and matches atproto's strictly host-scoped identity
layers — handles and did:web reject paths), not a spec requirement.
Caveat: non-root support is newer spec text, and Bluesky's appview
keeps a hardcoded subpath-friendly domain allowlist
(
packages/bsky/src/util/standard-site.ts) for document-URL validation — serve both the path-suffixed well-known and the hint link tags for maximum indexer compatibility. - Verification is bidirectional and consumer-side (indexers like
docs.surf, Bluesky's cardyb link-card service): record's
url+pathmust equal the page URL, and the well-known must return the record's AT-URI. Consequence for a shared reader domain: only affirm publications whose recordurlactually points at us — echoing an AT-URI for every path would falsely claim every book on the network. Known race: records published before the site deploys need indexer re-verification.
Open questions #
- Series ordering — is
indexon the book→series link enough, or does the series publication want its own ordered list (and what does a series "TOC" look like for generic apps)? - Version pinning / editions — spine references are floating AT-URIs; strongRefs would pin immutable editions at the cost of an update fan-out on every typo fix.
- Comic zoom mechanics — PDS blob size limits in the wild, range-request support on getBlob/CDNs, JPEG XL client support; when a per-page renditions array earns its complexity.
- Verse detection on import — heuristic vs author annotation in markdown sources.
Implementation delta (July 2026 revision → code) #
Done: lexicons match the revision (page.cavu.content and
blocks/chapterTitle deleted; blocks/toc entries target
(document, page[, facet anchor]); scripts/codegen.ts roots updated and
src/lexicons regenerated — pub.leaflet.content, both page types, and
the full block set are now typed). Still to apply — these files
currently import the deleted page/cavu/content types and fail
typecheck:
- Reader: render
pub.leaflet.contentchunks (multi-page linearDocument, portal-block cards,blobPageshonoring, open-union verse/toc); TOC-driven chapter-opening treatment keyed on (document, page id); skip TOC-untargeted (nav) pages in the reading flow; drop thechapterTitlerender case (Blocks.tsx); updatebook.tscontent discrimination anddocs.tsxprose. - Importers:
publish-driftwood.ts/publish-honeyguide.tsemitpub.leaflet.content— onelinearDocumentpage per chapter, nav page first when a chunk holds several chapters, headers instead of chapterTitle blocks; the frontmatter doc becomes leaflet content (title header + injected toc block).import-leaflet-comic.tsandsmoke.tsalso import deleted types. Done for the web importer (July 28):reader/src/routes/import.tsx(+lib/markdown.ts,lib/publish.ts) is a browser importer — cover + markdown files in, atomic applyWrites of the revised shapes, same-name re-import replacement — and writes abuzz.bookhive.bookshelf record with BookHive's deterministic hiveId (bk_+ sha256base64(objectHash({title, author} lowercased))[:20], ohash@1 — verified byte-identical against their production catalog). Auth is atproto OAuth with granular scopes at two grant levels (lib/oauth.ts+lib/useAuth.ts,@atproto/oauth-client-browser): the/loginreader grant isatproto include:site.standard.authSocial repo:buzz.bookhive.book?action=create&action=update(subscriptions/recommends + shelf progress tracking); the publisher grant — asked for only via "Publish my work" on/login(the logged-in profile view) or the upgrade prompt on/import— swaps ininclude:site.standard.authFulland addsblob:image/*. Client metadata (served/oauth/client-metadata.json; loopback client id in dev — browse viahttp://127.0.0.1:3000) declares the union of both scopes and redirect URIs/login+/import. DPoP sessions persist in IndexedDB. This covers delta item 4's OAuth login; the logged-in home page joining subscriptions × publications × shelf progress is still to build. The script importers remained pre-revision until they were removed (September 2026) in favor of the studio. - New:
release-pair.ts— the three-write serialized-release batch (create release doc, put TOC doc, put publication). - Subscription flow (pre-launch requirement): reader login (atproto
OAuth), subscribe/unsubscribe writing
site.standard.graph.subscriptionrecords, a logged-in home page joining subscriptions × publications ×page.cavu.progress(progress bars, "N new chapters", continue reading), and opt-in progress writing from the reading views.