Skip to content

Connect linkblogs to existing publications - #343

Open
disnet wants to merge 12 commits into
mainfrom
radial/impl-e4487980da06
Open

Connect linkblogs to existing publications#343
disnet wants to merge 12 commits into
mainfrom
radial/impl-e4487980da06

Conversation

@disnet

@disnet disnet commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • Connect linkblogs to existing user-owned standard.site publications with format-aware document publishing.

  • Keep externally managed publication records read-only and migrate follower scopes durably across rapid publication switches.

  • Merge old and connected publication scopes on public linkblogs, with page, permalink, and RSS metadata loaded from the resolved connected publication.

  • Resolve the connected publication on every read path: the in-app linkblog pull, discovery's subscribe targets, own-post affordances, and the source pill.

  • Read notes back from all four content formats and rewrite them in place on edit; on the public site, reconstruct Leaflet blockquote blocks with rebased mention facets.

  • Keep PublicationMeta.url on the Skyreader linkblog page (a connected publication's own site rides along as externalUrl), always offer the Skyreader linkblog in the settings picker, and list only entries that link out from a connected publication.

  • Mark Skyreader's own link posts and gate every mutating affordance on that marker, so a post the connected publication's home app wrote can't be note-edited or deleted from Skyreader (server-side too, with a 409).

  • Correct a follower's document scope server-side when its author has moved publications, echoing the requested scope back; clients also take feedUrl from the server on their next subscription sync.

  • Union locally-known connected linkblogs into discovery's registry, and persist site_url so a followed linkblog still reads as "Linkblog" on a device that didn't create the follow.

  • Stamp the marker on the optimistic just-shared document, and prune local shares only when NO pulled document points at the article — an unmarked document is ambiguous, not proof of deletion.

  • Keep site_url, atmosphere_previous_feed_url and active across a Jetstream mirror (it was an INSERT OR REPLACE), and push siteUrl/category to the PDS record instead of stripping them.

  • Anchor the linkblog-page path test to the whole path, index user_settings.linkblog_publication, and union the marker query over site.standard.document so connected-publication authors are discoverable network-wide.

  • Fix the E2E job crashing in Playwright global setup: wrangler d1 migrations apply output grew past execFileSync's 1 MB default maxBuffer once this branch pushed the repo to 64 migrations (spawnSync npx ENOBUFS).

  • Describe each publication in the settings picker before asking the user to choose one: the list endpoint infers each publication's app from its own posts (falling back to its host), and the picker became radio rows showing app, address and post count, with the content format pre-filled from what that publication already publishes and the Skyreader linkblog's own name/description separated out.

  • Name the connected publication in the first-share "this is public" warning: it linked the Skyreader linkblog address even when links were going to a connected publication, and now names that publication, links its own address, and mentions the linkblog page separately as somewhere the post also shows up.

Checks

  • backend: npm run check (tsc + prettier) clean; npm test 380/380 passing (4 new this round).
  • frontend: npm run check 0 errors (25 pre-existing warnings); npm test 296/296 passing (7 new this round); Playwright E2E 27/27 (1 new).
  • feed-proxy: npm run check:types clean; prettier clean at the repo-pinned 3.8.1.
  • Not run: feed-proxy's bun test (no bun binary available in this environment; the change there is one Constellation query added to the registry), Playwright e2e/PWA suites (browsers not installed; no linkblog coverage there).

Deploy order

feed-proxy forwards the skyreaderLinkblog marker; deploy it with or before the frontend, otherwise pulled documents in a connected publication read as foreign (no Remove, no note edit) until it ships.

Radial artifact
Radial artifact
Radial artifact
Radial artifact

codexbot.disnetdev.com (did:plc:hbonvqr5ysrscg5wdyb5klie) and others added 12 commits August 4, 2026 18:55
Co-Authored-By: codexbot.disnetdev.com (did:plc:hbonvqr5ysrscg5wdyb5klie) <codexbot.disnetdev.com@noreply.radial>
Co-Authored-By: codexbot.disnetdev.com (did:plc:hbonvqr5ysrscg5wdyb5klie) <codexbot.disnetdev.com@noreply.radial>
Co-Authored-By: codexbot.disnetdev.com (did:plc:hbonvqr5ysrscg5wdyb5klie) <codexbot.disnetdev.com@noreply.radial>
Co-Authored-By: codexbot.disnetdev.com (did:plc:hbonvqr5ysrscg5wdyb5klie) <codexbot.disnetdev.com@noreply.radial>
Sharing to a connected standard.site publication worked, but every reader
still assumed the fixed `skyreader-links` publication.

- The in-app pull (myLinkblogStore) now resolves the target and reads it
  alongside the default, reconciling entries by echoed siteUri. A pull that
  can't see the connected publication is no longer reported complete, so
  linkblogStore.reconcile() can't prune live shares out of IndexedDB.
- Discovery resolves each listed author's current publication in one batched
  settings lookup, so new subscribers aren't scoped to a dead publication.
- Notes are read back from all four content formats (Leaflet, pckt, Offprint,
  Markdown) in-app and on the public site, and note edits rewrite the note
  region in the record's own format instead of failing. The public site's
  Leaflet reader now reconstructs blockquote blocks too, with mention facets
  rebased.
- PublicationMeta.url stays the Skyreader linkblog page (the shareable one);
  a connected publication's own site rides along as `externalUrl`.
- Own-post affordances and the "Linkblog" source pill match the resolved
  publication rather than the default rkey.
- The Skyreader linkblog is always offered in the settings picker, so there's
  a way back even before its record exists; only entries that link out are
  listed from a connected publication, and the copy says so.

Co-Authored-By: claudebot.disnetdev.com (did:plc:n6ku5xddiuguwze3f356evla) <claudebot.disnetdev.com@noreply.radial>
… at them

Connecting an existing publication means sharing a repo with the app that owns
it, and the last iteration still assumed the whole publication was ours.

- Skyreader's link posts now carry the same constant marker its publications do.
  Un-share, in-place note editing, and the "already shared" overlay require it
  (or the default publication, where everything is ours), so a Leaflet/pckt post
  that happens to link out can't be rewritten or — the destructive half —
  deleted. The write paths re-read the record and refuse with 409, so a stale
  client can't get past the client-side gate either.
- The document batch endpoint re-points a request whose scope names a linkblog
  publication its author has moved off, echoing the requested scope back so the
  client's digests and reconciliation keys don't shift. Follower migration
  rewrote D1, but nothing could reach a follower's device; their feed just
  stopped updating. Clients also take feedUrl from the server on the next
  subscription sync, so they heal for themselves.
- Discovery unions the local list of connected linkblogs over the Constellation
  marker registry: connecting stamps nothing (the record isn't ours to touch), so
  a user who connects before their first share was invisible to it.
- subscriptions_cache persists site_url. The author's linkblog page is what says
  "linkblog" once the publication rkey is arbitrary, and it only existed on the
  device that created the subscription; the migration backfills it as followers
  move. The page is now recognized by its DID-keyed path, not a hostname prefix
  that both missed dev origins and matched unrelated `linkblogs.*` hosts.
- A scope that errored during a linkblog pull no longer counts as complete, so
  reconcile() can't prune live shares against a snapshot it couldn't confirm.

Co-Authored-By: claudebot.disnetdev.com (did:plc:n6ku5xddiuguwze3f356evla) <claudebot.disnetdev.com@noreply.radial>
…ying

Closes the four findings left open on the previous iteration.

The optimistic just-shared document now carries the `skyreaderLinkblog`
marker, so it reads as ours in a connected publication and stays in the
cross-device overlay while the proxy indexes the write. reconcile() also
stops using the marker-gated overlay as its prune signal: it now keeps a
local row whenever ANY pulled document points at that article. Deleting a
live share is the destructive direction, and an unmarked document is
ambiguous (a pre-marker share, or a proxy that doesn't forward the field
yet), not proof of deletion.

`site_url` now survives a PDS round-trip. The Jetstream mirror was an
`INSERT OR REPLACE`, which re-creates the row and so silently cleared
every column the record doesn't carry — `site_url`, the pending
`atmosphere_previous_feed_url` migration, and `active` (a parked feed came
back reactivated). It's an UPDATE of the fields the record actually
carries, falling back to an INSERT for a genuinely new row. The push side
was dropping `siteUrl`/`category` from the record it writes, so a changed
subscription stripped them from the PDS; both are pushed and compared in
the no-op check now.

The linkblog page test anchors the whole path shape instead of looking for
a DID anywhere in it — siteUrl is client-supplied, and plenty of
Atmosphere apps key pages by DID.

Discovery: a partial covering index on `user_settings.linkblog_publication`
(the connected-author list ran a full table scan on every /discover), and
the registry now unions the same marker query over `site.standard.document`
so authors who share to a publication we didn't create are visible
network-wide, not just to the deployment holding their settings row.

Co-Authored-By: claudebot.disnetdev.com (did:plc:n6ku5xddiuguwze3f356evla) <claudebot.disnetdev.com@noreply.radial>
Playwright's global setup shells out to `wrangler d1 migrations apply`
and buffers everything it prints. Wrangler reprints the whole migration
table after each migration it applies, so a fresh CI database emits
roughly the square of the migration count in bytes — at 64 migrations
that is 1.1 MB, just past execFileSync's 1 MB default maxBuffer, and the
whole E2E job died with `spawnSync npx ENOBUFS` before a single test ran.

Nothing reads that stdout, so discard it instead of buffering it; the
only output this step inspects (migration errors) goes to stderr, which
now gets room to spare.

Co-Authored-By: claudebot.disnetdev.com (did:plc:n6ku5xddiuguwze3f356evla) <claudebot.disnetdev.com@noreply.radial>
The connect flow shipped as two unstyled <select>s: one listing publication
names ("Untitled publication", twice), one asking for a "Content format" the
user had no way to answer. Nothing distinguished Skyreader's own linkblog from
a Leaflet or pckt publication except a "(Skyreader)" suffix.

Publications now arrive described. `GET /api/linkblog/publications` reads the
user's documents alongside their publications and infers each one's app from
the content lexicon its posts already use, falling back to the publication's
host — NSIDs are reverse-DNS, so `pub.leaflet.*` gives leaflet.pub without
guessing. Each choice carries its app, description, http-checked url, post
count, and the format Skyreader should write there. The document read is
best-effort: if it fails the list still returns, just plainer.

The picker is now radio rows in the save-backing vocabulary — Skyreader's
linkblog first and named as ours, then "Or a publication you already have",
each row showing name, app badge, host, post count and an open-in-new-tab
link. Selecting one pre-fills the format its own posts use and says so; the
format select is styled to match the inputs beside it instead of rendering as
a raw platform widget. With no other publications there's one quiet line
saying where they'd appear, and no dead controls.

Connecting also makes the split explicit: name and description are now shown
under "Your Skyreader linkblog" and hidden (not disabled) while a connected
publication is live, replaced by a line saying its home app owns those fields.

Co-Authored-By: claudebot.disnetdev.com (did:plc:n6ku5xddiuguwze3f356evla) <claudebot.disnetdev.com@noreply.radial>
The note under the button spoke as if a publication were already connected even
on the common path where none ever has been. Split it three ways: what
connecting does, what switching back leaves behind, and what Skyreader will and
won't touch in someone else's publication.

Co-Authored-By: claudebot.disnetdev.com (did:plc:n6ku5xddiuguwze3f356evla) <claudebot.disnetdev.com@noreply.radial>
The "this is public" dialog always said the share was going to the
Skyreader linkblog page, even after the user connected an existing
standard.site publication — which is where the post actually lands. It
now names that publication and links its own address, and mentions the
linkblog page separately as somewhere it also shows up.

Co-Authored-By: claudebot.disnetdev.com (did:plc:n6ku5xddiuguwze3f356evla) <claudebot.disnetdev.com@noreply.radial>
Co-Authored-By: claudebot.disnetdev.com (did:plc:n6ku5xddiuguwze3f356evla) <claudebot.disnetdev.com@noreply.radial>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant