Move the all-campaigns directory from /campaigns/all to /campaigns

/campaigns was a redirect to / (the curated home), and the actual
all-campaigns directory lived at /campaigns/all. Flip the routing
so /campaigns IS the directory, the home page stays at /, and
/campaigns/all becomes a redirect to /campaigns for any external
links and bookmarks that still point there.

Rewrite every internal link/navigate target accordingly (TopNav,
the Browse-all CTA on the home page, the OnboardingGate donor
redirect, NoteCard's kind-33863 nounRoute) and refresh the
doc/comment references in NIP.md and the discovery hooks.
This commit is contained in:
mkfain
2026-05-30 13:19:29 +02:00
parent 3a06dcd4cb
commit c53e476dee
9 changed files with 18 additions and 15 deletions
+2 -2
View File
@@ -70,7 +70,7 @@ Clients filter both case variants (`agora` and `Agora`) because Nostr `t` tags a
#### Backward compatibility
Events published before this marker was adopted do not carry `t:agora` and therefore do not appear in the Agora activity feed. They remain reachable by direct link and via kind-specific directories (e.g. the moderator-curated `/campaigns/all`). Authors who wish to surface a legacy event in the feed can republish it (any edit through the Agora UI will add the marker automatically).
Events published before this marker was adopted do not carry `t:agora` and therefore do not appear in the Agora activity feed. They remain reachable by direct link and via kind-specific directories (e.g. the moderator-curated `/campaigns`). Authors who wish to surface a legacy event in the feed can republish it (any edit through the Agora UI will add the marker automatically).
### Community Chat
@@ -498,7 +498,7 @@ The `pinnedEvents` array is ordered newest pin first. Pinning an already-pinned
### Agora Moderation Labels
Agora curates which kind 33863 campaigns appear on the homepage (`/`) and on the Support directory (`/campaigns/all`), which kind 34550 organizations appear in the Featured shelf on `/communities`, and which kind 36639 pledges appear in the discovery surfaces on `/pledges`, via moderator-signed NIP-32 label events (kind 1985) in a dedicated label namespace. The labeled event itself is never modified — surfacing is purely a client-side rollup of label events.
Agora curates which kind 33863 campaigns appear on the homepage (`/`) and on the Support directory (`/campaigns`), which kind 34550 organizations appear in the Featured shelf on `/communities`, and which kind 36639 pledges appear in the discovery surfaces on `/pledges`, via moderator-signed NIP-32 label events (kind 1985) in a dedicated label namespace. The labeled event itself is never modified — surfacing is purely a client-side rollup of label events.
Campaigns, organizations, and pledges share a single label namespace and a single moderator pack (Team Soapbox); the only thing distinguishing the three streams is the kind prefix on the `a` tag of each label:
+5 -2
View File
@@ -187,9 +187,12 @@ export function AppRouter() {
constraints. */}
<Route element={<FundraiserLayout narrow={false} />}>
<Route path="/" element={<CampaignsPage />} />
<Route path="/campaigns" element={<Navigate to="/" replace />} />
<Route path="/campaigns" element={<AllCampaignsPage />} />
<Route path="/campaigns/new" element={<CreateCampaignPage />} />
<Route path="/campaigns/all" element={<AllCampaignsPage />} />
{/* Legacy URL: the all-campaigns directory lived at
`/campaigns/all` for a while. Keep it as a redirect so
external links and bookmarks still resolve. */}
<Route path="/campaigns/all" element={<Navigate to="/campaigns" replace />} />
<Route path="/groups" element={<CommunitiesPage />} />
<Route path="/groups/new" element={<CreateCommunityPage />} />
<Route path="/events/new" element={<CreateEventPage />} />
+1 -1
View File
@@ -1867,7 +1867,7 @@ const KIND_HEADER_MAP: Record<number, KindHeaderConfig> = {
icon: HandHeart,
action: (event) => publishedAtKey(event, { created: "noteCard.kindHeader.campaignLaunched", updated: "noteCard.kindHeader.campaignUpdated", fallback: "noteCard.kindHeader.campaignFallback" }),
noun: "noteCard.kindHeader.campaignNoun",
nounRoute: "/campaigns/all",
nounRoute: "/campaigns",
},
8: {
icon: Award,
+2 -2
View File
@@ -132,7 +132,7 @@ function CaptiveOverlay() {
// Role pick is the final step. Picking a role both records the choice
// (used by the role-pick CTA labels) and navigates to the matching
// surface: creator → campaign-creation form, donor → full campaign grid
// (`/campaigns/all`, not `/`, so they land on the browse-everything view
// (`/campaigns`, not `/`, so they land on the browse-everything view
// rather than the curated home with its own marketing hero). No separate
// outro / celebration screen.
const handleRolePick = useCallback(
@@ -142,7 +142,7 @@ function CaptiveOverlay() {
if (next === 'creator') {
navigate('/campaigns/new');
} else {
navigate('/campaigns/all');
navigate('/campaigns');
}
},
[setContextRole, cancel, navigate],
+1 -1
View File
@@ -37,7 +37,7 @@ interface NavItem {
}
const NAV_ITEMS: NavItem[] = [
{ labelKey: 'nav.campaigns', to: '/campaigns/all', icon: HandHeart },
{ labelKey: 'nav.campaigns', to: '/campaigns', icon: HandHeart },
{ labelKey: 'nav.activity', to: '/feed', icon: Activity },
// Groups and Pledges are intentionally hidden from the main nav for
// launch — keep the routes and feature code intact so we can re-add
@@ -18,7 +18,7 @@ interface CampaignsDiscoverySectionProps {
* Where this section's filter state lives:
*
* • `'url'` — flat URL params (`?q=&sort=&country=`). Used by the
* dedicated `/campaigns/all` page so search results are
* dedicated `/campaigns` page so search results are
* shareable and survive refresh.
* • `'local'` — local-only state. Used by `/` where three
* discovery sections coexist and can't all own `?q=`.
+1 -1
View File
@@ -3,7 +3,7 @@ import { createContext, useContext } from 'react';
/**
* The two top-level roles a new user can pick during onboarding. Drives
* downstream copy (creator vs. donor framing) and the role-pick CTA target
* (creator → /campaigns/new, donor → /campaigns/all).
* (creator → /campaigns/new, donor → /campaigns).
*
* `null` before the user has answered the role-picker step.
*/
+4 -4
View File
@@ -12,7 +12,7 @@ import type { Nip50Sort } from '@/hooks/useNip50Search';
* - Anything else (missing, empty, legacy values) collapses to
* `'default'`, the curated featured-first idle state.
*
* Exported because the dedicated discovery pages (`/campaigns/all`,
* Exported because the dedicated discovery pages (`/campaigns`,
* `/pledges`) read `?sort=` independently from the section's hook to
* thread the value into ancillary derivations (hidden-list cache
* lookups, create-X href country prefills). One canonical parser
@@ -52,7 +52,7 @@ interface UseDiscoveryFiltersOptions {
* state.
*
* • `''` — flat URL params (`?q=…&sort=…&country=…`). The dedicated
* browse pages (`/campaigns/all`, `/groups`, `/pledges`) want
* browse pages (`/campaigns`, `/groups`, `/pledges`) want
* this so search results are shareable / linkable and survive
* refresh.
*
@@ -84,8 +84,8 @@ interface UseDiscoveryFiltersOptions {
* Owns three pieces of state — search input (debounced), sort mode,
* country code — and (optionally) mirrors them to URL params so deep
* links and browser back/forward work. Defaults are stripped on write
* so the canonical URL stays clean (`/campaigns/all`, not
* `/campaigns/all?q=&sort=`).
* so the canonical URL stays clean (`/campaigns`, not
* `/campaigns?q=&sort=`).
*
* Debouncing lives inside this hook (300ms) so consumers don't have
* to thread the debounced value back in — that would create a
+1 -1
View File
@@ -239,7 +239,7 @@ export function CampaignsPage() {
hidden. */}
<div className="pt-2 text-center sm:text-left">
<Button asChild variant="ghost" size="sm">
<Link to="/campaigns/all">{t('campaigns.home.browseAll')}</Link>
<Link to="/campaigns">{t('campaigns.home.browseAll')}</Link>
</Button>
</div>
</section>