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

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 configuration

Step 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 localeSchema for params
  • Always extend siteSearchParams for search params
  • Search params MUST be optional - Non-optional params cause routing errors
  • Search params MUST be z.string() - Use z.coerce for 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, search
  • folder, file, image, video, music
  • calendar, clock, mail, phone
  • chart, 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:build

This 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.ts files
  • 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

  1. Files Created

    • page.tsx created with proper structure
    • page.info.ts created with route configuration
    • localeSchema extended in params
    • siteSearchParams extended in search (if needed)
    • Search params are optional strings
  2. Build & Configuration

    • Run npm run dr:build successfully
    • No TypeScript errors in generated routes
    • Icon added to icons.ts
  3. Translations

    • Menu translation added for all locales
    • Page translations created for all locales
    • Translation keys tested in browser
  4. Navigation

    • Added to header/sidebar/footer (if needed)
    • Links work correctly
    • Active states work
  5. 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.

On this page

Overview
Prerequisites
Step 1: Route Path
Step 2: Page Name
Step 3: Route Params
Step 4: Search Params
Directory Structure
Step 1: Create Page File
With Dynamic Route Params
With Search Params
Step 2: Create Route Info File
Basic Page (No Dynamic Params)
Page with Dynamic Params
Page with Search Params
Page with Both Dynamic and Search Params
Step 3: Add Icon Mapping
Step 4: Add Menu Translations
Step 5: Add Page Translations
Step 6: Add to Navigation (Optional)
Header Navigation (Public Pages)
Dashboard Sidebar (Authenticated Pages)
Footer Links
Step 7: Rebuild Routes
Usage Examples
As a Link Component
As a URL String
Programmatic Navigation
Getting Current Route Info
Advanced: Protected Pages
Server-Side Protection
Client-Side Protection
Post-Creation Checklist
Common Patterns
List Page with Filtering
Detail Page with Back Button
Real-World Examples