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).
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).
Show known-provider logos beside foreign publication hostnames
Foreign `site.standard.publication` records (written by Leaflet, pckt,
Offprint, …) now display the originating service's logo next to their
hostname — on the dashboard "From other apps" list and the public
profile "Elsewhere" section, which also gains the hostname it didn't
show before.
The hostname alone can't identify the service, because the paid tiers
serve from a custom domain. So detection is two-step: an app-specific
`$type` discriminator embedded in the record first (pckt writes
`theme.$type === "blog.pckt.theme"`, which survives a custom domain),
then a dot-boundary hostname-suffix fallback (`*.leaflet.pub`,
`*.pckt.blog`, `*.offprint.app`). Leaflet records carry no such
discriminator and Offprint is unsampled, so those two on a custom
domain stay logo-less, exactly as before — see Decision 0017.
Detection and the monochrome glyph data live in one framework-agnostic
module (`lib/publish/providers.ts`), since Astro can't server-render a
React component and the read path takes no client island: the React
dashboard and the Astro profile each render the shared data through a
tiny `ProviderLogo` of their own. Leaflet's saved asset was a raster
PNG, so its glyph is a substitute vector feather (Lucide, ISC).