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.

0010 — Publication system: handle-namespaced URLs, frozen slugs, ownership, delete#

  • Status: Accepted
  • Date: 2026-06-08
  • Scope: SP10 — publication dashboard, creation flow, publication pages

Context#

SkyPress auto-created one invisible publication per user (name = handle, url = /@handle) and matched it by URL. We now support multiple, user-managed publications with a dashboard, logos, and publication-centric public pages. This forces decisions on the URL model, slugs, which publications SkyPress "owns", and what deleting one does — all under the brief's hard no-database / no-KV constraint.

Decision#

1. Handle-namespaced URLs (option A), no global slug registry#

/@handle              author index
/@handle/{slug}       publication home
/@handle/{slug}/{rkey} document

A publication's url becomes https://skypress.blog/@{handle}/{slug}. Everything resolves from the PDS: resolve handle → DID → PDS, list that one repo's publications, match by slug segment. No global registry means subdomains (slug.skypress.blog) are explicitly out for v1 (they'd need a KV/D1 registry + wildcard DNS — a separate sub-project).

2. Slugs are auto-derived and frozen#

The slug is derived from the name at creation (slugify), de-duplicated within the user's own repo only (-2, -3, …), and frozen into publication.url. Later name edits never change it — URLs must never break (consistent with documents addressed by stable rkey, Decisions 0005/0008). Empty/emoji-only names fall back to pub-{rkey} (the publication record's own TID rkey is globally unique → no collision check). The rkey is generated up front (TID.nextStr(), the publisher.ts pattern) so the fallback URL is known before the record is written.

3. SkyPress only manages publications it owns (origin === SITE_BASE)#

A user may hold site.standard.publication records from other tools sharing the lexicon (Leaflet, etc.). SkyPress lists, edits, deletes, renders, and resolves only publications whose url origin equals SITE_BASE (isSkyPressPublicationUrl). This keeps the dashboard from offering to delete a foreign tool's publication, keeps slug resolution unambiguous, and honours the lexicon's "additions only / don't touch others' data" discipline. The replaced ensurePublication matched a single pub by exact URL for the same reason; this generalises it.

4. Publishing targets a chosen publication#

ensurePublication (auto-create one pub per handle) is removed. publish() now takes the target publication's { uri, slug }; the editor presents a selector. A user with no publication is sent to the dashboard to create one first. Resolution still verifies doc.site === publication.uri so a document only renders under its true publication.

5. Deleting a publication cascades#

Danger-zone delete removes the publication and its documents and each document's companion app.bsky.feed.post (reusing unpublish semantics, Decision 0008), then the publication record. The alternative — refusing to delete a non-empty publication — leaves orphaned, unreachable documents in the PDS and is worse UX. The confirm dialog states exactly how many articles and Bluesky posts will be removed, since this is destructive and outward- facing. Blobs (logo, images) become unreferenced and the PDS garbage-collects them.

Consequences#

  • publicationHomeUrl(handle, slug) and canonicalArticleUrl(handle, slug, rkey) change signatures; records.test.ts is updated (TDD).
  • The reader route splits: [author]/[slug]/index.astro + [author]/[slug]/[rkey].astro; [author]/[rkey].astro is removed. No migration — skypress.blog is not yet live.
  • /@handle becomes the author index, reviving the paused profile work: it reads app.bsky.actor.profile from the PDS directly (no Bluesky appview dependency) for name/bio/avatar/banner; those are rendered as text/images, never injected as HTML.
  • Slug freezing means a renamed publication keeps a slug derived from its old name. Accepted: URL stability beats prettiness, and there is no slug registry to rename against.

Amendment (2026-06-09): foreign publications are displayed read-only#

Section 3 originally hid every non-owned site.standard.publication record from the dashboard. Writers found this confusing: Leaflet's home shows all their publications, so SkyPress appeared to have "lost" the ones created elsewhere.

SkyPress now displays foreign publications (origin ≠ SITE_BASE) on the dashboard in a separate read-only "From other apps" section — name, logo, a hostname badge, and an outbound link to the publication's own url. They are modelled with a distinct ForeignPublication type so they are structurally incapable of reaching the manage/edit/delete paths.

The ownership guards are unchanged: SkyPress still manages, renders, and resolves only publications it owns (isSkyPressPublicationUrl). Only listing/visibility expanded.