Documentation
Documentation
Introduction

Getting Started

Getting started
Getting StartedInstallationQuick StartProject Structure

Configuration

Configuration
ConfigurationEnvironment ConfigurationEdge ConfigDatabaseAuth SecretStripeFirebaseStorageGoogle Maps And Cloud Service AccountOAuth ProvidersEmail DeliverySentryFeature Flags

Architecture

Architecture
Architecture OverviewTech StackoRPC MiddlewareDesign Principles

Patterns

Patterns
Code Patterns & ConventionsFeature ModulesError HandlingType Safety

Database

Database
DatabaseSetupSchema DefinitionDatabase OperationsMigrationsCaching
Data Tables

API

oRPCProceduresRoutersoRPC Proxy Setup
APIsOpenAPIREST Endpoints

Auth & Access

AuthenticationConfigurationOAuth ProvidersRolesSession Management
AuthorizationUser RolesPermissions

Routing & i18n

RoutingDeclarative RoutingNavigation
InternationalizationTranslationsLocale Routing

Components & UI

ComponentsButtonsFormsNavigationDialogs
StylesTailwind CSSThemingTypography

Storage

Storage
StorageConfigurationUsageBuckets
Stripe Billing

Extra

Caching

Templates

Templates
Template GuidesCreate New FeatureCreate New PageCreate Database TableCreate oRPC RouterAdd Translations

Development

Development
DevelopmentCommandsAI AgentsBest Practices
Pulling Updates

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 configuration

Step 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 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

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

  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
    • Added to Routing.Sitemap() if page should be indexed
    • 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

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.

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
Step 8: Expose Page in Sitemap (if indexable)
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
Single Section Page (Dashboard/Settings)
Multiple Section Page (Home/Marketing)
Centered Form Page (Auth)
List Page with Filtering
Detail Page with Back Button
Real-World Examples
Layouts (wrap with main-content)
Pages (multiple sections)