Documentation
Documentation
Introduction

Getting Started

Getting StartedInstallationQuick StartProject Structure

Architecture

Architecture OverviewTech StacktRPC MiddlewareDesign Principles

Patterns

Code Patterns & ConventionsFeature ModulesError HandlingType Safety

Database

DatabaseSchema DefinitionDatabase OperationsMigrationsCaching

API

tRPCProceduresRouterstRPC Proxy Setup
APIsOpenAPIREST Endpoints

Auth & Access

AuthenticationConfigurationOAuth ProvidersRolesSession Management
AuthorizationUser RolesPermissions

Routing & i18n

RoutingDeclarative RoutingNavigation
InternationalizationTranslationsLocale Routing

Components & UI

ComponentsButtonsFormsNavigationDialogs
StylesTailwind CSSThemingTypography

Storage

StorageConfigurationUsageBuckets

Configuration

ConfigurationEnvironment VariablesFeature Flags

Templates

Template GuidesCreate New FeatureCreate New PageCreate Database TableCreate tRPC RouterAdd Translations

Development

DevelopmentCommandsAI AgentsBest Practices

Navigation

Navigation menus and links

Overview

All navigation utilities automatically handle locale prefixing and work seamlessly with declarative routes. The locale parameter is optional and defaults to the current locale.

Link Components

Basic Link

import { PageAbout } from "@/routes";

// Using declarative route
<PageAbout.Link>About Us</PageAbout.Link>

// Using Link component
import { Link } from "@/i18n/navigation";
<Link href={PageAbout()}>About Us</Link>

Link with Parameters

import { PageUserId } from "@/routes";

<PageUserId.Link id={user.id}>View Profile</PageUserId.Link>

Link with Search Params

import { PageSearch } from "@/routes";

<PageSearch.Link search={{ q: "hello", page: 1 }}>
  Search Results
</PageSearch.Link>

External Links

For external links, use the standard next/link:

import Link from "next/link";

<Link href="https://example.com" target="_blank" rel="noopener">
  External Link
</Link>

Client-Side Navigation

useRouter Hook

"use client";

import { useRouter } from "@/i18n/navigation";
import { PageDashboard, PageUserId } from "@/routes";

export function NavigationExample() {
  const router = useRouter();

  const handleNavigate = () => {
    // Navigate to a route - locale is automatically applied
    router.push(PageDashboard());
    
    // With params
    router.push(PageUserId({ id: "123" }));
    
    // Replace instead of push
    router.replace(PageHome());
  };

  return <Button onClick={handleNavigate}>Go to Dashboard</Button>;
}

Router Methods

MethodDescriptionExample
pushNavigate and add to historyrouter.push(PageHome())
replaceNavigate without history entryrouter.replace(PageHome())
backNavigate backrouter.back()
forwardNavigate forwardrouter.forward()
refreshRefresh current routerouter.refresh()
prefetchPrefetch route datarouter.prefetch(PageAbout())

Server-Side Redirects

The redirect() function only works in:

  • Server Components
  • Server Actions

It does NOT work in:

  • tRPC procedures
  • Middleware & API Routes (use NextResponse.redirect() instead)

In Server Components

import { redirect } from "@/i18n/navigation";
import { PageLogin } from "@/routes";
import { getAuth } from "@/lib/auth/server";

export default async function ProtectedPage() {
  const auth = await getAuth();

  if (!auth) {
    redirect(PageLogin()); // Locale automatically applied
  }

  return <div>Protected content</div>;
}

In Server Actions

"use server";

import { redirect } from "@/i18n/navigation";
import { PageDashboard } from "@/routes";

export async function createProject(data: FormData) {
  const project = await db.projects.create(/* ... */);
  redirect(PageDashboard());
}

In tRPC Procedures

For tRPC, return the redirect URL and handle it client-side:

// Server: tRPC procedure
login: publicProcedure
  .input(loginSchema)
  .mutation(async ({ ctx, input }) => {
    const result = await signIn(input);

    return {
      success: true,
      payload: {
        redirectUrl: PageDashboard(), // Return URL
      },
    };
  }),

// Client: Handle redirect
const mutation = api.auth.login.useMutation({
  onSuccess: (data) => {
    if (data.payload?.redirectUrl) {
      router.push(data.payload.redirectUrl);
    }
  },
});

Pathname Utilities

usePathname Hook

"use client";

import { usePathname } from "@/i18n/navigation";

export function NavigationItem() {
  const pathname = usePathname();
  // Returns: /en/dashboard (includes locale)

  const isActive = pathname === PageDashboard();

  return (
    <Link 
      href={PageDashboard()} 
      className={isActive ? "active" : ""}
    >
      Dashboard
    </Link>
  );
}

Pathname Without Locale

import { usePathnameWithoutLocale } from "@/routes/hooks";

const pathname = usePathnameWithoutLocale();
// URL: /en/dashboard → pathname: /dashboard

Check if Path Matches Route

import { navUtils } from "@/lib/utils/navigation-utils";
import { usePathname } from "@/i18n/navigation";

