12 KiB
Unified URL Variable Structure — music.html & vj.html
Background: How URL Hash Fragments Work
A URL has this anatomy:
https://example.com/vj.html?show=reggae&npub=npub1abc#/playlist/my-episode-42
\_____________________/\________/ \_______________/ \________________________/
origin pathname query string hash fragment
The # Hash Fragment
The hash fragment (everything after #) has special properties:
- Not sent to the server — When the browser requests a URL, the hash is stripped. The server never sees it. This makes it ideal for client-side-only state.
- No page reload — Changing
window.location.hashdoes NOT reload the page. It fires ahashchangeevent that JavaScript can listen to. This is how single-page app routing works without a framework. - Bookmarkable — The hash IS part of the URL in the address bar, so users can bookmark it, copy/paste it, and share it.
- History entries — Changing the hash pushes a new entry onto the browser history stack, so Back/Forward buttons work naturally.
Query String vs Hash
| Feature | Query String ?key=value |
Hash Fragment #/path |
|---|---|---|
| Sent to server | Yes | No |
| Page reload on change | Yes, unless using replaceState |
No |
| History entry on change | Only with pushState/replaceState |
Automatic |
| Best for | Identity, auth mode, external references | In-page navigation, view state |
Both pages currently use both mechanisms — query strings for identity/auth and hash fragments for in-page navigation. This is a good pattern.
Current State Audit
Query String Parameters Currently Used
| Parameter | Page | Purpose |
|---|---|---|
npub |
music.html | Target user pubkey in npub format |
pubkey |
music.html, vj.html | Target user pubkey in hex format |
auth |
both | Auth mode override: required, optional, none |
a |
vj-stream.mjs | Stream coordinate 30311:{pubkey}:{slug} |
naddr |
vj-stream.mjs | Nostr address encoding of stream |
show |
vj-playlist.html | Pre-select a show slug |
Hash Routes Currently Used
Both music.html and vj.html share identical hash routing via parseRoute() and handleRoute():
| Hash Route | Purpose |
|---|---|
#/ |
Home — empty search view |
#/search/{query} |
Search results for query |
#/album/{albumId} |
Album detail drill-down |
#/artist/{artistId} |
Artist detail drill-down |
#/track/{trackId} |
Play a specific track |
#/playlist/{identifier} |
Open own playlist by identifier |
#/playlist/{pubkey}/{identifier} |
Open friend playlist by pubkey + identifier |
Problems with Current Structure
- vj.html has no show in the URL — The selected show is only in the
<select>dropdown state. You cannot link to a specific show. - No episode in the URL — When an episode is selected, it is not reflected in the URL. You cannot link to a specific episode.
- music.html cannot load an episode playlist — There is no route that says "load this episode from this show by this pubkey."
- Inconsistent pubkey handling — music.html accepts both
npubandpubkeyquery params; vj.html only acceptspubkey. - No cross-page linking — A link generated in vj.html cannot be opened in music.html to play that episode's playlist.
Proposed Unified URL Structure
Design Principles
- Query string = identity + context that persists across hash navigation
- Hash fragment = current view/navigation state within the page
- Both pages share the same parameter vocabulary so links are interchangeable
- Show and episode are query params because they represent session context, not navigation state
Query String Parameters — Unified
?npub={npub1...} # Target user — npub format, preferred
&pubkey={hex} # Target user — hex format, fallback
&auth={required|optional|none} # Auth mode
&show={slug} # Active show slug — NEW for vj.html
&episode={identifier} # Active episode playlist identifier — NEW
&a={30311:pubkey:slug} # Stream coordinate — existing in vj-stream.mjs
&naddr={naddr1...} # Nostr address — existing in vj-stream.mjs
Hash Routes — Unified
Both pages support the same routes. The hash controls what is displayed in the search/results area:
#/ # Home — empty search
#/search/{query} # Search results
#/album/{albumId} # Album detail
#/artist/{artistId} # Artist detail
#/track/{trackId} # Play specific track
#/playlist/{identifier} # Own playlist/episode
#/playlist/{pubkey}/{identifier} # Friend/external playlist/episode
Full URL Examples
VJ working on a show
vj.html?show=saturday-reggae&episode=ep-1744382400
The VJ is working on the "Saturday Reggae" show, episode "ep-1744382400". The show dropdown auto-selects, the episode loads into the setlist.
Sharing a VJ episode link for music.html playback
music.html?npub=npub1abc123...&show=saturday-reggae#/playlist/ep-1744382400
music.html loads, fetches the playlist event for that episode from the given npub, and plays it.
Sharing a specific episode with full context
vj.html?npub=npub1abc123...&show=saturday-reggae&episode=ep-1744382400
Opens vj.html viewing someone else's show and episode in read-only mode.
Direct track link — works on either page
music.html#/track/12345678
vj.html#/track/12345678
Cross-page playlist link
music.html#/playlist/npub1abc123.../ep-1744382400
music.html loads the episode playlist from that pubkey and plays it.
URL Flow Diagram
flowchart TD
A[Page Load] --> B{Parse query string}
B --> C[Extract npub/pubkey -> targetPubkey]
B --> D[Extract auth -> authMode]
B --> E[Extract show -> selectedShow]
B --> F[Extract episode -> selectedEpisode]
C --> G[Initialize authentication]
D --> G
G --> H{Parse hash fragment}
E --> I[Auto-select show in dropdown]
F --> J[Auto-load episode into setlist/queue]
H --> K{Route type?}
K -->|#/search/...| L[Run search]
K -->|#/album/...| M[Show album detail]
K -->|#/artist/...| N[Show artist detail]
K -->|#/track/...| O[Play track]
K -->|#/playlist/...| P[Load playlist/episode]
K -->|#/ or empty| Q[Show home]
I --> R[Filter episodes by show]
J --> R
R --> S[Render setlist/queue]
P --> T{Has pubkey in path?}
T -->|Yes| U[Fetch from external pubkey]
T -->|No| V[Load from own playlists]
Implementation Plan
Phase 1: Add show and episode query params to vj.html
File: www/vj.html
1a. Read show from URL on page load
- In the initialization flow after authentication, read
?show=from the query string - If present, auto-select that show in
#selectStreamShow - This replaces the current behavior where the show is only selected manually
1b. Read episode from URL on page load
- Read
?episode=from the query string - If present, find the matching playlist by identifier and load it into the setlist
- Call
openMyPlaylistByIdentifier()or equivalent
1c. Update URL when show/episode changes
- When the user selects a show from the dropdown, update
?show=viahistory.replaceState - When an episode is selected, update
?episode=viahistory.replaceState - Use
replaceStatenotpushStateto avoid polluting history with every selection change
1d. Generate shareable links
- Add a "Copy Link" button or integrate into existing share flow
- Build URL:
vj.html?show={slug}&episode={identifier}for own content - Build URL:
vj.html?npub={npub}&show={slug}&episode={identifier}for sharing to others
Phase 2: Unify pubkey handling
Files: www/vj.html, www/music.html
2a. Accept both npub and pubkey in vj.html
- Update
getTargetPubkeyFromUrl()to checknpubfirst, thenpubkey— matching music.html's pattern
2b. Prefer npub in generated URLs
- When building shareable URLs, always use
npub=format since it is the Nostr standard for sharing
Phase 3: Enable music.html to load episode playlists
File: www/music.html
3a. Add show query param support
- Read
?show=on page load — used as context/filter when loading playlists from a target pubkey
3b. Add episode query param support
- Read
?episode=on page load - If present along with a target pubkey, subscribe to that user's kind 30078/30004 events and find the matching episode
- Load the episode's tracks into the queue and start playback
3c. Handle #/playlist/{pubkey}/{identifier} for episodes
- The existing
handleRoute()already handles#/playlist/{pubkey}/{identifier}viaopenFriendPlaylistByKey() - Ensure this path works for episode playlists — it should subscribe to the pubkey's events and find the playlist by identifier
- If the playlist is not yet in the local
friendPlaylistsmap, fetch it from relays
Phase 4: Create a shared URL utility module
New file: www/js/url-params.mjs
Extract the duplicated URL parsing logic into a shared module:
// Shared URL parameter parsing for all pages
export function getTargetPubkeyFromUrl() { ... }
export function resolveAuthModeFromUrl() { ... }
export function getShowFromUrl() { ... }
export function getEpisodeFromUrl() { ... }
export function updateUrlParam(key, value) { ... } // replaceState helper
export function removeUrlParam(key) { ... }
export function buildShareUrl(page, params) { ... }
Phase 5: Sync URL state on user actions
Files: www/vj.html, www/music.html
5a. vj.html — show selection updates URL
selectStreamShowchange handler callsupdateUrlParam('show', slug)
5b. vj.html — episode selection updates URL
- When an episode is clicked in the sidebar, call
updateUrlParam('episode', identifier)
5c. vj.html — episode creation updates URL
- After creating a new episode, update both
showandepisodein the URL
5d. music.html — playlist selection updates URL
- When a playlist is selected, update the hash to
#/playlist/{identifier}— this already happens
Summary of URL Parameter Vocabulary
| Parameter | Location | Type | Used By | Description |
|---|---|---|---|---|
npub |
query | string | both | Target user npub — preferred format |
pubkey |
query | hex | both | Target user hex pubkey — fallback |
auth |
query | enum | both | Auth mode: required, optional, none |
show |
query | slug | both | Active show slug |
episode |
query | string | both | Active episode playlist identifier |
a |
query | coord | vj.html | Stream coordinate 30311:pubkey:slug |
naddr |
query | naddr | vj.html | Nostr address of stream |
#/search/{q} |
hash | path | both | Search query |
#/album/{id} |
hash | path | both | Album detail view |
#/artist/{id} |
hash | path | both | Artist detail view |
#/track/{id} |
hash | path | both | Play specific track |
#/playlist/{id} |
hash | path | both | Own playlist/episode |
#/playlist/{pk}/{id} |
hash | path | both | External playlist/episode |
Files to Modify
| File | Changes |
|---|---|
www/vj.html |
Read show/episode from URL, update URL on selection, accept npub param |
www/music.html |
Read show/episode from URL, load episode playlists from external pubkeys |
www/js/url-params.mjs |
NEW — shared URL parsing utilities |
www/js/vj-stream.mjs |
No changes needed — already reads a and naddr |