Frontend: branded 404 page for unknown routes (preserve TimeCurve referral paths) #223

Closed
opened 2026-05-20 04:19:08 +00:00 by PlasticDigits · 3 comments
PlasticDigits commented 2026-05-20 04:19:08 +00:00 (Migrated from gitlab.com)

Summary

Unknown client routes currently mount RootLayout with an empty <Outlet /> — no dedicated not-found UI. Add a branded 404 page and a catch-all route, without breaking referral link shapes or static TimeCurve sub-routes.

Current behavior

  • ShellRoutes in frontend/src/app/LaunchGate.tsx registers explicit routes only; there is no path="*" fallback after launch / in no-env builds.
  • Pre-launch CountdownRoutes already uses path="*" for the countdown shell — out of scope except to avoid regressions when VITE_LAUNCH_TIMESTAMP is set.
  • TimeCurveSubnav intentionally links bare /timecurve (not a path that 404s) — keep that contract.

Referral / TimeCurve routing (must preserve)

Product spec: docs/product/referrals.md.

URL shape Expected behavior
?ref={code} on any matched route Pending capture via applyReferralUrlCapture + ReferralPathSync in RootLayout — must still run on 404 when query is valid
/timecurve/{slug} Valid slug (3–16 a-z0-9, not reserved) → TimeCurve Simple + path capture — not 404
/timecurve/arena, /timecurve/protocol Static routes (registered before timecurve/:timecurveSegment) — not 404
Bare /{code} Intentionally not routed (collides with /home, etc.) — should 404, not become referral capture
Reserved slugs referralPathReserved.ts (arena, protocol, home, referrals, …) — no path capture; route should remain the normal product surface or 404 per table below

Implementation references:

Proposed routing matrix (align in PR)

Clarify in implementation + docs which cases get 404 vs existing pages:

  1. 404 — examples: /nope, /timecurve/foo/bar, /referrals/extra, /home/extra, bare /luck777 (no top-level slug route).
  2. Keep current page — all registered surfaces in ROUTES_* / TIMECURVE_ROUTES / SECONDARY_ROUTES.
  3. Design decision — /timecurve/{segment} where segment is not a valid referral slug (too short, bad charset, reserved): today renders Simple without capture; consider 404 vs friendly “invalid referral link” vs redirect to /timecurve (pick one; document in referrals.md).

Catch-all should be last child under RootLayout so explicit routes win.

