Files
client/plans/fips-directory-page.md
T

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

Each FIPS link is a separate replaceable event, one per URL:

  • kind: 39701
  • d tag: the FIPS URL (scheme prefix omitted for https://, per NIP-B0; for http:// and ws:// FIPS URLs the full URL is used)
  • title tag: the link name
  • t tag: fips-directory (so the page can subscribe to just FIPS-directory bookmarks)
  • content: markdown description of the link (can be empty)
  • published_at tag: 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: 10000
  • p tags: pubkeys the user wants hidden from their own view
  • content: optionally NIP-44-encrypted private items (we only use public p tags)

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:39701 events 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:10000 mute 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() from init-ndk.mjs (auto-signs via the worker).

Auth mode

  • authMode = 'optional' (matches www/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.css variables (--border-color, --border-radius, --color, --muted-color, --font-family, etc.) — same inline-style pattern as www/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

  1. Set page title & header text — change <title>TEMPLATE</title> to FIPS DIRECTORY and set .divHeaderText to "FIPS DIRECTORY".
  2. Set authMode = 'optional' as the default (like app-stacks.html).
  3. Add state variablesbookmarks (Map), blocklist (Set), myMuteListEventId, load flags.
  4. Add esc() helper — prevent XSS from relay content (same as app-stacks.html).
  5. Add subscription + listener for kind:39701 with #t=['fips-directory']subscribeFipsBookmarks() + initBookmarkListener(). Parse each event into the bookmarks Map, keyed by event id. Re-render on each event / EOSE.
  6. 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 the blocklist Set.
  7. 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.
  8. Add doAddLink() — reads the form, publishes a kind:39701 event with d=<url>, title=<name>, t=fips-directory, content=<description>.
  9. Add doRemoveLink() — publishes a kind:5 (NIP-09 deletion) event referencing the bookmark's event id, then removes it from the local Map and re-renders.
  10. Add doToggleBlock() — adds/removes a p tag in the user's kind:10000 mute list, re-publishes, updates the blocklist Set, re-renders.
  11. Wire up subscriptions in main() — after initializeAuthenticatedPageFeatures(), 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).
  12. 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

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.css variables via inline styles.
  • The subscribe() / publishEvent() / getPubkey() APIs from www/js/init-ndk.mjs are 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.