Scaffold recipe for a new authenticated route: SSR shell, DB-derived types, Redux hydration, view sub-pages. Use when creating a new route or page, adding a feature with a sidebar/main layout, adding view modes to a route, or feature types don't match database.types.ts.
Installs into .claude/skills of the current project.
Are you the author of New Route Scaffold?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/armanisadeghi-new-route-scaffold)
---
name: new-route-scaffold
description: "Scaffold recipe for a new authenticated route: SSR shell, DB-derived types, Redux hydration, view sub-pages. Use when creating a new route or page, adding a feature with a sidebar/main layout, adding view modes to a route, or feature types don't match database.types.ts."
---
# New Route Scaffold — `app/(core)/`
This skill captures the exact workflow used to build the original `/notes` route (now `app/(core)/notes`, since rebuilt as a client workspace — see [route-architecture.md](route-architecture.md)). Follow it for every new authenticated route. **Many decisions require information only Arman has — ask explicitly before assuming anything.**
## Companion files — read at the step that needs them
- **Writing any scaffold file in Phases 1–4 → copy its template from [file-templates.md](file-templates.md).** Every code template (types, `data.ts`, route files, hydrators) lives there, keyed by phase.
- **Route UI not built yet (placeholder `page.tsx`) → read [coming-soon-placeholder.md](coming-soon-placeholder.md).**
- **Full shell implementations, data flow, anti-patterns, mobile, more questions for Arman → [route-architecture.md](route-architecture.md).**
- **A route needs agent-aware context or bindings → invoke `surface-authoring`; this includes agent-native routes, where outside helpers may receive an approved task-appropriate snapshot while native and resident agents remain isolated from ambient inheritance.**
## STOP: Questions to Ask Before Writing a Single Line
Never assume the following — always ask Arman:
1. **Layout shape** — How many panels? Sidebar width? Resizable? What goes in the header area?
2. **View modes** — Does this route have sub-views (like edit/preview/split)? What are they named?
3. **Group-by / filter dimensions** — How is the sidebar list organized? What FK relationships drive grouping?
4. **Data fetch scope** — What fields does the list view need? What triggers a "full" fetch?
5. **Redux slice** — Does one already exist? Should we create one or extend an existing slice?
6. **Existing types** — Do the feature types already exist? Are they derived from the DB or hand-written (likely stale)?
7. **Navigation entry** — Does `features/shell/constants/nav-data.ts` already have an entry? What favicon color/letter (`constants/favicon-route-data.ts`)?
8. **Mobile behavior** — Any mobile-specific overrides beyond standard rules?
**If anything is unclear, stop and ask. Do not invent layout details, field names, or group-by modes.**
---
## Phase 1: Audit & Fix Types
Before touching any route file, verify the feature type file matches `types/database.types.ts`.
### Step 1 — Find the DB Row shape
```bash
# Search for the table definition in the generated types
grep -A 30 '"your_table": {' types/database.types.ts
```
### Step 2 — Compare against the feature type file
Located at `features/[feature]/types.ts`. Hand-written types are almost always missing fields. The canonical pattern:
**Template → copy the canonical `features/[feature]/types.ts` pattern (aliases, working interface, `_CompatCheck` guard, list projection, route enums) from [file-templates.md](file-templates.md) § Phase 1.**
### Step 3 — Update Redux `notes.types.ts` factory functions
The `createBlankNoteRecordFromPartial` and `createAutogeneratedNoteRecord` functions hardcode default values for every field. After adding fields to the interface, update both factories to include the new fields — otherwise TypeScript will error. Key rule: **new nullable fields default to `null`, not `""` or `[]`.**
---
## Phase 2: Server Data Layer
Create `lib/[feature]/data.ts`. This is the **only** place server-side DB queries live for this route.
**Template → copy `lib/[feature]/data.ts` (list seed, single entity, preload helper) from [file-templates.md](file-templates.md) § Phase 2.**
**Critical rules:**
- `import "server-only"` is mandatory — it prevents accidental client import
- Every function must be wrapped in React `cache()` — layout, `generateMetadata`, and page all call the same function; `cache()` collapses them to one DB hit. **`'use cache'` is NOT available** (`cacheComponents` off; build error)
- 🚨 **Never `notFound()` on an empty single-record read** (`authInterrupts` is ON) — under RLS, empty means deleted, missing, denied, or signed out. The single-record read returns `null`; the route renders `<AccessGate token id/>` (live: `app/(core)/lists/[id]/page.tsx`) or refuses with `requireAccess(type, id, level, { forbid: true })`. Read `features/access-gate/FEATURE.md` first
- The preload pattern starts a fetch before an await chain, eliminating waterfalls
---
## Phase 3: Route Files
### File map
```
app/(core)/[feature]/
├── layout.tsx ← static metadata only
├── loading.tsx ← dimension-exact skeleton of the full shell
├── error.tsx ← <ErrorBoundaryView context="[Feature]" /> one-liner
├── page.tsx ← fetches list seed, hydrates Redux, renders shell
└── [id]/
├── layout.tsx ← parallel fetch, generateMetadata, BOTH hydrators, shell
├── loading.tsx ← skeleton of the content area only (not the full shell)
├── error.tsx ← <ErrorBoundaryView context="[Feature] Detail" /> one-liner
├── not-found.tsx ← malformed-URL 404 only; an empty record read renders <AccessGate>
├── page.tsx ← redirect to default view
├── edit/layout.tsx ← titlePrefix sub-layout (Phase 6), one per view that needs its own tab title
├── edit/page.tsx
├── split/page.tsx
├── rich/page.tsx
├── md/page.tsx
├── preview/page.tsx
└── diff/page.tsx
```
### `error.tsx` (route root and `[id]/`)
Every error boundary is a one-liner — **never build error UI inline**.
```typescript
"use client";
import { ErrorBoundaryView } from "@/components/errors/ErrorBoundaryView";
export default function MyFeatureError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
return <ErrorBoundaryView error={error} reset={reset} context="My Feature" />;
}
```
Optional props: `context` (string label for console logs), `homePath` (override home button path, default `"/"`).
`ErrorBoundaryView` shows all users a polished error UI with retry/back/home actions. Admins get a collapsible debug panel with full error details, request context, user context, stack trace, raw JSON dump, and a **"Copy for AI"** button that strips minified chunk URLs and formats a clean Markdown summary for pasting into any AI chat.
---
### `page.tsx` — placeholder for routes under development
**Only when the route's real UI isn't built yet → read [coming-soon-placeholder.md](coming-soon-placeholder.md)** (`ComingSoonPage` usage and the four props you must always set).
---
### `layout.tsx` (route root)
Static metadata only. No data fetching, no children manipulation.
**Template → [file-templates.md](file-templates.md) § Phase 3.**
### `loading.tsx` (route root)
Must mirror the exact dimensions of the shell. Every element has explicit `h-` and `w-`. No content-derived sizing. See [route-architecture.md](route-architecture.md) for the full skeleton example from the notes route.
Key rules:
- Outer container: `h-full flex overflow-hidden` (AppShell `.shell-main` is already full viewport — do not subtract header height)
- Route chrome: `<PageHeader>` — never an in-body faux header bar
- Sidebar: `w-[280px] shrink-0` — must match the real aside exactly
- No spinners — `<Skeleton>` components only
- `[id]/loading.tsx` mirrors the content area only (layout persists across navigation)
### `page.tsx` (route root — index, no item selected)
**Template → [file-templates.md](file-templates.md) § Phase 3** (list seed + hydrator + `Suspense` + empty state).
### `[id]/layout.tsx` — the most important file
This is where both hydrators live and parallel fetching happens.
**Template → [file-templates.md](file-templates.md) § Phase 3** (`generateMetadata`, `preloadNote` + `Promise.all`, `<AccessGate>` on an empty read, both hydrators, shell).
### `[id]/page.tsx` — redirect to default view
**Template → [file-templates.md](file-templates.md) § Phase 3.**
### View sub-pages (`[id]/edit/page.tsx`, etc.)
Sub-pages export no metadata and fetch no data. A view that needs its own tab title gets a sibling `layout.tsx` with `titlePrefix` (Phase 6) — never a page-level `{ title }`.
**Templates → [file-templates.md](file-templates.md) § Phase 3** (the `titlePrefix` sub-layout, the single-panel view, and the split view that passes both panels).
---
## Phase 4: Redux Hydrators
Both live in `features/[feature]/route/`. They render `null` and dispatch synchronously during the first render pass — **not** in `useEffect`.
**Templates → copy `ListHydrator` + `EntityHydrator` from [file-templates.md](file-templates.md) § Phase 4.**
**Why `useRef` and NOT `useEffect`:**
`useEffect` fires after paint — children reading from the store see empty state for one frame and flash. The `useRef` guard dispatches during the render pass, before any child reads.
---
## Phase 5: Shell Components
All shell components are Server Components. Client Component islands are pushed as deep as possible — only the interactive parts get `"use client"`.
**Component tree:**
```
[Feature]Shell (Server) — h-full flex overflow-hidden (+ PageHeader for route chrome)
├── [Feature]Sidebar (Server — w-[NNpx] shrink-0 frame) ← ask Arman for width
│ └── [Feature]SidebarClient (Client — Redux reads, interactions)
└── [Feature]MainArea (Server — flex-1 min-w-0 frame)
├── [Feature]TabBar (Client — open tabs from Redux)
└── {children} per page → [Feature]ViewShell (Server)
├── leftPanel (Client editor island)
└── rightPanel (Client preview island — split mode only)
```
**Key layout rules:**
- Outermost: `h-full overflow-hidden` — never `h-screen`, `h-page`, or `calc(100dvh - header)` on `(core)` routes
- Route chrome: `<PageHeader>` — see `features/shell/components/header/variants/USAGE.md`
- Sidebar: `w-[280px] shrink-0` — ask Arman for the exact width
- Main area: `flex-1 min-w-0 overflow-hidden`
- Scroll regions: `overflow-y-auto` only on the inner list container
- No inline styles unless Arman has explicitly approved (e.g., glass mode testing)
See [route-architecture.md](route-architecture.md) for the `NotesShell` / `NoteViewShell` composition — the pattern to build, not importable code (notes deleted its `shell/` components 2026-06-24, `features/notes/FEATURE.md`).
---
## Phase 6: Metadata Rules
From `app/(core)/_read_first_route_rules/metadata-and-seo.md` (full recipe: the `route-metadata-favicons` skill):
| Location | What to use | Notes |
|---|---|---|
| `layout.tsx` (root) | `createRouteMetadata("/path", { title, description })` | Provides favicon + OG for entire route |
| `[id]/layout.tsx` | `createDynamicRouteMetadata("/path", { title, description })` inside `generateMetadata` | Provides favicon + OG for item detail |
| `[id]/<view>/layout.tsx` | `createDynamicRouteMetadata("/path", { titlePrefix: "View Name", title: entity.name, letter })` (static route: `createRouteMetadata` + `titlePrefix`) | Tab reads `View Name \| Entity — AI Matrx`. A page-level `{ title }` renders `View Name — AI Matrx` and loses the name. No distinct title → no metadata; the parent layout covers favicon/OG. Live: `agents/[id]/run/layout.tsx` |
| New route | Add favicon entry in `constants/favicon-route-data.ts` | Ask Arman for color and letter |
---
## Phase 7: Navigation Entry
Add to `features/shell/constants/nav-data.ts` (label, href, `iconName`, section, color) if not already present; the favicon hex + letter go in `constants/favicon-route-data.ts`. **Ask Arman for all values** — icon, href, section, color (Tailwind name + hex), and favicon letter. Never guess these.
---
## Skeleton Rules (Non-Negotiable)
Every `loading.tsx` must satisfy:
1. **Identical outer container** — same `h-`, `w-`, `flex` classes as the real shell
2. **Exact fixed dimensions** — every skeleton block has explicit `h-` and `w-` (no content-derived sizing)
3. **Mirror structure** — sidebar skeleton has toolbar row + list items; editor skeleton has title + body lines
4. **No spinners for page content** — `<Skeleton>` only (from `@ai-matrx/design-system`)
5. **`[id]/loading.tsx`** covers only the content area, not the full shell (the layout persists)
---
## Checklist Before Asking Arman to Review
- [ ] DB type audit done — `_CompatCheck` added to types file
- [ ] All factory functions updated with new fields
- [ ] `lib/[feature]/data.ts` created with `server-only`, `cache()`, and `preloadNote`
- [ ] `app/(core)/[feature]/layout.tsx` — static metadata only
- [ ] `app/(core)/[feature]/loading.tsx` — exact dimension match to shell
- [ ] `app/(core)/[feature]/error.tsx` — one-liner wrapping `<ErrorBoundaryView context="[Feature]" />`
- [ ] `app/(core)/[feature]/page.tsx` — list seed + both hydrators + Suspense
- [ ] `app/(core)/[feature]/[id]/layout.tsx` — parallel fetch + both hydrators + generateMetadata
- [ ] `app/(core)/[feature]/[id]/loading.tsx` — content-area skeleton only
- [ ] `app/(core)/[feature]/[id]/error.tsx` — one-liner wrapping `<ErrorBoundaryView context="[Feature] Detail" />`, plus `not-found.tsx` (malformed URLs only)
- [ ] Empty single-record read renders `<AccessGate token id/>` — no `notFound()` anywhere in `lib/[feature]/data.ts`
- [ ] `app/(core)/[feature]/[id]/page.tsx` — redirect to default view
- [ ] All view sub-pages created (ask Arman for the list)
- [ ] `features/[feature]/route/` — `ListHydrator` + `EntityHydrator`
- [ ] Shell components: `Shell`, `Sidebar`, `SidebarClient`, `MainArea`, `TabBar`, `ViewShell`, `EditorPlaceholder`
- [ ] `NoteEditorPlaceholder` (or equivalent) clearly marked as stub for later replacement
- [ ] Navigation entry added (or confirmed already present)
- [ ] No `useEffect` in hydrators
- [ ] No `h-screen` or `min-h-screen` anywhere — use `h-dvh` for marketing pages; `h-full` for `(core)` AppShell bodies
- [ ] No inline styles (unless Arman explicitly approves for testing)
- [ ] Lints pass on all new files
---
## Related Skills — Read These First
These skills contain the rules this workflow is built on. When in doubt, they win:
- **`ssr-zero-layout-shift`** — `.claude/skills/ssr-zero-layout-shift/SKILL.md` — server/client component boundaries, Suspense rules, hydration pattern, skeleton design, fixed-dimension containers, CLS prevention (absorbed `nextjs-ssr-architecture` + `nextjs-app-router-expert`)
- **Shell header + page height** — `features/shell/components/header/variants/USAGE.md` — `<PageHeader>`, `h-full`, when `.h-page` applies
- **`redux-selector-rules`** — `.claude/skills/redux-selector-rules/SKILL.md` — selector patterns, curried selector caching, avoiding re-render loops
- **`core-route-headers`** — `.claude/skills/core-route-headers/SKILL.md` — invoke before writing any `(core)` shell or `loading.tsx`: `<PageHeader>` chrome, `h-full overflow-hidden` body, mobile header
- **Route rules** — `app/(core)/_read_first_route_rules/RULES.md` — mandatory, read before every session on this route group
---
## Additional Resources
- Full architecture patterns and route composition examples: [route-architecture.md](route-architecture.md)