§22 S1: frontend /c/<collection>/ route layer + C3.7 + legacy-URL redirects

- entryPaths builders carry the /c/<collection>/ segment (default collection in S1)
- /p/:projectId/* gains c/:collectionId/ corpus routes; serving stays project-scoped
- DefaultCollectionRedirect (C3.7: project landing -> default collection)
- LegacyCorpusRedirect (v0.35.0 /p/<p>/e/<slug> bookmarks -> /c/default/, query preserved)
- entryPaths unit test; build + vitest green (18 tests)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-05 08:30:18 -07:00
parent 9ca07a3f81
commit 4f72aa31e0
3 changed files with 94 additions and 18 deletions
+39 -6
View File
@@ -1,10 +1,10 @@
import { useEffect, useRef, useState } from 'react'
import { Routes, Route, Link, Navigate, useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import { Routes, Route, Link, Navigate, useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom'
import { getMe, subscribeToNotifications } from './api'
import { anonymize, EVENTS, identify, track } from './lib/analytics'
import { useLastState } from './lib/useLastState'
import { brandTitle } from './lib/brand'
import { entryPath, proposalPath } from './lib/entryPaths'
import { entryPath, proposalPath, DEFAULT_COLLECTION } from './lib/entryPaths'
import { useDeployment } from './context/DeploymentProvider'
import ProjectLayout from './components/ProjectLayout.jsx'
import Directory from './components/Directory.jsx'
@@ -360,10 +360,21 @@ export default function App() {
/>
<main className="main-pane">
<Routes>
<Route path="" element={<Welcome viewer={viewer} />} />
<Route path="e/:slug" element={<RFCView viewer={viewer} />} />
<Route path="e/:slug/pr/:prNumber" element={<PRView viewer={viewer} />} />
<Route path="proposals/:prNumber" element={<ProposalView viewer={viewer} onChange={() => setCatalogVersion(v => v + 1)} />} />
{/* §22 three-tier (C3.7): the project landing redirects into
its sole/default collection (S1: the `default` one). */}
<Route path="" element={<DefaultCollectionRedirect />} />
{/* Backcompat: the shipped v0.35.0 corpus URLs without a
/c/<collection>/ segment redirect into the default
collection, so old bookmarks keep working. */}
<Route path="e/:slug" element={<LegacyCorpusRedirect kind="entry" />} />
<Route path="e/:slug/pr/:prNumber" element={<LegacyCorpusRedirect kind="entryPr" />} />
<Route path="proposals/:prNumber" element={<LegacyCorpusRedirect kind="proposal" />} />
{/* Collection-scoped corpus. Serving stays project-scoped in
S1 (collection = default); collection-aware serving is S2. */}
<Route path="c/:collectionId" element={<Welcome viewer={viewer} />} />
<Route path="c/:collectionId/e/:slug" element={<RFCView viewer={viewer} />} />
<Route path="c/:collectionId/e/:slug/pr/:prNumber" element={<PRView viewer={viewer} />} />
<Route path="c/:collectionId/proposals/:prNumber" element={<ProposalView viewer={viewer} onChange={() => setCatalogVersion(v => v + 1)} />} />
</Routes>
</main>
</ProjectLayout>
@@ -403,6 +414,28 @@ export default function App() {
)
}
// §22 three-tier — corpus redirects mounted under /p/:projectId/*.
// C3.7: the project landing skips the (single-collection) directory and lands in
// the project's default collection.
function DefaultCollectionRedirect() {
const { projectId } = useParams()
return <Navigate to={`/p/${projectId}/c/${DEFAULT_COLLECTION}/`} replace />
}
// Backcompat for the shipped v0.35.0 corpus URLs that lacked the
// /c/<collection>/ segment: redirect into the default collection, preserving any
// query string (e.g. ?branch=).
function LegacyCorpusRedirect({ kind }) {
const { projectId, slug, prNumber } = useParams()
const { search } = useLocation()
const base = `/p/${projectId}/c/${DEFAULT_COLLECTION}`
let to = `${base}/`
if (kind === 'entry') to = `${base}/e/${slug}`
else if (kind === 'entryPr') to = `${base}/e/${slug}/pr/${prNumber}`
else if (kind === 'proposal') to = `${base}/proposals/${prNumber}`
return <Navigate to={to + (search || '')} replace />
}
function DeploymentLanding() {
// §22.10 + design decision 2 — N=1 lands in the single visible project so
// OHM's "land in the corpus" UX is preserved; the directory appears only