diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b790f5..f13d30b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,19 @@ skip versions are the composition of each intervening adjacent release's steps in order — no A-to-B path is pre-computed beyond that. +## 0.52.3 — 2026-06-09 + +**Patch — unmatched routes render a 404 instead of a blank page or a +silent redirect home (no operator action required).** + +- **The top-level catch-all redirected any unknown path to `/`**, and the + nested `/admin/*`, `/p/:projectId/*`, and `/docs/*` route groups had no + catch-all (so an invalid subpath rendered an empty pane). Added a shared + `NotFound` 404 page and wired it into all four route groups, so a bad or + typo'd URL — including a stale `/admin/users/allowlist/graduation`-style + path — now shows a clear "page not found" with a link back to the catalog. + (Client-side 404 UI; the SPA still serves over HTTP 200, as before.) + ## 0.52.2 — 2026-06-09 **Patch — admin sidebar nav links no longer accumulate URL segments (no diff --git a/VERSION b/VERSION index fbb7b19..d572c9b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.52.2 \ No newline at end of file +0.52.3 \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 0a31e1c..7c77605 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "rfc-app-frontend", "private": true, - "version": "0.52.2", + "version": "0.52.3", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.css b/frontend/src/App.css index b8901f5..3b09763 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -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; } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index aabfa33..63e961d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -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() { } /> } /> setCatalogVersion(v => v + 1)} />} /> + } /> } /> - {/* Any other path (incl. the retired bare-slug corpus URLs that - somehow reach the SPA) lands on the deployment landing. */} - } /> + {/* Any unmatched path is a real 404 — surface it rather than + silently bouncing home, so a bad/typo'd URL is visible. */} + } /> {(proposeOpen || proposeParam != null) && viewer && ( @@ -507,6 +509,7 @@ function DocsWithSidebar({ viewer }) { client-side redirect to the first configured spec. */} } /> } /> + } /> diff --git a/frontend/src/components/Admin.jsx b/frontend/src/components/Admin.jsx index a08c504..21fcdd8 100644 --- a/frontend/src/components/Admin.jsx +++ b/frontend/src/components/Admin.jsx @@ -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 && } />} } /> } /> + } /> diff --git a/frontend/src/components/NotFound.jsx b/frontend/src/components/NotFound.jsx new file mode 100644 index 0000000..7a3c239 --- /dev/null +++ b/frontend/src/components/NotFound.jsx @@ -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 ( +
+

404

+

+ {message || "We couldn't find that page."} +

+

+ Return to the catalog +

+
+ ) +}