Skip to content

Add first-time artist setup flow for album + publisher feeds#71

Open
ChadFarrow wants to merge 150 commits into
masterfrom
new-onboarding-v2
Open

Add first-time artist setup flow for album + publisher feeds#71
ChadFarrow wants to merge 150 commits into
masterfrom
new-onboarding-v2

Conversation

@ChadFarrow

Copy link
Copy Markdown
Owner

Introduces an "Artist Setup" path that creates both an album feed and a
publisher catalog simultaneously with GUIDs cross-linked, so new artists
aren't left to manually wire the two feeds together.

  • NewFeedChoiceModal: adds Artist Setup button (album mode only) with
    description explaining the linked-feed approach
  • App.tsx: handleArtistSetup generates both feeds with pre-linked GUIDs
    and deposits them into state; user lands on the album editor
  • Editor.tsx: Publisher Feed section shows a green "linked" banner with
    a "Go to Publisher" shortcut when a local publisher feed is already
    cross-referenced; updates description text for the unlinked case
  • CatalogFeedsSection: shows an inline prompt to add the current album
    when it isn't yet in the catalog, or a confirmation when it is
  • SaveModal: adds "Download Feed Package (album + publisher)" option
    that downloads both XMLs plus a next-steps.txt when feeds are linked
  • xmlGenerator: adds downloadText helper used by the package download
  • PublisherFeedReminderSection: replaces wall-of-text with a visual
    checklist (title, artwork, catalog count) and adds a reminder to
    submit the album feed to Podcast Index too

Existing flows are completely unchanged — new paths are additive only.

https://claude.ai/code/session_013pnx6R6iNhgVxMJFkmLMrN

claude and others added 30 commits May 21, 2026 16:51
Introduces an "Artist Setup" path that creates both an album feed and a
publisher catalog simultaneously with GUIDs cross-linked, so new artists
aren't left to manually wire the two feeds together.

- NewFeedChoiceModal: adds Artist Setup button (album mode only) with
  description explaining the linked-feed approach
- App.tsx: handleArtistSetup generates both feeds with pre-linked GUIDs
  and deposits them into state; user lands on the album editor
- Editor.tsx: Publisher Feed section shows a green "linked" banner with
  a "Go to Publisher" shortcut when a local publisher feed is already
  cross-referenced; updates description text for the unlinked case
- CatalogFeedsSection: shows an inline prompt to add the current album
  when it isn't yet in the catalog, or a confirmation when it is
- SaveModal: adds "Download Feed Package (album + publisher)" option
  that downloads both XMLs plus a next-steps.txt when feeds are linked
- xmlGenerator: adds downloadText helper used by the package download
- PublisherFeedReminderSection: replaces wall-of-text with a visual
  checklist (title, artwork, catalog count) and adds a reminder to
  submit the album feed to Podcast Index too

Existing flows are completely unchanged — new paths are additive only.

https://claude.ai/code/session_013pnx6R6iNhgVxMJFkmLMrN
Shows a 3-step onboarding modal (Welcome, Choose Mode, Quick Start)
on first visit using a localStorage flag. Re-openable from the
hamburger menu via "Getting Started". Dismissal at any step marks
onboarding complete.

https://claude.ai/code/session_01MPtgXUYSqMGU8b52RoYLvM
- Replace OnboardingModal with OnboardingPage (full-screen takeover via
  early return from AppContent, mirroring the AdminPage pattern).
- Add a step-0 gate that asks returning users if they want to skip the
  tour; first-visit auto-shows the gate, hamburger menu opens directly
  at step 1.
