Create New Page
Step-by-step guide for creating a new page with declarative routing
Overview
This guide walks you through creating a new page with declarative routing. Pages in this application use a type-safe routing system that generates route helpers, providing autocomplete and compile-time safety.
Declarative routing eliminates hardcoded paths and provides type-safe navigation throughout the application.
Prerequisites
Before creating a page, determine:
Step 1: Route Path
The URL path for the page (e.g., /dashboard/analytics, /settings/billing)
Step 2: Page Name
A descriptive name in PascalCase (e.g., PageDashboardAnalytics, PageSettingsBilling)
Step 3: Route Params
Any dynamic segments (e.g., [id], [slug]) - Optional
Step 4: Search Params
Query parameters (e.g., ?q=search, ?page=2) - Optional
Directory Structure
Pages are created in the Next.js app directory:
src/app/[locale]/<path>/
├── page.tsx # Page component
└── page.info.ts # Route configurationStep 1: Create Page File
Create src/app/[locale]/<path>/page.tsx:
import { Section } from "@/components/section";
import { H1, P } from "@/components/navigation/common/heading";
import { getTranslations } from "next-intl/server";
export default async function Page() {
const t = await getTranslations("page<PageName>");
return (
<Section variant="main-content">
<div className="space-y-4">
<div>
<H1>{t("heading.title")}</H1>
<P className="text-muted-foreground">
{t("heading.description")}
</P>
</div>
{/* Page content goes here */}
</div>
</Section>
);
}Always use typographic components: Use H1-H6 and P from @/components/navigation/common/heading instead of raw HTML tags.
With Dynamic Route Params
For dynamic routes like /users/[id]:
import { Section } from "@/components/section";
import { H1 } from "@/components/navigation/common/heading";
import { getTranslations } from "next-intl/server";
type PageProps = {
params: Promise<{
locale: string;
id: string; // Dynamic param
}>;
};
export default async function Page({ params }: PageProps) {
const { id } = await params;
const t = await getTranslations("pageUser");
return (
<Section variant="main-content">
<H1>{t("heading.title", { id })}</H1>
{/* Fetch and display user with id */}
</Section>
);
}With Search Params
For pages with query parameters:
import { Section } from "@/components/section";
import { H1 } from "@/components/navigation/common/heading";
import { getTranslations } from "next-intl/server";
type PageProps = {
searchParams: Promise<{
q?: string;
page?: string;
}>;
};
export default async function Page({ searchParams }: PageProps) {
const { q, page } = await searchParams;
const t = await getTranslations("pageSearch");
return (
<Section variant="main-content">
<H1>{t("heading.title")}</H1>
{/* Use q and page for search/pagination */}
</Section>
);
}Step 2: Create Route Info File
Create src/app/[locale]/<path>/page.info.ts:
CRITICAL REQUIREMENTS:
- Always extend
localeSchemafor params - Always extend
siteSearchParamsfor search params - Search params MUST be optional - Non-optional params cause routing errors
- Search params MUST be
z.string()- Usez.coercefor type transformation
Basic Page (No Dynamic Params)
import { localeSchema } from "@/i18n/routing";
import { siteSearchParams } from "@/lib/utils/schema-utils";
export const Route = {
name: "Page<PageName>" as const,
params: localeSchema, // Always include locale
search: siteSearchParams, // Base search params only
};Page with Dynamic Params
import { localeSchema } from "@/i18n/routing";
import { siteSearchParams } from "@/lib/utils/schema-utils";
import { z } from "zod";
export const Route = {
name: "PageUser" as const,
params: localeSchema.extend({
id: z.string().uuid(), // Dynamic param
}),
search: siteSearchParams,
};Page with Search Params
import { localeSchema } from "@/i18n/routing";
import { siteSearchParams } from "@/lib/utils/schema-utils";
import { z } from "zod";
export const Route = {
name: "PageSearch" as const,
params: localeSchema,
search: siteSearchParams.extend({
q: z.string().optional(), // Search query
page: z.coerce.number().optional(), // Page number (coerced from string)
sort: z.enum(["asc", "desc"]).optional(), // Sort direction
}),
};Type Coercion: URL params are always strings. Use z.coerce.number() or z.coerce.boolean() to transform them.
Page with Both Dynamic and Search Params
import { localeSchema } from "@/i18n/routing";
import { siteSearchParams } from "@/lib/utils/schema-utils";
import { z } from "zod";
export const Route = {
name: "PageProjectTasks" as const,
params: localeSchema.extend({
projectId: z.string().uuid(),
}),
search: siteSearchParams.extend({
status: z.enum(["open", "closed"]).optional(),
assignee: z.string().uuid().optional(),
}),
};Step 3: Add Icon Mapping
In src/routes/config/icons.ts:
import { Page<PageName> } from "@/routes";
export const Icons: () => Record<string, IconKey> = () => ({
// ...existing icons
[Page<PageName>.routeName]: "icon-name", // Choose from IconKey type
});Available icon names are defined in IconKey type. Common icons:
home,dashboard,settings,user,bell,searchfolder,file,image,video,musiccalendar,clock,mail,phonechart,table,list,grid
Step 4: Add Menu Translations
In src/messages/dictionaries/en/menu.json:
{
"links": {
"PageHome": "Home",
"PageDashboard": "Dashboard",
"Page<PageName>": "Page Title" // Add your page
}
}Repeat for other locales:
src/messages/dictionaries/it/menu.json:
{
"links": {
"PageHome": "Home",
"PageDashboard": "Dashboard",
"Page<PageName>": "Titolo Pagina"
}
}Step 5: Add Page Translations
Create src/messages/dictionaries/en/page<PageName>.json:
{
"seo": {
"title": "Page Title",
"description": "Page description for search engines"
},
"heading": {
"title": "Page Title",
"description": "Subtitle or description shown on the page"
}
}For pages with forms or complex content, use this extended structure:
{
"seo": {
"title": "Page Title",
"description": "Page description for SEO"
},
"heading": {
"title": "Page Title",
"description": "Page description"
},
"content": {
"section1": {
"title": "Section 1",
"description": "Section 1 description"
}
},
"actions": {
"create": "Create New",
"edit": "Edit",
"delete": "Delete"
}
}Repeat for other locales (e.g., it/page<PageName>.json).
Step 6: Add to Navigation (Optional)
If the page should appear in navigation menus, add it to src/routes/routing.ts:
Header Navigation (Public Pages)
export const Header = {
links: [
PageHome(),
PageAbout(),
Page<PageName>(), // Add here
],
};Dashboard Sidebar (Authenticated Pages)
export const Dashboard = {
sections: [
{
items: [
PageDashboard(),
Page<PageName>(), // Add here
],
},
],
};Footer Links
export const Footer = {
sections: [
{
title: "Product",
links: [
PageFeatures(),
Page<PageName>(), // Add here
],
},
],
};Step 7: Rebuild Routes
Run the route builder to generate type-safe helpers:
npm run dr:buildThis generates:
- Route functions:
Page<PageName>() - Link components:
<Page<PageName>.Link> - Type definitions for params and search params
Always run dr:build after:
- Creating new pages
- Modifying
page.info.tsfiles - Changing route structure
Usage Examples
As a Link Component
import { Page<PageName> } from "@/routes";
// Basic link
<Page<PageName>.Link>
Go to Page
</Page<PageName>.Link>
// With custom className
<Page<PageName>.Link className="text-blue-500">
Go to Page
</Page<PageName>.Link>
// With search params
<Page<PageName>.Link search={{ q: "hello", page: 2 }}>
Search Results
</Page<PageName>.Link>As a URL String
import { Page<PageName> } from "@/routes";
// Get URL string
const url = Page<PageName>();
// With params (for dynamic routes)
const url = PageUser({ id: "123" });
// With search params
const url = PageSearch({ q: "hello" });
// With both
const url = PageProjectTasks(
{ projectId: "abc" },
{ status: "open" }
);Programmatic Navigation
"use client";
import { Page<PageName> } from "@/routes";
import { useRouter } from "next/navigation";
function MyComponent() {
const router = useRouter();
const handleClick = () => {
router.push(Page<PageName>());
};
return <button onClick={handleClick}>Go to Page</button>;
}Getting Current Route Info
"use client";
import { Page<PageName> } from "@/routes";
import { usePathname } from "next/navigation";
function MyComponent() {
const pathname = usePathname();
const isActive = pathname === Page<PageName>();
return (
<Page<PageName>.Link className={isActive ? "font-bold" : ""}>
Page Link
</Page<PageName>.Link>
);
}Advanced: Protected Pages
Server-Side Protection
Use layout authentication:
// src/app/[locale]/dashboard/layout.tsx
import { auth } from "@/features/auth/helpers";
import { redirect } from "next/navigation";
import { PageSignIn } from "@/routes";
export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
const session = await auth();
if (!session) {
redirect(PageSignIn());
}
return <>{children}</>;
}Client-Side Protection
"use client";
import { useSession } from "@/features/auth/hooks";
import { PageSignIn } from "@/routes";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function ProtectedPage() {
const { session, loading } = useSession();
const router = useRouter();
useEffect(() => {
if (!loading && !session) {
router.push(PageSignIn());
}
}, [session, loading, router]);
if (loading) return <div>Loading...</div>;
if (!session) return null;
return <div>Protected Content</div>;
}Post-Creation Checklist
-
Files Created
-
page.tsxcreated with proper structure -
page.info.tscreated with route configuration -
localeSchemaextended in params -
siteSearchParamsextended in search (if needed) - Search params are optional strings
-
-
Build & Configuration
- Run
npm run dr:buildsuccessfully - No TypeScript errors in generated routes
- Icon added to
icons.ts
- Run
-
Translations
- Menu translation added for all locales
- Page translations created for all locales
- Translation keys tested in browser
-
Navigation
- Added to header/sidebar/footer (if needed)
- Links work correctly
- Active states work
-
Testing
- Page loads without errors
- Route params work (if dynamic)
- Search params work (if used)
- SEO metadata correct
- Mobile responsive
Common Patterns
List Page with Filtering
type PageProps = {
searchParams: Promise<{
q?: string;
category?: string;
page?: string;
}>;
};
export default async function Page({ searchParams }: PageProps) {
const { q, category, page } = await searchParams;
const currentPage = page ? parseInt(page) : 1;
// Fetch filtered data...
return (
<Section variant="main-content">
{/* Search UI */}
{/* Results */}
{/* Pagination */}
</Section>
);
}Detail Page with Back Button
import { PageList } from "@/routes";
type PageProps = {
params: Promise<{ id: string }>;
};
export default async function Page({ params }: PageProps) {
const { id } = await params;
return (
<Section variant="main-content">
<PageList.Link className="mb-4 inline-flex items-center">
← Back to List
</PageList.Link>
{/* Detail content */}
</Section>
);
}Real-World Examples
See these pages for reference:
- src/app/[locale]/page.tsx - Home page
- src/app/[locale]/dashboard/page.tsx - Dashboard
- src/app/[locale]/dashboard/api-keys/page.tsx - List with CRUD
Page created! Your page is now accessible via type-safe routing throughout the application.