Skip to content

CON-50: route the app views behind a prefix no page slug can produce - #100

Open
molgerz wants to merge 3 commits into
mainfrom
con-50-unproducible-app-route-prefix
Open

molgerz wants to merge 3 commits into
mainfrom
con-50-unproducible-app-route-prefix

Conversation

@molgerz

@molgerz molgerz commented Sep 19, 2026 •

Copy link
Copy Markdown
Owner

Ticket

Kaneo CON-50 — "A page whose slug matches an app route becomes unreachable" (#93)

What

A page titled "Archive", "Search" or "New" normalises to exactly the slugs of the app's own view routes (/s/:group/new, /search, /archive). React Router matches the static segment before :slug, so the page publishes fine, shows up in the tree — and its own URL opens an app view. The slug arrives from the relay (d tag), so refusing those titles in the editor cannot fix it: the fix has to hold without the writer cooperating.

The app views now live behind ~, a segment normalizeSlug can never return (it keeps only letters, numbers, combining marks and -). No slug from any NIP-54 client can shadow a view, and every page title is free again.

Implementation

  • src/routes/space-urls.ts (new): owns APP_VIEW_PREFIX and the URL builders spaceNewUrl (with ?slug= / ?parent=), spaceSearchUrl (with ?q=), spaceArchiveUrl.
  • src/routes/router.tsx: the three view routes are built through the builders (/s/:group/~new, ~search, ~archive), so routes and links cannot drift apart. No redirect routes for the old addresses — a redirect for those words would be a static segment in the page namespace again, the exact thing being removed.
  • Call sites switched to the builders: Sidebar.tsx (4), Topbar.tsx (1), SpaceOverview.tsx (2), PageView.tsx (2). Query params survive unchanged.
  • Docs: routing table and a new "Why the app's views carry ~" section in docs/06-ui-information-architecture.md; the archive address in docs/05-versioning-history.md.
  • vite.config.ts: exclude the local git worktree checkouts (.worktrees/, .claude/worktrees/) from the vitest run — their other branches' tests were being picked up from the repo root and failing the gates for every branch on this machine. No effect on CI.

Acceptance criteria

  • The app's own views are reachable at /s/:group/~new, /s/:group/~search, /s/:group/~archive.
  • A page titled "Archive" / "Search" / "New" resolves to PageView at /s/:group/<slug>.
  • A pinned test proves the invariant: normalizeSlug can never return a string starting with ~ (corpus test over the colliding titles and the shapes that could smuggle the prefix through).
  • All internal links use the shared URL-builder module; query params (?q=, ?parent=, ?slug=) survive.
  • No redirect route for the old URLs inside the page namespace.
  • Routing table and reasoning updated in docs/06; docs/05 archive address corrected.
  • Gates green: npm run typecheck && npm run lint && npm test && npm run build.

Test plan

  • npm run typecheck && npm run lint && npm test && npm run build green
  • space-urls.test.ts: builders produce /s/:group/~new (?slug=, ?parent=), ~search (?q=), ~archive; the invariant corpus never yields a slug starting with ~
  • router.test.ts: pins the view routes as absolute, prefixed literal paths (/s/:group/~new etc.) and resolves both the colliding slugs and the prefixed addresses through the real route table (matchRoutes); page URLs (:slug, :slug/history) are untouched
  • Full existing suite passes with the worktree exclusions (54 files / 595 tests)
  • Manual (browser, relay :8080, app :5273): create a page titled "Archive" — its URL opens the page, not the archive view
  • Manual: sidebar Search/New page/Archive rows, the Topbar search and the space overview's New page and Archive links all land on the ~-prefixed URLs and render the right views
  • Manual: search from the Topbar with a query, and "New subpage" from a page's toolbar, still carry ?q= / ?parent= correctly

Test results

Automated (this session — DeepSeek subagent unavailable, model selection disabled; run on GLM 5.3 Flash):

  • Gates: typecheck ✅, lint ✅, test ✅ (54 files / 595 tests), build ✅ — exit 0, re-verified after the branch was rebuilt.
  • space-urls.test.ts + router.test.ts: 45 tests passed (builders, invariant corpus, route-table resolution through matchRoutes).

Known failure mode found and fixed before review: the first version built the route paths from the bare :group parameter, so they came out relative (:group/~new) and matched nothing — every app view fell through to PageView. The route test now pins the absolute literal paths instead of comparing the builders with themselves, and SPACE_ROUTE_PATTERN in space-urls.ts is the base both the routes and the links are built from.

  • CI on the PR: green.

The three remaining items are browser flows against the running dev setup and are deliberately left to manual testing.

Out of scope

  • Redirects for the old view addresses (would reintroduce a static segment in the page namespace).
  • Reserved-word validation in the editor (cannot fix relay-borne slugs).
  • Hardening against a hostile client publishing a non-NIP-54 d tag such as ~new directly — no compliant client produces one; documented as residual risk in the refinement.
  • CON-11 archive behaviour itself (deliberately separate per the ticket).

The app views and the page slugs share /s/:group. Every app-view path now
starts with ~, which normalizeSlug can never produce: it keeps only letters,
numbers, combining marks and '-', so no slug from any NIP-54 client can
shadow a view or a view a page (CON-50). The corpus test pins the invariant
against the colliding titles and the shapes that could try to smuggle the
prefix through.
A page titled "Archive", "Search" or "New" normalises to exactly the slugs
of the app's own view routes, and React Router matches the static segment
first — the page publishes fine and then its own URL opens an app view.
The slug arrives from the relay, so refusing the titles in the editor
cannot fix it: the fix has to hold without the writer cooperating.

Every app-view path now starts with ~, which normalizeSlug can never
return (it keeps only letters, numbers, combining marks and '-'). The
prefix and the route pattern are owned by space-urls.ts, which builds the
routes and every link to the views, so the two cannot drift apart; the
router test resolves both the colliding slugs and the prefixed addresses
through the real route table, against literal paths. Old view addresses
are gone rather than redirected — a redirect route for those words would
be a static segment in the page namespace again.

Routing table and reasoning updated in docs/06, the archive address in
docs/05. Page URLs are untouched. (CON-50)
@molgerz
molgerz force-pushed the con-50-unproducible-app-route-prefix branch from 3ea7f95 to 087410f Compare September 19, 2026 04:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants