A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

Select the types of activity you want to include in your feed.

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:

  1. site.standard.publication (rkey: TID) — created once per writer if missing. url (required) = the writer's SkyPress homepage https://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 by url — NOT a self singleton, and NOT just any standard.site publication the writer may already have from other tools.
  2. site.standard.document (rkey: TID) — the article.
    • site = the publication's at:// URI (links document↔publication, which Bluesky requires for the highest-fidelity card — brief §3).
    • path = /<rkey> (the document's own record key). Canonical URL = publication url + path = https://skypress.blog/@<handle>/<rkey>.
    • title, publishedAt (required); description; textContent (de-facto required — Bluesky computes reading-time/search from it, ignoring content); bskyPostRef (strongRef to the post, below); content = our Gutenberg object.
  3. app.bsky.feed.post (rkey: TID) — the social signal, with an app.bsky.embed.external link 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 getRecord the 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 }]
  • blocks items are typed unknown (arbitrary objects): Gutenberg block attributes are open-ended per block type and can't be strictly schematised. Each node matches the BlockNode shape render.ts consumes, so reconstruction is a direct read.
  • Lexicon authoring discipline (brief §3): optional fields, open unions, additions-only. version lets us evolve the serialization without breaking old records; a breaking change ships as blog.skypress.content.gutenberg v2 (a new $type), never a silent mutation.
  • Graceful degradation: any reader that doesn't understand this $type falls back to the document's textContent — which is why we always write good textContent.

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.