- Fix undefined --accent-color CSS variable (use --accent-primary).
- Misc copy edits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces a new 'artist' feedType for editing album and publisher feeds
on a single page, eliminating the mode-switch friction in PR #63's
Artist Setup flow. MVP stacks existing editor sections; v2 may
de-duplicate shared fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11-task plan covering FeedType extension, ArtistEditor composition,
handleSwitchFeedType auto-creation logic, CatalogFeedsSection dead-code
fix, and manual verification. References spec at
docs/superpowers/specs/2026-05-21-combined-artist-editor-design.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
handleStartBlank now creates fresh cross-linked album+publisher with
new GUIDs when invoked from Artist mode; previously it silently fell
through to album-only SET_ALBUM, demoting feedType back to 'album'.
The New button tooltip also gains an Artist branch so it stops
displaying "New Album" while the user is in Artist mode.
The section's purpose is linking an externally-hosted publisher feed
by URL — it does a Podcast Index lookup to populate
album.publisher.feedGuid. In Artist mode the publisher feed is created
locally with a fresh GUID and cross-linked via state, so the URL-lookup
flow is contradictory. Task 6 only hid the banner/text inside the
section; this hides the whole Section element so the URL input and
PI-lookup UI don't appear at all in the combined editor.
Hide CatalogFeedsSection, PublisherFeedReminderSection,
DownloadCatalogSection, and PublishSection when feedType is 'artist'.
Artist mode is first-time setup — the user is building one album and
one publisher feed simultaneously, not managing a catalog of other
albums. The current album is auto-included in the publisher's
remoteItems by handleSwitchFeedType, and Save → Download Feed Package
covers the export, so these sections only add noise. They remain
available in standalone Publisher mode.
New hostedBoth SaveModal mode that hosts album + publisher feeds in
sequence via /api/hosted using Nostr auth, surfacing the album's
hosted state and showing both PI-submission URLs on success. The
internal /api/hosted handler already calls notifyPodcastIndex for each
feed, so both feeds end up submitted to Podcast Index (pubnotify +
add/byfeedurl) and podping'd without a second round trip.

In Artist mode, the dropdown swaps the single Host on MSP option for
this combined variant when the user is Nostr-logged-in. Without
Nostr login, the single Host on MSP option remains as fallback (hosts
album only — same as today).

Also updated the Download Feed Package next-steps.txt to point users
at Host Both as the one-click alternative, and dropped the
mode-switching instructions that are no longer necessary.
Renders a 'Publish your feeds' panel at the bottom of ArtistEditor with
two primary action buttons inline:
- Host on MSP — album + publisher (one click), gated on Nostr login
- Download Feed Package (host yourself)

Both call shared helpers in src/utils/artistPublish.ts that SaveModal's
hostedBoth and package cases were refactored to use, eliminating
duplication. Status, progress, and per-feed result URLs render
inline in the section after the action completes.

A 'More save options →' link opens the SaveModal for the longer tail
of destinations (Submit to PodcastIndex by URL, Publish to Nostr
Music, etc.), so the inline section guides the common case without
hiding the rest.
The link was redundant — the bottom toolbar's Save button already opens
the SaveModal from any mode. Clean up the now-unused onOpenSaveModal
prop chain through ArtistEditor and App.tsx.
When the user clicks 'Host on MSP — album + publisher', render a live
step list inside the publish panel showing:
  1. Host album feed on MSP + submit to Podcast Index
  2. Host publisher feed on MSP + submit to Podcast Index
  3. Verify both feeds appear in Podcast Index

Each step transitions ○ → ◐ → ✓ as work progresses. Hosting steps
expand to show the hosted URL and PI ID once done. After both feeds
are uploaded, the verification step polls /api/pisearch?q={guid} for
each feed with timed retries (~22 s budget). When PI confirms both,
the row goes green with 'Both feeds are searchable in Podcast Index'
and lookup links; if either is still pending the row shows a
'check back later' message — Podcast Index can take a few minutes to
crawl new submissions.

