# 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 1. **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. 2. **A "chapter" is a TOC breakpoint, not a record.** The start of a chapter exists at a specific point in a specific record. 3. **The TOC is multi-level; its leaf nodes are chapters.** Inner nodes are groupings (parts, volumes, acts). 4. **Serialized writing and series must both be easy to subscribe to** — adding chapters to a book, and adding books to a series. 5. **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. 6. **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**: ```jsonc { "$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 `book` distinguishes book publications from series publications. - `$type` inside 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 `coverImage` at 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's `toc` role plays EPUB's `properties="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)** — the `linearDocument` page's own (standard) `id` field — 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.series` points 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: 1. **create** the release document (`pub.leaflet.content`; one `linearDocument` page per chapter, pages carrying ids like `ch-5`; `pages[0]` is the release nav page with a `blocks.page` card per chapter when the release holds more than one). 2. **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). 3. **put** the book publication with one new spine entry (and `completionStatus` flipped 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 with `at://` 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) → optional `block` index path + byte `offset` mirroring `pub.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. - **`position` vs `furthest`**: 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 — the `bookProgress` logic in `book.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 after `position.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.book` as the progress record** (checked before settling): `hiveId` is required and Cavu books aren't in the hive catalog; `key: tid` means list-and-match lookups, read-modify-write updates, and cross-device duplicate races; `bookIdentifiers` has no AT-URI field, so the record can't even name our book without a pass-through extension; and `bookProgress` (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. `textContent` gives indexers/previews plaintext. Target chunk size ~100–300KB. - **Comics**: `page.cavu.pages` — pre-paginated image content, the fixed-layout counterpart to a `pub.leaflet.content` text chunk (see "Comic content format" below). Zoom options: 1. **Renditions per page** — blobs at multiple widths. Simple, works everywhere; start here. 2. **Progressive formats** — JPEG XL / progressive JPEG decode low-res from a byte prefix; needs range-request support on blob endpoints (not guaranteed). 3. **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 `pages` array holds one `pub.leaflet.pages.linearDocument` per chapter. The page's own (standard, optional) `id` field 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 into `blobPages` (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* through `pub.leaflet.blocks.page` portal blocks (Notion-style tree, no sequential next/prev — confirmed in their `PostPages.tsx`). So a multi-chapter chunk's first page is a short release landing page — the release title and one `blocks.page` card per chapter — and each chapter page is a portal target. Single-chapter chunks skip this; the chapter itself is `pages[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.header` with 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 former `page.cavu.blocks.chapterTitle` block 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 also `header`; the TOC, not the heading level, distinguishes them. - **Book blocks ride the open union.** `linearDocument`'s block union is open (no `closed: true`; the lexicon spec makes unions open by default), and Leaflet's published-view renderer confirms the skip behavior: its block dispatch ends in `default: return null` (`PostContent.tsx`). So `page.cavu.blocks.verse` (and `toc`, 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's `textContent` complete 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 `#id` facets.** Leaflet's richtext facet lexicon includes an `#id` feature ("used for linking to a segment") — an inline anchor on a byte range, in standard vocabulary. Finer-than- page targets (deep links, the future `page.cavu.position` reading-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.toc`** block — 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`** — `plaintext` with significant newlines (`\n` = line break, `\n\n` = stanza break) + the same Leaflet facets. Mirrors `pub.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) as `plaintext` + facets, with an optional `sceneNumber` and a `dual` flag 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) + optional `id` + optional `spread` hint. 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 | rtl` on `page.cavu.book` (EPUB's spine-level `page-progression-direction`; absent = ltr, `rtl` for manga) decides which slot of a spread comes first in reading order and which way pages turn. - Per-page `spread: left | right | center` mirrors EPUB's `page-spread-*` properties. `left`/`right` pin a page to a physical side — renderers insert a blank slot when the natural flow disagrees, exactly as print does (e.g. `right` on page 1 makes it open alone like a proper recto); `center` gives 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 `identifiers` pattern (`hiveBookUri`) is the template: an identifier/link pointing at our book publication is the natural ask to byline; a `listing` AT-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 `name` on the membership object (render without a fetch) and their `contributor` def (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 `bskyPostRef` cover 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.book` records 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 `bookProgress` is page-granularity; our fine-grained record is `page.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: 1. **Book theme** — `page.cavu.theme`-style object on the publication (the `theme`/`basicTheme` pattern): 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. 2. **Block attributes** — declarative, enumerable hints on the block wrapper: alignment (Leaflet precedent), inset, size variant. Safely ignorable by simple renderers. 3. **Author stylesheet** — a CSS blob on the publication + a `class` hook 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. 4. **Freeform layout content types** — for concrete poetry and House-of-Leaves pages: a positioned-canvas content type (`pub.leaflet.pages.canvas` is precedent) or SVG pages with `textContent` fallback, 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 `` in the page head. Fully path-scoped. There is also a `rel="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.publication` returns the publication's AT-URI as plaintext — and it is **path-capable** ("Non-root Publications"): a publication whose `url` is `https://example.com/books/foo` verifies at `https://example.com/.well-known/site.standard.publication/books/foo`. Live precedent: atproto.com/blog. The publication `url` field is `format: 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` + `path` must 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 record `url` actually 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 `index` on 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: 1. **Reader**: render `pub.leaflet.content` chunks (multi-page linearDocument, portal-block cards, `blobPages` honoring, open-union verse/toc); TOC-driven chapter-opening treatment keyed on (document, page id); skip TOC-untargeted (nav) pages in the reading flow; drop the `chapterTitle` render case (`Blocks.tsx`); update `book.ts` content discrimination and `docs.tsx` prose. 2. **Importers**: `publish-driftwood.ts`/`publish-honeyguide.ts` emit `pub.leaflet.content` — one `linearDocument` page 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.ts` and `smoke.ts` also 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 a `buzz.bookhive.book` shelf 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 `/login` reader grant is `atproto 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 in `include:site.standard.authFull` and adds `blob:image/*`. Client metadata (served `/oauth/client-metadata.json`; loopback client id in dev — browse via `http://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. 3. **New**: `release-pair.ts` — the three-write serialized-release batch (create release doc, put TOC doc, put publication). 4. **Subscription flow** (pre-launch requirement): reader login (atproto OAuth), subscribe/unsubscribe writing `site.standard.graph.subscription` records, 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.