GenerateSaaS

Navigation

Configure the navbar, sidebar, user menu, and section tabs in the Nuxt frontend.

Navigation is plain TypeScript config under apps/web-nuxt/app/config/ (marketing navbar, dashboard sidebar, user menu) plus the shared sectionTabsConfig in @repo/config. Items hide declaratively via enabled, roles, and feature flags, so disabled features and unauthorized links never render.

Config files

Each list is exported from its own file. Edit the typed structures directly.

FileExportRenders
app/config/navbar.tsnavbarItemsMarketing site top nav
app/config/sidebar.tssidebarConfigDashboard sidebar
app/config/user-menu.tsuserMenuItemsAvatar dropdown
@repo/config section-tabs.tssectionTabsConfigSub-tabs in /admin, /settings, /settings/organization

Gating fields

Sidebar items, sidebar categories, and section tabs share these optional gating fields. An item renders only when all present conditions pass.

FieldTypeEffect
requiresSidebarFeatureFlag[]Show only when every listed feature flag is enabled.
roles("user" | "admin")[]Show only for matching app roles (e.g. ["admin"]).
orgRoles("member" | "admin" | "owner")[]Show only for matching organization roles.

SidebarFeatureFlag values: multiTenant, notifications, apiKeys, credits, adminNotifications. Sidebar categories support only requires and roles (no orgRoles). Navbar items gate differently - they use a single enabled boolean (see below).

This config only hides UI. It is not access control - the underlying API routes and pages enforce auth independently. Never rely on a missing nav link to protect a route.

sidebarConfig is an object with navMain, categories, and navSecondary. Each item carries a title translation key, a url, and a directly imported Vue Phosphor icon. The Organization item requires multiTenant; the Admin category requires the admin role.

Active state resolves via match ("exact" | "startsWith", default "startsWith") or activeMatch - a path string that highlights the item on any route below it (e.g. activeMatch: "/settings" keeps Settings active across all settings pages).

// apps/web-nuxt/app/config/sidebar.ts
import { PhGauge, PhBuilding, PhShieldChevron } from "@phosphor-icons/vue";

export const sidebarConfig: SidebarConfig<Component> = {
  navMain: [
    { title: "sidebar.dashboard", url: "/dashboard", icon: PhGauge, match: "exact" },
  ],
  categories: [
    {
      title: "sidebar.categories.account",
      items: [
        // Hidden unless multi-tenancy is enabled
        { title: "sidebar.organization", url: "/settings/organization",
          icon: PhBuilding, requires: ["multiTenant"] },
      ],
    },
    {
      // Whole category is admin-only
      title: "sidebar.categories.admin",
      roles: ["admin"],
      items: [
        { title: "sidebar.admin", url: "/admin/users",
          icon: PhShieldChevron, roles: ["admin"] },
      ],
    },
  ],
  navSecondary: [],
};

Marketing navbarItems gate with enabled (consumers filter on enabled !== false), support subitems dropdowns (optionally laid out as a multi-column grid via columns, 1 | 2 | 3 | 4, default 1), and mark primary: true to surface in the mobile bottom nav. The Blog and Docs links bind enabled to their config flags. A dropdown parent may omit href to act as a pure trigger that only opens its submenu instead of navigating. Mark a link external for hard navigation to a URL served outside the app; external links open in a new tab by default, so set newTab: false to keep one in the same tab.

// apps/web-nuxt/app/config/navbar.ts
{ title: "nav.blog", href: "/blog", icon: PhNewspaper,
  match: "startsWith", enabled: config.blog.enabled },
{ title: "nav.docs", href: config.docs?.url ?? "/docs", icon: PhBookOpen,
  match: "startsWith", enabled: config.docs?.enabled ?? false },

Add columns to an item with subitems to spread its dropdown across multiple columns on desktop, filling left to right; the mobile menu stays single-column. Four subitems with columns: 2 render as a 2x2 grid:

// apps/web-nuxt/app/config/navbar.ts
{ title: "nav.product", href: "/#features", icon: PhStackSimple, columns: 2,
  subitems: [ /* 4 items render as a 2x2 grid */ ] },

Section tabs

The shared sectionTabsConfig drives the horizontal sub-tabs and uses the same gating fields plus match: "exact".

// packages/config/src/section-tabs.ts
admin: [
  { title: "sidebar.users", url: "/admin/users" },
  { title: "sidebar.api_keys", url: "/admin/api-keys", requires: ["apiKeys"] },
],
organization: [
  { title: "sidebar.overview", url: "/settings/organization", match: "exact" },
  { title: "sidebar.activity", url: "/settings/organization-activity",
    orgRoles: ["owner", "admin"] },
],

Icons

Icons are imported directly from @phosphor-icons/vue (Ph* prefix) - there is no string-key registry.

  • Import the Ph* component (e.g. PhGauge) and assign it to icon.
  • Each config file is self-contained: its imports sit at the top, keeping the structure buyer-editable.
  • Browse names at phosphoricons.com.

Labels

title is always a translation key, never raw text - rendered with $t(). Define the key in packages/i18n/translations/en/web.json (e.g. sidebar.users); other locales are generated automatically.

Frequently asked questions

How do I add a sidebar link? Push an object onto a category's items (or navMain) in app/config/sidebar.ts with a title key, a url, and an imported Ph* icon. Add requires, roles, or orgRoles to make it conditional.

Why is a nav item not showing? A gate failed - confirm every flag in requires is enabled and the session has a matching roles/orgRoles entry. For navbar items, check enabled !== false. Disabled-feature links are hidden by design.

Are navbar and sidebar config shared with other frontends? No. navbar.ts, sidebar.ts, and user-menu.ts are per-app under apps/web-nuxt/app/config/. Only sectionTabsConfig is shared via @repo/config.

What's the difference between roles and orgRoles? roles is the global app role; orgRoles is the user's role within their active organization. See Authorization.

On this page