hostBothOnMSP's progress signature changed from `onProgress(msg)` to
`onStep(step)` with structured step events. SaveModal's caller was
updated to convert in-progress steps back to a single message string
so the modal's behavior is unchanged.
Replace ~22 s waitForBothFeedsInIndex with a multi-phase poll: 5 fast
attempts in the first 22 s (PI typically commits new submissions in a
few seconds), then back off to 15-60 s intervals across the next ~6
minutes. Each tick emits a VerifyProgress event so the section can
render which feed has been confirmed, which is still pending, and how
long until the next check. Cancellation token plumbed through so
unmount or a re-click aborts in-flight polling.

The verify row now shows per-feed status while polling — '✓ Album:
found' / '⏳ Publisher: not yet found · check manually →' plus a
countdown line 'Checking again in 30s · attempt 8 of 16'. When both
land green, the row consolidates back to the single 'Both feeds are
searchable in Podcast Index' confirmation. After the full ~6.5 min
budget exhausts, it falls back to a 'stopped checking — refresh later'
note.

Host on MSP button stays disabled and changes its label once the
upload starts AND remains disabled for the rest of the session even
after polling ends — so the user can't accidentally re-trigger the
whole flow. To re-host they refresh or use the bottom-toolbar Save.
Two fixes surfaced by the first end-to-end test:

1. Disable Host Both until both album.title and publisherFeed.title are
   non-empty. PI silently drops feeds with empty titles, leaving the
   verify step stuck on 'not yet found' even though the upload
   succeeded. Helper text under the button now points at the missing
   side ('Add a title to your publisher feed above…').

2. In handleSwitchFeedType('artist'), when an existing publisher is
   preserved but its remoteItems doesn't reference the current album,
   append the link. Without this, switching into Artist mode after
   restoring an old session's publisher (or after the album GUID has
   changed) results in a hosted publisher XML carrying a stale
   <podcast:remoteItem feedGuid=…> that doesn't match the album it's
   supposed to catalog.
The hamburger menu's '🧪 Load Test Data' button only dispatched
SET_ALBUM, which (a) left the publisher feed in whatever state it
happened to be in (often empty or stale from a prior session) and
(b) reset feedType to 'album', yanking the user out of Artist mode.

Net result: clicking Load Test Data in Artist mode produced a fully
populated album feed paired with a blank publisher feed carrying a
stale remoteItem from earlier work. Hosting that publisher to MSP
uploaded an empty XML which Podcast Index couldn't index.

Now: in Artist mode, the button generates a matched album+publisher
fixture pair with cross-linked GUIDs and restores feedType to
'artist' after dispatching. In other modes, behavior is unchanged.
3-5 s checks were too eager — Podcast Index typically takes 30 s to a
few minutes to commit new submissions to its byguid index, so the
fast initial polls burned API calls without finding anything. New
schedule starts at 20 s, ramps to 60 s, then 120 s. Total budget
~10.5 minutes across 12 attempts.
If the user clicks Host Both, refreshes mid-flow, and the publisher
feed is still being indexed by Podcast Index, the section used to
present an unlocked 'Host on MSP' button — easy to accidentally
re-submit. Now: on mount, check localStorage for hosted info for both
feeds. If both are present, hydrate the section as if the host just
completed: result is set (button locks), the two host steps show ✓,
the verify step flips to in-progress, and PI polling resumes
automatically.

Polling honors the same cancellation token, so unmounting still
cleans up. The effect depends only on the GUIDs (not other album/
publisher fields) so editing a title or other field while polling
won't restart the verification cycle.
When Podcast Index's add/byfeedurl returns an empty body (which it
appears to do for medium=publisher submissions), the previous code
parsed JSON unconditionally and the whole call collapsed into a
generic 'Unexpected end of JSON input'. Now: capture response.text()
first, surface the HTTP status + content-type when the body is empty
or non-JSON, and only attempt JSON parsing on a non-empty payload.

Useful permanently as error visibility; immediately useful for
diagnosing why publisher feeds aren't reaching PI's registry.
PI's add/byfeedurl returns an empty body for our publisher submissions
even though album submissions through the same code path eventually
land in the index. To avoid blocking the verify step on a feed that may
take much longer to crawl, the success criterion is now album-only.
The publisher's host step keeps its own row showing the hosted URL and
a manual lookup link.

