components/l1 → app/ Alignment Spec
Status: Proposed
Scope: apps/web/src/components/l1 (240 files, 75,448 LOC) and apps/web/src/app
Question answered: which parts of components/l1 are route concerns that belong in the App Router tree, and which are components that should stay put.
1. The sorting rule
One test decides every file:
app/answers "what URL am I at?" —components/answers "what does this look like?"
A file belongs in app/ if it reads or writes location, maps a path to a view, or owns chrome that persists across a URL change. Everything else is a component, regardless of how large or important it is.
Today components/l1 contains all four App Router primitives — route table, layout, template, loading — implemented as client components. That is the entire misalignment. Nothing is missing; it is all in the wrong place, so Next.js cannot see it.
2. Current state
2.1 Two routing worlds coexist
World A — already Next-native (the reference pattern).
/domains/[domain]/[view] resolves through domain-route-manifest.ts (server-safe) into domain-route-shell.tsx, which uses next/navigation and drives navigation by URL. This is correct and should be the template for everything else.
World B — SPA (everything else).
Thirteen entry points in app/ return the identical component:
// app/settings/page.tsx, app/admin/page.tsx, app/knowledge/page.tsx,
// app/governance/page.tsx, app/administration/page.tsx, app/customer-zero/page.tsx,
// app/app/page.tsx, app/app/[...segments]/page.tsx,
// app/workspace/[department]/page.tsx, .../work/[module]/page.tsx,
// app/workbench/[department]/page.tsx, .../work/[module]/page.tsx
return <WorkbenchRoute />;
}
WorkbenchRoute → ClientProviders (mounts <BrowserRouter>) → AppShell (ssr: false) → WorkspaceShellNew → ContentRouter. Next.js sees one indistinguishable page at seven distinct URLs. The real routing happens in content-router.tsx reading useLocation().
Consequences: no nested layouts (one layout.tsx exists in the whole app), no loading.tsx/error.tsx, no per-route metadata, no streaming, and all 75k LOC of l1 sits behind a single ssr: false boundary that must resolve before first paint.
2.2 Four registries describe the same route set
content-router.tsx · domain-view-registry.tsx (774) + its hand-synced twin domain-route-manifest.ts · platform-registry.tsx · config/os-shell-registry.ts · app/_lib/workbench-routes.ts
The manifest file's own header — "Keep in sync with domain-view-registry.tsx" — is the cost being paid. The App Router filesystem is the registry; these collapse into it.
2.3 The shim points backwards
shims/next-navigation.ts maps next/navigation → react-router, commented "Used during migration from Next.js to Vite + React Router." The repo has since reversed direction. 37 files import react-router; 16 use next/navigation. The shim now actively holds the codebase in the world it is trying to leave.
3. Classification
3.1 MOVE to app/ — route resolvers (delete on completion)
These are routers wearing component costumes. The App Router replaces them with directories.
| File | LOC | Becomes |
|---|---|---|
workspace/content-router.tsx | 794 | route segments + generateStaticParams |
l1-module-content.tsx | 908 | route segments |
domains/domain-view-registry.tsx | 774 | filesystem routes (retires domain-route-manifest.ts with it) |
workspace/animated-content-router.tsx | 384 | template.tsx — Next remounts templates on navigation, which is precisely the transition semantic this file hand-rolls |
workspace/platform-registry.tsx | 118 | folded into segments |
3.2 PROMOTE to layout.tsx — persistent chrome
Stay as presentational components; get mounted by a layout instead of wrapping their own children. This is what stops sidebars remounting on every navigation.
| File | LOC | Becomes |
|---|---|---|
admin/admin-shell.tsx | 350 | app/admin/layout.tsx — already does isActive(pathname, href) with react-router Link; a mechanical swap to next/link + usePathname |
domains/domain-route-shell.tsx | 214 | app/domains/[domain]/layout.tsx; page.tsx renders only the view |
workspace/workspace-shell-new.tsx | 1436 | split: chrome → app/(workbench)/layout.tsx, nav data → server module, view state → segments |
navigation/global-nav-bar.tsx | 342 | layout-mounted |
workspace/loader-phase1.tsx | 463 | loading.tsx |
3.3 CONVERT to page.tsx — files already named "page" that are not pages
admin/pages/* — 17 files, 3,154 LOC, each 1:1 with a URL that already exists in admin-shell's nav:
actions · admin-today · audit · billing · connectors · governance · iam · knowledge-governance · observability · provisioning · registry · roles · schema · tenancy · tools · usage · users
→ app/admin/<name>/page.tsx, each with its own metadata, loading.tsx, and code-split boundary.
3.4 STAY in components/ — genuine components
All of domains/*/views/* (~45k LOC), workbench/* primitives, views/*, twin/, knowledge/, notifications/, command-palette/, ooda/, surfaces/. These render; they do not route. They move only if the l1/ folder name is retired (§6).
4. Target tree
app/
├── layout.tsx # root: html, providers, fonts
├── (marketing)/ # route group — public
│ ├── layout.tsx # ← legacy-pages/marketing/MarketingShell
│ └── [slug]/page.tsx
├── (workbench)/ # route group — authenticated
│ ├── layout.tsx # ← workspace-shell-new chrome
│ ├── template.tsx # ← animated-content-router
│ ├── loading.tsx # ← loader-phase1
│ ├── error.tsx
│ ├── domains/[domain]/
│ │ ├── layout.tsx # ← domain-route-shell (sidebar persists)
│ │ ├── page.tsx
│ │ └── [view]/page.tsx
│ └── admin/
│ ├── layout.tsx # ← admin-shell
│ └── {users,roles,audit,…}/page.tsx # ← admin/pages/* ×17
└── onboarding/
├── layout.tsx # ← OnboardingShell chrome
└── [step]/page.tsx
The seven duplicate WorkbenchRoute URL families collapse into one (workbench) group.
5. Sequence
Ordered by evidence per unit of risk — each phase proves the pattern before the next inherits it.
| # | Move | Risk | Why first |
|---|---|---|---|
| 1 | admin/pages/* → app/admin/*/page.tsx + admin-shell → layout.tsx | Low | 17 files already 1:1 with URLs; isolated subtree; zero product-surface impact. Proves the pattern end to end. |
| 2 | domain-route-shell → layout.tsx; delete domain-route-manifest.ts | Low | Already next/navigation. Removes the hand-synced registry and stops sidebar remount. |
| 3 | Collapse 7 WorkbenchRoute families into (workbench) | Medium | Redirects from retired paths; decide canonical vocabulary first (§6). |
| 4 | content-router + l1-module-content → segments | High | Only safe once 1–3 have established the layout contract. |
| 5 | Delete shims/next-navigation.ts; convert remaining react-router imports | High | Last. Deleting the shim is the completion signal, not the starting move. |
Phases 1–2 are independently shippable and reversible.
6. Two decisions this surfaces
Route vocabulary. /app, /workspace, /workbench, and /domains currently mount the same shell. CLAUDE.md says "The Workbench is one surface." Four URL families for one surface is naming drift with a routing cost. Recommend /workbench/* canonical, the rest permanent redirects.
The l1/ folder name. CLAUDE.md forbids L1/L2/L4/L7 in user-facing UI. The prohibition is about UI, so the folder is not a violation — but layers/, l1/, l2/, l4/, l7/, and _deprecated/l2-removed/ keep retired vocabulary alive in the paths every contributor reads daily. Moving route concerns out shrinks l1/ naturally; renaming the remainder to components/workbench/ is a follow-on, not part of this work.
7. Definition of done
-
shims/next-navigation.tsdeleted; zeroreact-routerimports inapps/web/src -
<BrowserRouter>removed fromclient-providers.tsx - Every persistent chrome surface is a
layout.tsx; no shell wraps its own children - Route set is derivable from the filesystem alone — no parallel registry
-
ssr: falsecount reduced from 22 to only genuinely browser-only widgets - Each route family has
loading.tsx,error.tsx, andmetadata