Acceptance criteria

  • New NotFoundPage (or equivalent) with on-brand copy, primary CTAs (e.g. TimeCurve, Home//home per launch mode, Referrals).
  • path="*" route in ShellRoutes renders the 404 inside RootLayout (header/footer/audio shell unchanged).
  • Valid /timecurve/{referral} and static /timecurve, /timecurve/arena, /timecurve/protocol unchanged.
  • ?ref= capture still works when visiting an unknown path with a valid query (ReferralPathSync remains mounted).
  • Playwright: at least one test that /definitely-not-a-route shows 404 UI; one that /timecurve/{validSlug} does not show 404 (extend navigation.spec.ts or referral e2e).
  • Optional: document.title / meta or role="main" heading for a11y; note SPA hosting (index.html fallback) if deploy config needs an explicit 404 document for non-asset paths.

Out of scope

  • Server/nginx custom HTTP 404 status for unknown paths (SPA still returns 200 + JS — document if needed).
  • Adding bare /{referral} public routes (explicitly rejected in product docs).
  • Reserved slug list / routing hygiene: issue #43 (per referralPathReserved.ts comment)
  • Referrals UX: #64, #85, #91; path capture spec in docs/product/referrals.md
  • Empty /home outlet fix: #199
## Summary Unknown client routes currently mount `RootLayout` with an **empty `<Outlet />`** — no dedicated not-found UI. Add a branded **404 page** and a catch-all route, without breaking referral link shapes or static TimeCurve sub-routes. ## Current behavior - `ShellRoutes` in [`frontend/src/app/LaunchGate.tsx`](frontend/src/app/LaunchGate.tsx) registers explicit routes only; there is **no** `path="*"` fallback after launch / in no-env builds. - Pre-launch [`CountdownRoutes`](frontend/src/app/LaunchGate.tsx) already uses `path="*"` for the countdown shell — out of scope except to avoid regressions when `VITE_LAUNCH_TIMESTAMP` is set. - [`TimeCurveSubnav`](frontend/src/pages/timecurve/TimeCurveSubnav.test.tsx) intentionally links bare `/timecurve` (not a path that 404s) — keep that contract. ## Referral / TimeCurve routing (must preserve) Product spec: [`docs/product/referrals.md`](docs/product/referrals.md#client-link-capture-frontend). | URL shape | Expected behavior | |-----------|-------------------| | `?ref={code}` on **any** matched route | Pending capture via [`applyReferralUrlCapture`](frontend/src/lib/referralStorage.ts) + [`ReferralPathSync`](frontend/src/components/ReferralPathSync.tsx) in `RootLayout` — **must still run on 404** when query is valid | | `/timecurve/{slug}` | Valid slug (3–16 `a-z0-9`, not reserved) → **TimeCurve Simple** + path capture — **not** 404 | | `/timecurve/arena`, `/timecurve/protocol` | Static routes (registered **before** `timecurve/:timecurveSegment`) — **not** 404 | | Bare `/{code}` | **Intentionally not routed** (collides with `/home`, etc.) — should **404**, not become referral capture | | Reserved slugs | [`referralPathReserved.ts`](frontend/src/lib/referralPathReserved.ts) (`arena`, `protocol`, `home`, `referrals`, …) — no path capture; route should remain the normal product surface or 404 per table below | Implementation references: - Path capture: [`extractReferralCodeFromPathname`](frontend/src/lib/referralPathCapture.ts) - Branch router: [`TimeCurveBranchPage`](frontend/src/pages/TimeCurveBranchPage.tsx) (`timecurve/:timecurveSegment` → Simple for unknown segments today) - Tests: [`referralPathCapture.test.ts`](frontend/src/lib/referralPathCapture.test.ts), [`referralPathReserved.test.ts`](frontend/src/lib/referralPathReserved.test.ts) ## Proposed routing matrix (align in PR) Clarify in implementation + docs which cases get **404** vs existing pages: 1. **404** — examples: `/nope`, `/timecurve/foo/bar`, `/referrals/extra`, `/home/extra`, bare `/luck777` (no top-level slug route). 2. **Keep current page** — all registered surfaces in `ROUTES_*` / `TIMECURVE_ROUTES` / `SECONDARY_ROUTES`. 3. **Design decision** — `/timecurve/{segment}` where `segment` is **not** a valid referral slug (too short, bad charset, reserved): today renders Simple **without** capture; consider **404** vs friendly “invalid referral link” vs redirect to `/timecurve` (pick one; document in `referrals.md`). Catch-all should be **last** child under `RootLayout` so explicit routes win. ## Acceptance criteria - [ ] New `NotFoundPage` (or equivalent) with on-brand copy, primary CTAs (e.g. TimeCurve, Home/`/home` per launch mode, Referrals). - [ ] `path="*"` route in `ShellRoutes` renders the 404 inside `RootLayout` (header/footer/audio shell unchanged). - [ ] Valid `/timecurve/{referral}` and static `/timecurve`, `/timecurve/arena`, `/timecurve/protocol` unchanged. - [ ] `?ref=` capture still works when visiting an unknown path with a valid query (ReferralPathSync remains mounted). - [ ] Playwright: at least one test that `/definitely-not-a-route` shows 404 UI; one that `/timecurve/{validSlug}` does **not** show 404 (extend [`navigation.spec.ts`](frontend/e2e/navigation.spec.ts) or referral e2e). - [ ] Optional: `document.title` / `meta` or `role="main"` heading for a11y; note SPA hosting (`index.html` fallback) if deploy config needs an explicit 404 document for non-asset paths. ## Out of scope - Server/nginx custom **HTTP 404** status for unknown paths (SPA still returns 200 + JS — document if needed). - Adding bare `/{referral}` public routes (explicitly rejected in product docs). ## Related - Reserved slug list / routing hygiene: issue #43 (per `referralPathReserved.ts` comment) - Referrals UX: #64, #85, #91; path capture spec in `docs/product/referrals.md` - Empty `/home` outlet fix: #199
PlasticDigits commented 2026-05-20 04:41:57 +00:00 (Migrated from gitlab.com)

mentioned in commit 8fb7290f1e

mentioned in commit 8fb7290f1e2b243ee41d0b6971293fc32fd161ad
PlasticDigits commented 2026-05-20 04:42:09 +00:00 (Migrated from gitlab.com)

Implementation summary (GitLab #223)

Merged to main in 8fb7290 — branded 404 for unknown client routes inside RootLayout.

What changed

  • NotFoundPage (data-testid="not-found-page") with on-brand copy, CTAs (TimeCurve, hub, Referrals), role="main", and document.title Page not found · YieldOmega.
  • ShellRoutes catch-all path="*" registered last so explicit routes win.
  • homeHubPath() helper: / (no-env) vs /home (post-launch) for the hub CTA.
  • Routing preserved: /timecurve, /timecurve/arena, /timecurve/protocol, valid /timecurve/{referral}; invalid /timecurve/{segment} still renders TimeCurve Simple without path capture (unchanged).
  • ?ref= capture still runs on unknown paths via ReferralPathSync.

Docs / invariants

  • INV-FRONTEND-223-NOT-FOUND — docs/testing/invariants-and-business-logic.md#branded-404-catch-all-gitlab-223
  • Product spec — docs/product/referrals.md (Client link capture)
  • Manual QA — docs/testing/manual-qa-checklists.md#manual-qa-issue-223
  • Third-party agents — skills/README.md, guardrails item #54

Automated verification (ran before merge)

  • vitest — launchHubRoute.test.ts
  • npm run build (tsc + vite)
  • Playwright — e2e/navigation.spec.ts, e2e/referral-path.spec.ts (6/6 passed)

Manual verification checklist

  • Open /definitely-not-a-route → 404 heading + shell chrome, not empty outlet
  • /timecurve/abc12 → TimeCurve Simple, no 404
  • /timecurve/arena, /timecurve/protocol, /timecurve unchanged
  • /luck777 → 404 (no bare /{code} route)
  • /definitely-not-a-route?ref=test1 → 404 UI and yieldomega.ref.v1 pending code test1
  • Hub CTA: / on no-env dev build, /home on post-launch build
  • Browser tab title shows Page not found on 404; clears after navigating away

@nullvector_ux — please verify the checklist above on your preferred build (no-env dev and/or post-launch). Leaving the issue open until sign-off.

## Implementation summary (GitLab #223) Merged to `main` in **8fb7290** — branded **404** for unknown client routes inside `RootLayout`. ### What changed - **`NotFoundPage`** (`data-testid="not-found-page"`) with on-brand copy, CTAs (TimeCurve, hub, Referrals), `role="main"`, and `document.title` **Page not found · YieldOmega**. - **`ShellRoutes`** catch-all **`path="*"`** registered **last** so explicit routes win. - **`homeHubPath()`** helper: **`/`** (no-env) vs **`/home`** (post-launch) for the hub CTA. - **Routing preserved:** `/timecurve`, `/timecurve/arena`, `/timecurve/protocol`, valid `/timecurve/{referral}`; invalid `/timecurve/{segment}` still renders **TimeCurve Simple** without path capture (unchanged). - **`?ref=`** capture still runs on unknown paths via **`ReferralPathSync`**. ### Docs / invariants - **`INV-FRONTEND-223-NOT-FOUND`** — docs/testing/invariants-and-business-logic.md#branded-404-catch-all-gitlab-223 - Product spec — docs/product/referrals.md (Client link capture) - Manual QA — docs/testing/manual-qa-checklists.md#manual-qa-issue-223 - Third-party agents — skills/README.md, guardrails item **#54** ### Automated verification (ran before merge) - vitest — launchHubRoute.test.ts - npm run build (tsc + vite) - Playwright — e2e/navigation.spec.ts, e2e/referral-path.spec.ts (6/6 passed) ### Manual verification checklist - [ ] Open `/definitely-not-a-route` → **404** heading + shell chrome, not empty outlet - [ ] `/timecurve/abc12` → TimeCurve Simple, **no** 404 - [ ] `/timecurve/arena`, `/timecurve/protocol`, `/timecurve` unchanged - [ ] `/luck777` → 404 (no bare `/{code}` route) - [ ] `/definitely-not-a-route?ref=test1` → 404 UI **and** `yieldomega.ref.v1` pending code `test1` - [ ] Hub CTA: `/` on no-env dev build, `/home` on post-launch build - [ ] Browser tab title shows **Page not found** on 404; clears after navigating away @nullvector_ux — please verify the checklist above on your preferred build (no-env dev and/or post-launch). Leaving the issue **open** until sign-off.
nullvector_ux commented 2026-05-20 13:52:28 +00:00 (Migrated from gitlab.com)

Manual verification checklist @PlasticDigits

  • Open /definitely-not-a-route → 404 heading + shell chrome, not empty outlet
  • /timecurve/abc12 → TimeCurve Simple, no 404
  • /timecurve/arena, /timecurve/protocol, /timecurve unchanged
  • /luck777 → 404 (no bare /{code} route)
  • /definitely-not-a-route?ref=test1 → 404 UI and yieldomega.ref.v1 pending code test1
  • Hub CTA: / on no-env dev build, /home on post-launch build
  • Browser tab title shows Page not found on 404; clears after navigating away
### Manual verification checklist @PlasticDigits * [x] Open `/definitely-not-a-route` → **404** heading + shell chrome, not empty outlet * [x] `/timecurve/abc12` → TimeCurve Simple, **no** 404 * [x] `/timecurve/arena`, `/timecurve/protocol`, `/timecurve` unchanged * [x] `/luck777` → 404 (no bare `/{code}` route) * [x] `/definitely-not-a-route?ref=test1` → 404 UI **and** `yieldomega.ref.v1` pending code `test1` * [x] Hub CTA: `/` on no-env dev build, `/home` on post-launch build * [x] Browser tab title shows **Page not found** on 404; clears after navigating away
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-05-21 12:01:37 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
code/yieldomega#223
No description provided.