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.
Section Component Usage: Layouts use <MainSection> component to wrap all page content. Pages then use multiple <Section variant="..."> components within them for different visual sections (heroes, content blocks, etc.).
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, SectionHeader } from "@/components/section";
import { getTranslations } from "next-intl/server";
export default async function Page() {
const t = await getTranslations("page<PageName>");
return (
<Section variant="default" padding="md">
<SectionHeader
title={t("heading.title")}
description={t("heading.description")}
/>
{/* Page content goes here */}
</Section>
);
}SectionHeader: Use this component for consistent page headers with title, description, and optional CTA buttons. The first section automatically gets reduced padding - no prop needed.
Pass CTA buttons as children to position them inline with the title on desktop and next to the back button on mobile:
<SectionHeader title={t("heading.title")} description={t("heading.description")}>
<ButtonCreate />
</SectionHeader>With Dynamic Route Params
For dynamic routes like /users/[id]:
import { Section, SectionHeader } from "@/components/section";
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="default" padding="md">
<SectionHeader
title={t("heading.title", { id })}
showBackButton
/>
{/* Fetch and display user with id */}
</Section>
);
}With Search Params
For pages with query parameters:
import { Section, SectionHeader } from "@/components/section";
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="default" padding="md">
<SectionHeader
title={t("heading.title")}
description={t("heading.description")}
/>
{/* 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
Step 8: Expose Page in Sitemap (if indexable)
If the page should be indexed by search engines, add it to Routing.Sitemap():
// src/routes/routing.ts
Sitemap: () => [
AllRoutes.PageHome(),
AllRoutes.Page<PageName>(), // add your page
],src/app/sitemap.ts reads from Routing.Sitemap(), so routes not listed there are not emitted in sitemap output by default.
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 - Added to
Routing.Sitemap()if page should be indexed - 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
Single Section Page (Dashboard/Settings)
export default async function Page({ params, searchParams }: PageProps) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "pageDashboard" });
return (
<Section variant="default" padding="md">
<SectionHeader
title={t("heading.title")}
description={t("heading.description")}
actions={<Button>Create New</Button>}
/>
<ListOrganizations />
</Section>
);
}Multiple Section Page (Home/Marketing)
export default async function Page({ params }: Props) {
return (
<>
{/* Hero with wider content */}
<Section variant="breakout" padding="lg">
<HeroHomeMain />
</Section>
{/* Features with background */}
<Section variant="bg-color" padding="lg">
<HeroHomeFeatures />
</Section>
{/* Standard content */}
<Section variant="default" padding="lg">
<HeroHomeSpecs />
</Section>
{/* Showcase with background */}
<Section variant="bg-color" padding="lg">
<HeroHomeShowcase />
</Section>
</>
);
}Pattern: Marketing/landing pages typically use multiple sections with alternating default, bg-color, and breakout variants for visual hierarchy.
Centered Form Page (Auth)
export default async function Page({ params, searchParams }: Props) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "pageLogin" });
return (
<Section variant="center-page" padding="md" className="min-h-[60vh]">
<SectionHeader
title={t("heading.title")}
description={t("heading.description")}
align="center"
/>
<FormLogin />
</Section>
);
}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="default" padding="md">
{/* 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="default" padding="md">
<SectionHeader
title="User Details"
description="View and manage user information"
showBackButton
/>
{/* Detail content */}
</Section>
);
}Real-World Examples
See these files for reference:
Layouts (wrap with main-content)
- src/layouts/layout-site.tsx - Public site layout
- src/layouts/layout-sidebar.tsx - Dashboard sidebar layout
- src/layouts/layout-auth.tsx - Auth pages layout
Pages (multiple sections)
- src/app/[locale]/(site)/page.tsx - Home page (multiple sections)
- src/app/[locale]/(site)/about/page.tsx - About page (bg-color sections)
- src/app/[locale]/(dashboard)/dashboard/page.tsx - Dashboard (default sections)
- src/app/[locale]/(dashboard)/dashboard/organizations/page.tsx - List page
- src/app/[locale]/(site)/(auth)/login/page.tsx - Form page
Page created! Your page is now accessible via type-safe routing throughout the application.