To improve discoverability regardless of PI's behavior, hostBothOnMSP
now precomputes both feeds' hosted URLs (feedId == podcastGuid in
/api/hosted) and patches them into the album's <podcast:publisher
feedUrl=…> reference and the publisher's <podcast:remoteItem feedUrl=…>
before serializing the XMLs. Crawlers that follow Podcasting 2.0
references can now reach the publisher organically. Existing user-set
feedUrls are preserved — we only fill in when empty.

After upload, the injected URLs are also dispatched back into the
feed store so the editor reflects the cross-links the user just
shipped.
Hydration on refresh used to permanently lock the Host Both button
once result was set, blocking the most-common need: 'I edited a feed,
let me re-host with the latest XML.' Now the button only locks while
hosting is actively in flight (uploads + verify polling). After
that completes, it stays clickable with a 'Re-host both feeds
(update with latest XML)' label, distinguishing the re-host case
from a fresh first-time host.

This is needed RIGHT NOW because the previously-hosted publisher
XMLs are missing feedUrl on their remoteItems — the new cross-link
injection in 8557c7d only takes effect on the next upload. Users
need to be able to click Host Both again to push the updated XML
to MSP, which triggers PI to re-crawl with the proper cross-link.
Confirmed with a known-working example (Longy publisher, PI ID
7696544) that PI does index publisher feeds — they just need the
remoteItem feedUrls populated so PI's crawler can follow them. The
8557c7d cross-link injection now provides that, so the verify step
can again poll both feeds with confidence.

The step is 'done' only when both album AND publisher land in PI.
While polling, the row shows per-feed status (✓ Album · ⏳ Publisher)
with a countdown to the next attempt. If the poll budget exhausts,
the fallback copy still suggests checking manually later.
MSP's create-empty factories seed a single blank ValueRecipient
(name='', address='', split=0, type='lnaddress') so the editor has
something to render. That placeholder was getting serialized into
the hosted XML even when the user never filled it in — producing a
<podcast:value> block with one empty recipient and split='0'.

This appears to be what's tripping Podcast Index's automated parser
on publisher submissions: PI registers the URL but fails to extract
title/guid/image, leaving a half-indexed entry that only gets
properly populated after a manual web-form re-add (which seems to
trigger a more forgiving parse).

generateValueXml now filters recipients that have no address before
emitting. If nothing remains, the whole <podcast:value> block is
suppressed (return ''). Albums with real value blocks keep their
existing behavior unchanged because their recipients all have real
addresses.
ChadFarrow and others added 9 commits June 24, 2026 21:26
Artist Profile (src/components/Profile/): a returning-artist home that
lists the user's MSP-hosted feeds with Edit / Copy URL / RSS / PI / New
Album actions. Logged-in owners of >=1 hosted feed auto-route here on
load; reachable anytime via hamburger > My Profile.

- useMyHostedFeeds hook: owner-filtered GET /api/hosted/ fetch (shared,
  skips the redundant per-feed XML re-fetch)
- Edit loads a hosted feed into the editor and persists hosted creds
  (buildHostedInfoForEdit) so Save does a PUT, not a 409 POST
- New Album attaches to the existing publisher (no second publisher),
  with an unsaved-changes confirmation
- Onboarding gate "Yes, used before" routes toward the profile, offering
  Nostr sign-in when logged out (onChooseReturning)
- Sign In modal: drop "New to Nostr?" tab, context-aware Google copy for
  returning users, note that an account is only needed for MSP hosting,
  fix spurious tab scrollbar, align the Quick Sign In panel
- Trim Getting Started and Feature Preferences from the hamburger menu

