7.2 KiB
Collections Feature Plan (NIP-51)
Overview
Let users organize their favorite community templates into named collections — "My Pixar Set", "Horror Night", "Criterion Shelf" — and share them publicly via Nostr. Anyone can browse a collection and import templates from it directly. Collections are standard NIP-51 bookmark sets (kind 30001), so they sync across Nostr clients automatically.
Complexity
Low–Moderate — Estimated 3–5 hours
Nostr Primitive: NIP-51 Bookmark Sets (kind 30001)
Kind 30001 is an addressable (replaceable) event that holds a named list of references.
For template collections, each list item is an a tag pointing to a kind 30078 template:
{
"kind": 30001,
"pubkey": "<user-pubkey>",
"tags": [
["d", "my-pixar-set"],
["title", "My Pixar Set"],
["description", "All my Pixar case wraps"],
["image", "https://...cover.jpg"],
["a", "30078:<creator-pubkey>:<d-tag>", "<relay-hint>"],
["a", "30078:<creator-pubkey>:<d-tag>", "<relay-hint>"]
],
"content": ""
}
- The
dtag is the collection's unique identifier (slug). Updating with the samedreplaces the previous version — no duplicate events accumulate. title,description, andimageare optional display metadata.- Each
atag references one template (kind 30078 addressable event).
Key Components
1. Creating a Collection
async function createCollection(name, description) {
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
const event = {
kind: 30001,
tags: [
['d', slug],
['title', name],
['description', description],
],
content: '',
created_at: Math.floor(Date.now() / 1000),
};
await publishToRelays(await window.nostr.signEvent(event));
return slug;
}
2. Adding/Removing a Template from a Collection
Since kind 30001 is a replaceable event, adding an item means fetching the current
version, appending the new a tag, and re-publishing:
async function addToCollection(collectionSlug, templateEvent) {
const aTag = buildATag(templateEvent); // '30078:<pubkey>:<d-tag>'
const current = await fetchMyCollection(collectionSlug);
const tags = current ? [...current.tags] : [['d', collectionSlug]];
if (!tags.some(t => t[0] === 'a' && t[1] === aTag)) {
tags.push(['a', aTag, '']);
}
await publishToRelays(await window.nostr.signEvent({
kind: 30001, tags, content: '', created_at: Math.floor(Date.now() / 1000),
}));
}
async function removeFromCollection(collectionSlug, templateEvent) {
const aTag = buildATag(templateEvent);
const current = await fetchMyCollection(collectionSlug);
if (!current) return;
const tags = current.tags.filter(t => !(t[0] === 'a' && t[1] === aTag));
await publishToRelays(await window.nostr.signEvent({
kind: 30001, tags, content: '', created_at: Math.floor(Date.now() / 1000),
}));
}
3. Fetching Collections
async function fetchMyCollections() {
// Query kind 30001 authored by myPubkey
return await queryRelays(relays, {
kinds: [30001],
authors: [state.community.myPubkey],
}, { timeoutMs: 3000 });
}
async function fetchCollectionByAddress(pubkey, slug) {
// Fetch a specific collection by pubkey + d-tag for sharing/browsing
return await queryRelays(relays, {
kinds: [30001],
authors: [pubkey],
'#d': [slug],
}, { timeoutMs: 3000 });
}
4. Browsing a Shared Collection
Collections are shareable via a URL fragment or Nostr address (naddr):
casewrap.app/#collection/naddr1...
When a collection address is opened:
- Fetch the kind 30001 event by naddr
- Extract all
atags → list of template addresses - Fetch each kind 30078 template event
- Render them as a mini-feed with "Import All" option
5. State Additions
// Add to state:
collections: {
mine: [], // user's own kind 30001 events
viewing: null, // currently browsed collection (own or external)
}
UI
Template Card — "Save to Collection" Menu Item
Add to the card action menu (⋮):
⑃ Remix this design
📁 Save to collection ▶ [My Pixar Set]
[Horror Night]
[+ New collection…]
Collections Panel (in sidebar or settings)
┌─────────────────────────────────────────┐
│ 📁 My Collections [+ New] │
│ │
│ My Pixar Set 8 templates [→] │
│ Horror Night 3 templates [→] │
│ Criterion Shelf 12 templates [→] │
└─────────────────────────────────────────┘
Collection Detail View
Clicking a collection opens a mini-feed inside the community panel:
← Back My Pixar Set (8 templates)
─────────────────────────────────────
[Import All] [Share] [Edit] [Delete]
[Card] [Card] [Card] ...
"Share" copies the naddr Nostr address to the clipboard for sharing.
Feed Cards — Collection Badge
If a template is in one of the user's collections, show a small 📁 icon on the card corner.
Performance Considerations
- Kind 30001 events are small (just tags) — fast to fetch and publish
- Fetch the user's collections once at login, cache in
state.collections.mine - Re-fetch lazily when the collections panel is opened
- Template events inside a collection are fetched only when the collection detail is opened
Limitations
- Requires NIP-07 login to create or modify collections
- Read-only browsing of other users' shared collections works without login
- Nostr clients that don't parse kind 30001
atags (onlyetags) may not display templates correctly — this is a client compatibility issue, not a bug here - No private/encrypted collections — kind 30001 is a public event (NIP-51 has a draft for private lists but it is not widely supported)
Future Enhancements
- Collaborative collections: multiple pubkeys can contribute (needs a separate coordination event)
- Collection cover image: auto-generated from the first template's preview image
- "Import All" bulk-imports every template in a collection into a local queue
- Featured collections: curated by app admins on a discovery page
- RSS/Atom feed for a collection (static export)
Testing Checklist
- Creating a collection publishes a valid kind 30001 event with
dandtitletags - Adding a template adds the correct
atag and re-publishes the event - Removing a template removes the
atag without affecting others - Duplicate adds are ignored (same
atag not added twice) - Collections panel lists all user's kind 30001 events with correct counts
- Collection detail fetches and renders referenced template events
- "Share" copies a valid naddr to clipboard
- Shared collection URL loads and renders without login
- "Import All" imports each template in the collection
- 📁 badge appears on feed cards that are in a user collection
- Collections sync correctly after page reload (re-fetched from relays)