const pathname = usePathname();
const isActive = navUtils.pathMatches(pathname, PageDashboard);

Navigation Menus

Header Navigation

Configure in src/routes/routing.ts:

export const routingConfig = {
  Header: (role?: UserRole | null) => [
    { route: PageHome },
    { route: PageAbout },
    { route: PagePricing, group: "product" },
  ],
};

Dashboard Sidebar

Dashboard: (pathname: string): SidebarMenuGroup[] => [
  {
    groupLabel: "",
    menus: [
      {
        ...routeMenu(PageDashboard, { activePathname: pathname }),
        submenus: [],
      },
    ],
  },
  {
    groupLabel: "content",
    menus: [
      {
        ...routeMenu(PageDashboardProjects, { activePathname: pathname }),
        submenus: [],
      },
    ],
  },
],

Footer Navigation

Footer: (t) => [
  // Column 1
  [
    { page: PageHome },
    { page: PageAbout },
  ],
  // Column 2
  [
    { page: PagePrivacyPolicy },
    { page: PageTerms },
  ],
],

Route Icons

Icons are mapped to routes in src/routes/config/icons.ts:

import { PageHome, PageDashboard } from "@/routes";

export const Icons: () => Record<string, IconKey> = () => ({
  [PageHome.routeName]: "home",
  [PageDashboard.routeName]: "dashboard",
  [PageDashboardApiKeys.routeName]: "key",
});

Get Route Icon

import { getPageIcon } from "@/routes/routing";

const icon = getPageIcon(PageDashboard); // Returns "dashboard"

Use in Component

import { Icon } from "@/components/ui/icon";
import { getPageIcon } from "@/routes/routing";

<Icon iconKey={getPageIcon(PageDashboard)} />

Menu Translations

Page names for navigation are defined in menu.json:

// src/messages/dictionaries/en/menu.json
{
  "links": {
    "PageHome": "Home",
    "PageDashboard": "Dashboard",
    "PageDashboardApiKeys": "API Keys"
  },
  "groups": {
    "content": "Content",
    "settings": "Settings"
  }
}

Usage

import { useTranslations } from "next-intl";

const t = useTranslations("menu");

// Get page name
const pageName = t(`links.${PageDashboard.routeName}`);

// Get group name
const groupName = t("groups.content");

Active Navigation State

Highlight Active Link

"use client";

import { usePathname } from "@/i18n/navigation";
import { PageDashboard } from "@/routes";
import { cn } from "@/lib/utils";

export function NavItem() {
  const pathname = usePathname();
  const isActive = pathname === PageDashboard();

  return (
    <PageDashboard.Link
      className={cn(
        "nav-link",
        isActive && "nav-link-active"
      )}
    >
      Dashboard
    </PageDashboard.Link>
  );
}

Match with Params

import { extractParamsFromRoute } from "@/routes/makeRoute";

const params = extractParamsFromRoute(pathname, PageUserId);
const isActive = !!params?.id;

Programmatic Navigation Examples

Navigate After Form Submit

"use client";

import { useRouter } from "@/i18n/navigation";
import { PageDashboard } from "@/routes";

export function CreateForm() {
  const router = useRouter();
  const mutation = api.projects.create.useMutation({
    onSuccess: () => {
      router.push(PageDashboard());
    },
  });

  return <form onSubmit={mutation.mutate}>...</form>;
}

Conditional Navigation

const handleClick = () => {
  if (user.isAdmin) {
    router.push(PageAdmin());
  } else {
    router.push(PageDashboard());
  }
};

Navigation with Query Params

router.push(PageSearch({}, { q: searchQuery, page: 1 }));

Best Practices

1. Use Declarative Routes

// ✅ Good
<PageDashboard.Link>Dashboard</PageDashboard.Link>

// ❌ Bad
<Link href="/dashboard">Dashboard</Link>

2. Handle Navigation Errors

const handleNavigate = () => {
  try {
    router.push(PageDashboard());
  } catch (error) {
    console.error("Navigation failed", error);
  }
};

3. Prefetch Important Routes

useEffect(() => {
  router.prefetch(PageDashboard());
}, [router]);

4. Use replace for Redirects

// Use replace to avoid adding to history
router.replace(PageLogin());

Next Steps

Declarative Routing

i18n Configuration

Protected Routes

On this page

Overview
Link Components
Basic Link
Link with Parameters
Link with Search Params
External Links
Client-Side Navigation
useRouter Hook
Router Methods
Server-Side Redirects
In Server Components
In Server Actions
In tRPC Procedures
Pathname Utilities
usePathname Hook
Pathname Without Locale
Check if Path Matches Route
Navigation Menus
Header Navigation
Dashboard Sidebar
Footer Navigation
Route Icons
Get Route Icon
Use in Component
Menu Translations
Usage
Active Navigation State
Highlight Active Link
Match with Params
Programmatic Navigation Examples
Navigate After Form Submit
Conditional Navigation
Navigation with Query Params
Best Practices
1. Use Declarative Routes
2. Handle Navigation Errors
3. Prefetch Important Routes
4. Use replace for Redirects
Next Steps