fix(routing): render a 404 page for unmatched routes (v0.52.3)

The top-level catch-all silently redirected unknown paths to "/", and the
nested /admin/*, /p/:projectId/*, and /docs/* route groups had no catch-all
(invalid subpaths rendered a blank pane). Add a shared NotFound component and
wire it into all four route groups so a bad/typo'd URL shows a clear 404 with
a link home. Client-side 404 UI (SPA still serves HTTP 200).

Patch 0.52.2 → 0.52.3; CHANGELOG updated. Caught by the operator on PPE.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-08 20:14:25 -07:00
parent 7bcf784d06
commit 948ee88160
7 changed files with 60 additions and 5 deletions
+16
View File
@@ -2687,3 +2687,19 @@ select:focus-visible,
align-items: flex-start;
gap: 12px;
}
/* 404 / not-found page (client-side, for unmatched routes) */
.not-found {
max-width: 32rem;
margin: 4rem auto;
padding: 0 1.5rem;
text-align: center;
}
.not-found-code {
font-size: 3.5rem;
line-height: 1;
margin: 0 0 0.5rem;
color: var(--c-gray-900, #111);
}
.not-found-message { color: var(--c-gray-700, #444); margin: 0 0 1.25rem; }
.not-found-actions a { color: var(--c-gray-900, #111); text-decoration: underline; }
+6 -3
View File
@@ -7,6 +7,7 @@ import { brandTitle } from './lib/brand'
import { entryPath, proposalPath, DEFAULT_COLLECTION } from './lib/entryPaths'
import { useDeployment } from './context/DeploymentProvider'
import ProjectLayout from './components/ProjectLayout.jsx'
import NotFound from './components/NotFound.jsx'
import Directory from './components/Directory.jsx'
import ProjectSwitcher from './components/ProjectSwitcher.jsx'
import Catalog from './components/Catalog.jsx'
@@ -381,13 +382,14 @@ export default function App() {
<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)} />} />
<Route path="*" element={<NotFound />} />
</Routes>
</main>
</ProjectLayout>
} />
{/* Any other path (incl. the retired bare-slug corpus URLs that
somehow reach the SPA) lands on the deployment landing. */}
<Route path="*" element={<Navigate to="/" replace />} />
{/* Any unmatched path is a real 404 surface it rather than
silently bouncing home, so a bad/typo'd URL is visible. */}
<Route path="*" element={<NotFound />} />
</Routes>
</div>
{(proposeOpen || proposeParam != null) && viewer && (
@@ -507,6 +509,7 @@ function DocsWithSidebar({ viewer }) {
client-side redirect to the first configured spec. */}
<Route path="specs" element={<DocsSpecsIndex />} />
<Route path="specs/:name" element={<DocsSpec />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</main>
+2
View File
@@ -29,6 +29,7 @@ import {
} from '../api.js'
import { EVENTS, track } from '../lib/analytics.js'
import { entryPath, useProjectId } from '../lib/entryPaths'
import NotFound from './NotFound.jsx'
// v0.17.0 roadmap item #16. The max length the backend enforces
// (Pydantic body bound + `invites.CUSTOM_MESSAGE_MAX_LENGTH`); kept
@@ -82,6 +83,7 @@ export default function Admin({ viewer }) {
{isSiteOwner && <Route path="retired" element={<RetiredTab />} />}
<Route path="audit" element={<AuditTab />} />
<Route path="permissions" element={<PermissionsTab />} />
<Route path="*" element={<NotFound message="That admin page doesn't exist." />} />
</Routes>
</div>
</div>
+21
View File
@@ -0,0 +1,21 @@
// Client-side 404 for routes that don't match. An SPA serves index.html
// for every non-API path (HTTP 200), so an unknown URL would otherwise
// render blank or, worse, silently bounce home. This surfaces a clear
// "page not found" instead. Used by the top-level router and by the
// nested /admin, /p/:projectId, and /docs route groups so an invalid
// subpath (e.g. /admin/users/allowlist/graduation) lands here too.
import { Link } from 'react-router-dom'
export default function NotFound({ message }) {
return (
<div className="not-found" role="alert">
<h1 className="not-found-code">404</h1>
<p className="not-found-message">
{message || "We couldn't find that page."}
</p>
<p className="not-found-actions">
<Link to="/">Return to the catalog</Link>
</p>
</div>
)
}