8.9 KiB
FIPS Directory Page Plan
Goal
Turn www/fips-directory.html (currently a copy of the template) into a community FIPS link directory where:
- Anyone can view FIPS links (no login required).
- Logged-in users can add / remove their own links.
- There is no admin — every person maintains their own blocklist (NIP-51 mute list) and decides what to hide from their own view.
- The site owner publishes their curated FIPS links the same way everyone else does (no hardcoded seed data).
Standards Used
Links — NIP-B0: Web Bookmarking (kind:39701)
Each FIPS link is a separate replaceable event, one per URL:
kind:39701dtag: the FIPS URL (scheme prefix omitted forhttps://, per NIP-B0; forhttp://andws://FIPS URLs the full URL is used)titletag: the link namettag:fips-directory(so the page can subscribe to just FIPS-directory bookmarks)content: markdown description of the link (can be empty)published_attag: unix seconds string
Add a link = publish a new kind:39701 event with d = <url>.
Remove a link = publish a NIP-09 deletion request (kind:5) referencing the event id, OR publish an empty/blank replacement (NIP-B0 is replaceable by d, so republishing with empty content effectively removes it). We'll use NIP-09 deletion for a clean removal.
Blocklist — NIP-51: Lists (kind:10000 mute list)
The standard Nostr mute list. Each user publishes one replaceable event:
kind:10000ptags: pubkeys the user wants hidden from their own viewcontent: optionally NIP-44-encrypted private items (we only use publicptags)
Block a publisher = re-publish kind:10000 with their p tag added.
Unblock = re-publish without that p tag.
Sovereign moderation: blocking only affects the blocking user's own view. It does not delete or hide content for anyone else. This is exactly how every NIP-51-compatible Nostr client already works.
Page Behavior
Public (not logged in)
- Subscribe to all
kind:39701events with#t = ['fips-directory']. - Render every publisher's links as cards grouped by publisher.
- Show a "Sign in to add your links" prompt.
- No blocklist controls (no identity to attach a blocklist to).
Logged in
- Load the user's own
kind:10000mute list and filter blocked pubkeys out of the rendered list. - Show an Add Link form (name, URL, description).
- Show remove buttons on the user's own links (publishes a NIP-09 deletion).
- Show a block / unblock button on each publisher's section (for publishers other than yourself).
- Publishing uses
publishEvent()frominit-ndk.mjs(auto-signs via the worker).
Auth mode
authMode = 'optional'(matcheswww/app-stacks.html) — public load, login on demand via the sidenav logout/login button or the "Sign in" prompt.
UI Layout (inside #divBody)
┌─────────────────────────────────────────────┐
│ FIPS DIRECTORY │ (header text)
├─────────────────────────────────────────────┤
│ [Sign in to add your links] (if anon) │
│ │
│ ┌─ Add Link form ─────────────────────────┐ │ (only if logged in)
│ │ Name / URL / Description │ │
│ │ [Add Link] │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ┌─ Publisher: laantungir ─────────────────┐ │
│ │ [block] (if logged in & not you) │ │
│ │ • My Relay — ws://....fips/relay/ │ │
│ │ [remove] (if yours) │ │
│ │ • My Client — http://....fips/client │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ┌─ Publisher: someone-else ───────────────┐ │
│ │ [block] [unblock] │ │
│ │ • Their Thing — http://....fips/thing │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
- Cards use the existing
client.cssvariables (--border-color,--border-radius,--color,--muted-color,--font-family, etc.) — same inline-style pattern aswww/app-stacks.html. - Links open in a new tab (
target="_blank" rel="noopener"). - FIPS URLs are clickable as-is (the server is FIPS-enabled, per the user).
Data Structures (in-page state)
// Map: eventId -> { id, pubkey, url, title, description, createdAt }
let bookmarks = new Map();
// Set of blocked pubkeys (from current user's kind 10000)
let blocklist = new Set();
// The current user's kind 10000 event id (for re-publishing)
let myMuteListEventId = null;
// Load flags
let bookmarksLoaded = false;
let blocklistLoaded = false;
Implementation Steps
- Set page title & header text — change
<title>TEMPLATE</title>toFIPS DIRECTORYand set.divHeaderTextto "FIPS DIRECTORY". - Set
authMode = 'optional'as the default (like app-stacks.html). - Add state variables —
bookmarks(Map),blocklist(Set),myMuteListEventId, load flags. - Add
esc()helper — prevent XSS from relay content (same as app-stacks.html). - Add subscription + listener for
kind:39701with#t=['fips-directory']—subscribeFipsBookmarks()+initBookmarkListener(). Parse each event into the bookmarks Map, keyed by event id. Re-render on each event / EOSE. - Add subscription + listener for
kind:10000(mute list) —subscribeMuteLists()+initMuteListListener(). Only the current user's mute list matters for filtering; subscribe broadly, pick out the logged-in user's. Store blocked pubkeys in theblocklistSet. - Add
renderDirectory()— builds the directory HTML (add-link form if logged in, publisher sections grouped by pubkey, link cards with block/remove buttons), writes to#divBody. Filters out blocked pubkeys. - Add
doAddLink()— reads the form, publishes akind:39701event withd=<url>,title=<name>,t=fips-directory,content=<description>. - Add
doRemoveLink()— publishes akind:5(NIP-09 deletion) event referencing the bookmark's event id, then removes it from the local Map and re-renders. - Add
doToggleBlock()— adds/removes aptag in the user'skind:10000mute list, re-publishes, updates theblocklistSet, re-renders. - Wire up subscriptions in
main()— afterinitializeAuthenticatedPageFeatures(), call the subscribe/listener init functions (bookmarks subscription runs for both public and logged-in; mute list subscription only matters when logged in but can run always). - Footer UX note — "Public mode / Sign in from side menu to add your links" when anonymous.
Architecture Diagram
flowchart LR
A[User loads fips-directory.html] --> B[authMode = optional]
B --> C{Logged in?}
C -- no --> D[Show links + Sign in prompt]
C -- yes --> E[Show links + Add/Remove form + Block controls]
D --> F[Subscribe kind 39701 t:fips-directory]
E --> F
E --> G[Subscribe kind 10000 mute list]
F --> H[Render directory cards grouped by pubkey]
G --> H
H --> I[User clicks link -> opens FIPS URL]
E --> J[Add -> publishEvent kind 39701]
E --> K[Remove -> publishEvent kind 5 NIP-09 deletion]
E --> L[Block/Unblock -> publishEvent kind 10000]
J --> H
K --> H
L --> H
NIP References
- NIP-B0: Web Bookmarking —
kind:39701for links - NIP-51: Lists —
kind:10000mute list for blocklist - NIP-09: Event Deletion Request —
kind:5for removing a link
Notes
- No new JS modules needed — everything is inline in the HTML, matching the app-stacks.html pattern.
- No CSS file changes — uses existing
client.cssvariables via inline styles. - The
subscribe()/publishEvent()/getPubkey()APIs fromwww/js/init-ndk.mjsare already imported by the template. - Fully standards-based: other Nostr clients that support NIP-B0 bookmarks and NIP-51 mute lists will interoperate with the data we publish.