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
| Method | Description | Example |
|---|---|---|
push | Navigate and add to history | router.push(PageHome()) |
replace | Navigate without history entry | router.replace(PageHome()) |
back | Navigate back | router.back() |
forward | Navigate forward | router.forward() |
refresh | Refresh current route | router.refresh() |
prefetch | Prefetch route data | router.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: /dashboardCheck 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());