Files
client/plans/unified-url-structure.md
2026-04-17 16:52:51 -04:00

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:

  1. 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.
  2. No page reload — Changing window.location.hash does NOT reload the page. It fires a hashchange event that JavaScript can listen to. This is how single-page app routing works without a framework.
  3. Bookmarkable — The hash IS part of the URL in the address bar, so users can bookmark it, copy/paste it, and share it.
  4. 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

  1. 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.
  2. No episode in the URL — When an episode is selected, it is not reflected in the URL. You cannot link to a specific episode.
  3. music.html cannot load an episode playlist — There is no route that says "load this episode from this show by this pubkey."
  4. Inconsistent pubkey handling — music.html accepts both npub and pubkey query params; vj.html only accepts pubkey.
  5. 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

  1. Query string = identity + context that persists across hash navigation
  2. Hash fragment = current view/navigation state within the page
  3. Both pages share the same parameter vocabulary so links are interchangeable
  4. 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.

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.

music.html#/track/12345678
vj.html#/track/12345678
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= via history.replaceState
  • When an episode is selected, update ?episode= via history.replaceState
  • Use replaceState not pushState to avoid polluting history with every selection change
  • 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

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} via openFriendPlaylistByKey()
  • 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 friendPlaylists map, 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

  • selectStreamShow change handler calls updateUrlParam('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 show and episode in 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