Also adds the onboarding landing-flow design doc (docs/superpowers/specs/).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gate's 'I'm new' branch now asks where the feed will live: self-host
goes straight to the editor (no account, no wizard); MSP-host opens the
guided wizard. Replaces onChooseFirstTime with onChooseSelfHost/onChooseMspHost.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'Yes, I've used this before' now lands in the editor instead of auto-
opening the sign-in modal, so a self-host returner isn't walled. The
header Sign In remains the offer; the auto-route effect still moves a
signed-in owner to their Profile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The wizard already exits to the editor via the top-right X; this adds a
labeled footer button so an MSP-host newcomer who decides to self-host
isn't stuck. Reuses handleDismiss; entered fields are preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Final whole-branch review (ready to merge, no Critical/Important) raised
two Minor clarity items: note WHY self-host uses plain album mode (not
artist mode, which scaffolds publisher/V4V), and mark the now-unreachable
tour steps 1-4 as dead-until-re-wired per the Phase 1 plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Gate hosting-choice: MSP hosts your 'feed and media'
- New Artist self-host helper: list 'your own website' first; drop CDN
- Downloaded next-steps guide (downloadArtistFeedPackage): align STEP 1
  host examples with the UI (own website / GitHub Pages / S3), and fix the
  STEP 2 faster-path to point at the New Artist page's 'Publish your feeds
  -> Host on MSP (album + publisher)' section (there is no such option in
  the Save modal)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Gate now forks 'I'm new' into self-host (-> album editor) vs MSP-host
  (-> wizard); 'Yes, used before' offers (not forces) sign-in
- Wizard 'Skip to editor' exit (bottom-right)
- New Artist Profile section: auto-route, useMyHostedFeeds, edit/add-album
  (PUT-vs-POST cred + one-publisher invariant)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ChadFarrow and others added 6 commits June 25, 2026 21:48
The footer 'Skip to editor' button sat flush against the primary Next and
invited mis-clicks. The top-right X already closes the wizard into the
editor (handleDismiss), so drop the footer button and keep that as the
single exit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A managed keypair starts with no kind-0 profile, so there's nothing to
pull — hide the button when connectionMethod === 'managed' (mirrors the
V4V hide in ValueStep). NIP-07/NIP-46 users still see it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an opt-in hideNpub prop to PersonsSection (default false); the
wizard's ExtrasStep passes it for managed (Google) users, who have no
Nostr context. Everywhere else (per-track persons in TrackList, the main
editor) keeps the npub field unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PersonsSection gains an opt-in myNpub prop; the wizard's ExtrasStep passes
the logged-in Nostr user's npub (not for managed/Google, whose field is
hidden) so an artist can fill a person's npub with their own in one click.
Mirrors the existing 'use mine' button on the album Artist npub field.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each track with an enclosureUrl renders a native audio control (preload
'none' so it only fetches on play) so the artist can listen before publishing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 'View on Podcast Index' link was buried in the feed-URL help text;
move it to a bordered card with a primary-styled button below the album
feed URL — it's the key 'is it live?' action.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…PI CTA

CLAUDE.md wizard section: 'Use my Nostr name & photo' hidden for managed
keys (publisher step); PersonsSection hideNpub/myNpub props in the Credits
step; per-track audio preview on Review; post-publish PI link as a standout card.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ch fallback)

PI's web /search?q= matches title/author, never the podcast:guid, so the
old guid-search fallback could never find the feed. And the numeric
/podcast/<id> page was resolved only once at publish, before PI finished
indexing.

- New side-effect-free GET /api/pi-resolve?guid= (byguid -> numeric URL),
  safe to poll (no pubnotify ping / podping, unlike /api/pubnotify)
- OnboardingWizard polls it with backoff after publish and upgrades the
  link to /podcast/<id> once PI indexes; cancelled on unmount
- ReviewStep fallback now searches by artist/album NAME instead of guid

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expands BLOSSOM_MEDIA_SERVERS from 2 to 4 servers for upload redundancy,
and surfaces "Hosted on X of Y servers" on every successful upload (not
just partial failures) so users can see how many servers hold the file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants