Add a thumbnail to the companion Bluesky post embed
The companion app.bsky.feed.post already carried a clickable link facet
and associatedRefs for the rich standard.site card (Decision 0013), but
embed.external.thumb was unset. Clients that don't resolve standard.site
cards — and the default card before the AppView resolves the refs —
therefore showed no image, unlike the reference post (pckt.blog).
Set embed.external.thumb to the first uploaded content image's existing
in-repo blob ref (image/*, size <= 1MB), selected depth-first from the
document's block tree. Omit thumb when there is no usable image; the
card still renders via associatedRefs.
Reuse the existing blob ref rather than re-uploading: atproto blobs are
content-addressed and repo-scoped, so re-uploading identical bytes only
yields the same CID — there is no separate post-owned copy. The publish
flow creates the document first, committing the image blob, before the
post, so the post can reference that same CID and the AppView resolves
the thumb by did + cid. This needs no uploadBlob, no byte-fetch, and so
no SSRF guard.
Researched against pckt.blog (live record) and Leaflet (source): both
upload a fresh, OG-cropped 1.91:1 blob server-side (sharp / screenshot).
SkyPress is a browser client + static renderer with no such pipeline, so
v1 follows the spirit (a real image blob) not the letter (no OG-shaping;
bsky center-crops the native image). Decision 0014 records the rationale
and the follow-ups (canvas OG-crop, cover-image picker, icon fallback).
Replace isolated-block-editor with @wordpress/block-editor directly
Compose SkyEditor from the Gutenberg block-editor packages directly
instead of wrapping @automattic/isolated-block-editor, and upgrade the
whole @wordpress/* tree from IBE's frozen line to the current release.
IBE is effectively maintenance-only (Dependabot-only commits, README
self-describes as "experimental", pins Gutenberg 16.9) and it forced
the entire @wordpress/* tree to be version-pinned via a ~60-package
overrides map — what Decision 0003 called the project's biggest
maintenance liability. That override map only existed to reconcile
IBE's old pinned line against transitive caret ranges floating to a
newer one. Depending on @wordpress/block-editor directly at one
current line removes that collision: the tree resolves to a single
coherent copy of every store singleton with no overrides, so upgrading
becomes a normal version bump instead of regenerating the map.
SkyEditor now wires BlockEditorProvider over a header toolbar (Inserter
+ a fixed BlockToolbar + undo/redo + a BlockInspector cog popover) and
the canvas (BlockTools / WritingFlow / ObserveTyping / BlockList), with
app-level undo via useStateWithHistory. The prop contract, curated
allowlist, @-mention format/autocompleter, and media-upload filter are
unchanged. The reader/render split (Decision 0003, Finding 1) is
untouched — reading pages still use the dependency-free render.ts and
the render-fidelity test-lock still passes against the new packages.
Two sharp edges, both recorded in Decision 0021 and AGENTS.md:
- core-data/notices/date install as nested copies with no hoisted
top-level one, so each registers its store ("Store 'core' is already
registered"). Fixed with npm dedupe + an expanded resolve.dedupe in
both astro.config and vitest.config (deduping before the hoist breaks
the build).
- In vitest, @wordpress/* must be Vite-inlined (Node rejects
@wordpress/blocks' attribute-less JSON import) while moment stays
external, or moment-timezone's augmentation of moment breaks.
The floating block toolbar needs iframe/content-ref plumbing a bespoke
inline canvas doesn't provide, so a fixed BlockToolbar is placed in the
header per the framework's guidance for custom editors.
Verified: npm run check (0 errors), npm test (592 pass incl. the render
fidelity lock), npm run build, and an in-browser smoke test of /write
on the production preview (boot, insert, type, draft-save, undo/redo,
publish-enable, allowlist, clean console).
Replace isolated-block-editor with @wordpress/block-editor directly
Compose SkyEditor from the Gutenberg block-editor packages directly
instead of wrapping @automattic/isolated-block-editor, and upgrade the
whole @wordpress/* tree from IBE's frozen line to the current release.
IBE is effectively maintenance-only (Dependabot-only commits, README
self-describes as "experimental", pins Gutenberg 16.9) and it forced
the entire @wordpress/* tree to be version-pinned via a ~60-package
overrides map — what Decision 0003 called the project's biggest
maintenance liability. That override map only existed to reconcile
IBE's old pinned line against transitive caret ranges floating to a
newer one. Depending on @wordpress/block-editor directly at one
current line removes that collision: the tree resolves to a single
coherent copy of every store singleton with no overrides, so upgrading
becomes a normal version bump instead of regenerating the map.
SkyEditor now wires BlockEditorProvider over a header toolbar (Inserter
+ a fixed BlockToolbar + undo/redo + a BlockInspector cog popover) and
the canvas (BlockTools / WritingFlow / ObserveTyping / BlockList), with
app-level undo via useStateWithHistory. The prop contract, curated
allowlist, @-mention format/autocompleter, and media-upload filter are
unchanged. The reader/render split (Decision 0003, Finding 1) is
untouched — reading pages still use the dependency-free render.ts and
the render-fidelity test-lock still passes against the new packages.
Two sharp edges, both recorded in Decision 0021 and AGENTS.md:
- core-data/notices/date install as nested copies with no hoisted
top-level one, so each registers its store ("Store 'core' is already
registered"). Fixed with npm dedupe + an expanded resolve.dedupe in
both astro.config and vitest.config (deduping before the hoist breaks
the build).
- In vitest, @wordpress/* must be Vite-inlined (Node rejects
@wordpress/blocks' attribute-less JSON import) while moment stays
external, or moment-timezone's augmentation of moment breaks.
The floating block toolbar needs iframe/content-ref plumbing a bespoke
inline canvas doesn't provide, so a fixed BlockToolbar is placed in the
header per the framework's guidance for custom editors.
Verified: npm run check (0 errors), npm test (592 pass incl. the render
fidelity lock), npm run build, and an in-browser smoke test of /write
on the production preview (boot, insert, type, draft-save, undo/redo,
publish-enable, allowlist, clean console).
Add publication system: dashboard, creation flow, publication pages
SkyPress auto-created one invisible publication per user (url = /@handle)
and had no UI to manage it. Replace that with a real, user-managed system:
multiple publications, a dashboard, a create/edit flow (name, logo,
description), and publication-centric public pages.
URL model becomes handle-namespaced (Decision 0010), all resolved from the
PDS so the no-database/no-KV stance holds — no global slug registry:
/@handle author index (lists the author's SkyPress publications)
/@handle/{slug} a publication's home
/@handle/{slug}/{rkey} a document within that publication
Slugs are auto-derived from the name, de-duplicated within the writer's own
repo, and frozen into publication.url so a rename never breaks links. SkyPress
manages only publications whose url origin is its own, so a record written by
another standard.site tool (Leaflet, …) is never listed, edited, or rendered.
Deleting a publication cascades to its documents and their Bluesky posts.
Publishing now targets a chosen publication (the editor gains a selector); the
implicit ensurePublication is gone. The author index revives the paused profile
work, reading app.bsky.actor.profile straight from the PDS (no appview) for
name/bio/avatar/cover — rendered as text, never injected as HTML.
Guardrails kept: reading pages import no @wordpress JS, every PDS-derived fetch
goes through safe-fetch, and the Bluesky-post disclosure stays on publish.
Spec: docs/specs/sp10-publication-system.md. Built test-driven (records, CRUD,
publish target, cascade delete, slug rules, profile + publication resolution).
Add publication system: dashboard, creation flow, publication pages
SkyPress auto-created one invisible publication per user (url = /@handle)
and had no UI to manage it. Replace that with a real, user-managed system:
multiple publications, a dashboard, a create/edit flow (name, logo,
description), and publication-centric public pages.
URL model becomes handle-namespaced (Decision 0010), all resolved from the
PDS so the no-database/no-KV stance holds — no global slug registry:
/@handle author index (lists the author's SkyPress publications)
/@handle/{slug} a publication's home
/@handle/{slug}/{rkey} a document within that publication
Slugs are auto-derived from the name, de-duplicated within the writer's own
repo, and frozen into publication.url so a rename never breaks links. SkyPress
manages only publications whose url origin is its own, so a record written by
another standard.site tool (Leaflet, …) is never listed, edited, or rendered.
Deleting a publication cascades to its documents and their Bluesky posts.
Publishing now targets a chosen publication (the editor gains a selector); the
implicit ensurePublication is gone. The author index revives the paused profile
work, reading app.bsky.actor.profile straight from the PDS (no appview) for
name/bio/avatar/cover — rendered as text, never injected as HTML.
Guardrails kept: reading pages import no @wordpress JS, every PDS-derived fetch
goes through safe-fetch, and the Bluesky-post disclosure stays on publish.
Spec: docs/specs/sp10-publication-system.md. Built test-driven (records, CRUD,
publish target, cascade delete, slug rules, profile + publication resolution).