0005 — Lexicon design & the publish model#
- Status: Accepted
- Date: 2026-06-08
- Scope: SP2 (publish). Edit semantics (the "puppy problem") are SP5.
Shapes below are verified against the live site.standard.* lexicons
(tangled.org/standard.site/lexicons, fetched 2026-06-08), not just the brief.
Records written on publish#
Two (or three) records on the writer's PDS, per brief §1:
site.standard.publication(rkey: TID) — created once per writer if missing.url(required) = the writer's SkyPress homepagehttps://skypress.blog/@<handle>;name(required) = a default derived from the handle. Reused on later publishes by listing the writer's existing publication records and matching byurl— NOT aselfsingleton, and NOT just any standard.site publication the writer may already have from other tools.site.standard.document(rkey: TID) — the article.site= the publication'sat://URI (links document↔publication, which Bluesky requires for the highest-fidelity card — brief §3).path=/<rkey>(the document's own record key). Canonical URL = publicationurl+path=https://skypress.blog/@<handle>/<rkey>.title,publishedAt(required);description;textContent(de-facto required — Bluesky computes reading-time/search from it, ignoringcontent);bskyPostRef(strongRef to the post, below);content= our Gutenberg object.
app.bsky.feed.post(rkey: TID) — the social signal, with anapp.bsky.embed.externallink card pointing at the canonical article URL.
URL structure#
https://skypress.blog/@<handle>/<rkey>. The @ marks the handle segment, and the
document is addressed by its record key, not a title slug:
- a slug isn't stable across title edits; the rkey never changes (URL stability for the edit flow, SP5);
- the renderer (SP4) can
getRecordthe document directly by rkey — no listing + path-matching.
Publish order (avoids a circular dependency)#
The document's rkey is generated client-side up front (TID.nextStr()), so the article
URL is known before any record is written. So:
ensure publication → create post (embed → article URL) → create document at the pre-chosen rkey (content + textContent + bskyPostRef). One document write, no follow-up
update.
Superseded by Decision 0013. To embed the standard.site link card, the post must carry the document's strongRef, so the order is now
create document (no bskyPostRef) → create post (facet + associatedRefs) → putRecord document (add bskyPostRef). Two document writes; see 0013 for why the resulting stale ref is harmless.
The SkyPress content lexicon — blog.skypress.content.gutenberg#
Goes inside the document's open content union (brief §3). The block tree is canonical
(the array from onSaveBlocks), not rendered HTML.
blog.skypress.content.gutenberg (object)
version: integer — serialization version (currently 1)
blocks: array of unknown — the Gutenberg block tree: [{ name, attributes, innerBlocks }]
blocksitems are typedunknown(arbitrary objects): Gutenberg block attributes are open-ended per block type and can't be strictly schematised. Each node matches theBlockNodeshaperender.tsconsumes, so reconstruction is a direct read.- Lexicon authoring discipline (brief §3): optional fields, open unions, additions-only.
versionlets us evolve the serialization without breaking old records; a breaking change ships asblog.skypress.content.gutenbergv2 (a new $type), never a silent mutation. - Graceful degradation: any reader that doesn't understand this
$typefalls back to the document'stextContent— which is why we always write goodtextContent.
NSID rationale: the app lives at skypress.blog, so the reverse-DNS namespace is
blog.skypress.* (brief §3). Only the content format is SkyPress-owned; publication +
document metadata reuse site.standard.* for interop.
Scope#
Writing these records needs write access. Per brief §2, use atproto transition:generic for now ("keep transition:generic until read-side scopes
stabilize"). This is the existing OAUTH_SCOPE; SP2 wires it into the dev loopback
client_id (which previously defaulted to read-only atproto), so the user re-auths once
to grant writes. The granular repo:site.standard.* / repo:app.bsky.feed.post scopes
(brief §2) are a later tightening.
Edit semantics (the "puppy problem") — deferred to SP5, but constrained now#
SP2 only creates. The document uses a TID rkey (the lexicon mandates key: 'tid'),
so a stable URL across edits will mean updating the same record (rkey) in SP5. SP2 does
not implement re-publish/update; that decision (mutate vs. version vs. new record) is
SP5's, and the lexicon's updatedAt field is reserved for it.
Don't surprise the user (brief §10, non-negotiable)#
Publishing creates a public Bluesky post. The publish UI must state this unmistakably and require an explicit confirm before writing. Implemented in the publish panel.