Audit of the newsletter section found two real bugs.
1) /newsletter rendered in an infinite loop (100% CPU, React 'Maximum update
depth exceeded'). Chain: useSyncedAppData returned a fresh object each render
-> useReadState.apply (dep: synced) changed identity -> markNewsletterRead /
markAllRead changed every render -> the page effect (deps include it) re-ran
after every render -> the mark set a NEW state object -> loop. The
notification bell looped the same way while open.
- useLocalStorage: setter is now referentially stable (ref + functional set).
- useSyncedAppData: returned object is memoized.
- useReadState: apply reads synced/interactive from refs (stable); marks are
idempotent (same object when already marked) so React bails.
- NewsletterPage marks read once per visit (mount-once ref guard).
2) Issues + subscription markers were published via the default pool (the
author's own NIP-65 relays) but READ via useSkateRelays (community relay +
reader's relays; logged-out = community relay only), so an admin without
goskatespots in their list would publish drops nobody could see. Composer,
backfill panel and the subscribe hook now publish to getWriteRelays() and
fail loudly if every relay rejects (useSpotChat pattern). NIP.md untouched
(no schema change).
Also: guard the overlay pill/reminder against an empty archive, and drop a
redundant canPublish clause.
The composer derived the kind 30023 addressable `d` identifier straight from
slugify(title). Because 30023 is addressable, two issues sharing a slug —
a recurring "Weekly Roundup", or "Update!" vs "Update?" — would share a
coordinate and the newer publish would silently overwrite the older issue.
Blank slug now auto-generates a unique coordinate (title-slug + UTC date +
short base36 time suffix). An explicit manual slug is still honored verbatim,
so intentionally editing/replacing a specific issue keeps working.
Verified clean (no changes): backfill panel is idempotent (skips slugs already
on-relay, deterministic issue-{n} identifiers) and the subscriber-count hook
uses the standard newest-marker-per-author tally with no double-counting.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Turn the static newsletter into a live, Nostr-native marketing channel that
npubs can subscribe to and receive in-app.
Publishing (admins):
- Newsletter issues are published as NIP-23 long-form articles (kind 30023)
tagged `skatespotsnews`, on the community relay.
- NewsletterComposer: admin-only dialog to write + publish an issue (title,
slug, summary, cover image, Markdown body).
Subscribing (any npub):
- useNewsletterSubscription: per-user opt-in stored as a NIP-78 (kind 30078)
marker, so it syncs across every device the user logs into.
- NewsletterSubscribeButton on the /newsletter page and issue reader.
Mailbox / notifications:
- New "News" tab in the NotificationBell showing all issues, with an unread
badge for subscribers (tracked via a per-account last-read timestamp).
Latest unread drop also surfaces in the "All" tab.
- Opening the bell or the /newsletter page clears the News badge.
Reading:
- useNewsletterIssues: queries admin-authored issues (author-scoped + tag +
admin re-validation as the trust boundary against spam).
- LiveIssuesSection on /newsletter shows issue cards linking to a reader.
- NewsletterIssueView: naddr reader (wired into NIP19Page for kind 30023) with
a small safe Markdown renderer, zap + subscribe CTAs.
Docs: NIP.md documents the 30023 issue schema, the 30078 subscription marker,
and the admin trust boundary.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>