Recipes

Admin navigation restructure: 17 sidebar items to 10 flat items with per-feature Settings tabs

Discovered from README cross-links · today

Software & DevOpsWork & Operations

Collapsed a bloated admin sidebar into ten items with section tabs, URLs

What they did

The admin area had grown to 17 sidebar items under four group headings, a hamburger sheet plus a tab bar on phones, and one /admin/settings page with 16 sections in six groups. Finding a feature's controls meant knowing whether they lived on the feature's page or somewhere on Settings. Working from a usability audit, the author planned a restructure and executed it as thirteen tasks on separate worktrees by implementer agents, each with a task-scoped review (spec compliance plus code quality), fix rounds where a review found an Important issue, merges as the union of both sides, a whole-branch review at the end and one fix wave with a scoped re-review. Per-task commits were kept so pieces can be cherry-picked or reverted individually. The result: ten flat sidebar items with no group headings (Overview, Assistant, Members, Announcements, Rewind, Discord, AI, Automations, Audit log, Settings), each feature's controls on its own page as a Settings tab, and /admin/settings kept only for what is global (Connections, Billing, System). lib/admin/nav.ts stays the single list, with items carrying children (the tab strip) and also (sibling paths that light the item); the sidebar, mobile tab bar, page titles, command menu and each section's tab strip all read it. URLs are unchanged: sibling routes keep their paths through a route group per section, and each group's layout.tsx is export default sectionLayout("<id>"), which draws SectionTabs from the item's permitted children. Four new pages host the sections that used to sit on Settings, with the same components, LastSaved footers and test ids. Old /admin/settings#<moved id> links redirect to the section's new page. On phones there is now one menu: the hamburger sheet is gone and the tab bar is Overview, Members, Rewind, Automations, More. The command menu lists Page › Tab entries, and the Support role sees only its permitted items.

How it went

The restructure landed as a single PR with typecheck, lint (0 errors), design-system --strict, knip, env:docs --check and mcp:docs --check all passing on the final head. Full jest had only two failures that fail identically on main. Playwright runs in CI. Visual baselines will drift (sidebar, phone chrome, Settings nav) and the advisory Visual Diffs jobs will show it, with the Visual Baselines workflow refreshing them. Four follow-ups were filed rather than fixed in this PR: client-side navigation away from a dirty stand-alone section drops the draft without a prompt (#515), the command menu matches one substring on the label so old names no longer match (#516), navFor rewrites a gated item's href to the first permitted tab which could fail to light an item for a hypothetical role (#517), and pre-existing broken links in Support's user list and the Overview Services tile (#518).

Worth knowing

Layouts are not authorisation boundaries: every page keeps its own assertAdmin()/permission check, so the route groups must not be treated as access control. A .claude/skills path token changed and CLAUDE.md asks for a /security-review of any such change; two reviews inspected it as prose only, but no separate /security-review has run and that is left to the reviewer to run or waive before merge. Draft protection is incomplete: beforeunload covers reload and close only, not tab or sidebar switches. The command menu's single-substring matching means multi-word queries like "automations request" find nothing. Visual baselines need refreshing after merge.

Wiring

lib/admin/nav.ts is the single nav list: items carry children (the tab strip) and also (sibling paths that light the item). The sidebar, mobile tab bar, page titles, command menu and each section's tab strip all read it. Sibling routes keep their paths through a route group per section (app/admin/(members)/, (rewind)/, (ai)/, (automations)/, and the discord/ segment); each group's layout.tsx is export default sectionLayout("<id>"), which draws SectionTabs from the item's permitted children, and detail routes such as /admin/users/[userId] render no strip. Layouts are not authorisation boundaries — every page keeps its own assertAdmin()/permission check. The four new pages (/admin/rewind/settings, /admin/discord/settings, /admin/ai/settings, /admin/request-limits) reuse the same components, LastSaved footers and test ids, and the actions that save those sections revalidate the new pages. Old /admin/settings#<moved id> links redirect to the section's new page. The nav provider keeps the per-role href and permitted children the server computed, so a Support user's folded item opens its first permitted tab. settingsSection(id) is the only way to link to a settings section, and it ignores keys inherited from Object. A shared useLeaveGuard keeps the reload/close prompt for sections that no longer sit inside the Settings layout. Every section tab has a loading.tsx. CLAUDE.md gains an Admin navigation subsection with the rules: a feature's controls live on the feature's page, the sidebar budget is ten items, and settingsSection(id) is the only way to link to a settings section.

Try it yourself

Restructure the admin navigation in this repo. Read the current sidebar definition and the /admin/settings page first. Then: (1) collapse the sidebar to at most ten flat items with no group headings; (2) keep one nav list module as the single source of truth, where each item carries its tab children and any sibling paths that should light it, and have the sidebar, mobile tab bar, page titles, command menu and section tab strips all read from it; (3) move each feature's settings sections onto that feature's own page as tabs, leaving /admin/settings for global concerns only; (4) keep every existing URL unchanged by using a route group per section whose layout renders the tab strip from the item's permitted children, and render no strip on detail routes; (5) redirect old /admin/settings#<section> links to the new pages; (6) replace the phone hamburger sheet with a single tab bar capped at four link tabs plus a More entry; (7) make the command menu list Page › Tab entries; (8) add a loading.tsx per section tab; (9) keep every page's own permission check — layouts are not authorisation boundaries. Run typecheck, lint and the test suite, and list any follow-ups you deliberately leave out of scope.

Source

Tools in this stack