- Introduction
- Pitch
- Hogwarts
- Live Demo
- MVP
- Roadmap
- Launch Sprint
- PRD
- Get Started
- Localhost
- Architecture
- Structure
- Pattern
- Page
- Layout
- Content
- Types
- Config
- Actions
- Queries
- Authorization
- Validation
- Form
- Table
- Detail
- Card
- Util
- Hooks
- List Params
- Views
- README.md
- ISSUE.md
- Technology Stack
- Database
- File
- CDN Assets
- Entry Points
- Dashboard
- Authentication
- Credentials
- OAuth
- Flow Diagrams
- Multi-Tenancy
- Offline
- Onboarding
- Onboarding Videos
- Add Values
- Admission
- Application
- Attendance
- Compliance
- Profile
- Exams
- Exam Wizard
- Timetable
- Classrooms
- Notifications
- Conference
- LMS (Lumos)
Finance
- Finance
- Fee Management
- Invoice
- Wallet
- Salary
- Payroll
- Timesheet
- Expenses
- Budget
- Receipt
- Accounts
- Banking
- Reports
- Dashboard
- Permissions
- Messages
- Integration Flow
- Provision
- AI Document Processing
- Document Intelligence
- Internationalization
- Translation
- Translation Guide
- Icons
- Docs Factory
- Inspiration
- Listings
- Teachers
- Students
- Catalog
- Library
- Contributing
- Code of conduct
- GitHub Workflow
- Database Seeds
- Database Safety
- Test Accounts
- Playwright
- Prettier
- Block Rebound
Sales & GTM
page.tsx is the route entry. It does not own logic — auth() + getTenantContext() (when needed) + render <XxxContent /> from the matching feature directory.
Rules
| # | Rule |
|---|---|
| 1 | export default async function Page() — never arrow, never feature-named |
| 2 | params and searchParams are Promise<...> in Next.js 16 — await them |
| 3 | Import from @/components/<feature>/content (mirror pattern) |
| 4 | Parallelise multiple awaits with Promise.all |
| 5 | Export metadata or generateMetadata |
| 6 | import type for Metadata, Locale |
| 7 | No inline data fetching — move to queries.ts |
| 8 | No inline try/catch — error.tsx boundaries handle exceptions |
Canonical example
import type { Metadata } from "next"
import { getDictionary } from "@/components/internationalization/dictionaries"
import type { Locale } from "@/components/internationalization/dictionaries"
import StudentsContent from "@/components/school-dashboard/listings/students/content"
export const metadata: Metadata = { title: "Students" }
interface Props {
params: Promise<{ lang: Locale }>
searchParams: Promise<Record<string, string | string[] | undefined>>
}
export default async function Page({ params, searchParams }: Props) {
const { lang } = await params
const dictionary = await getDictionary(lang)
return (
<StudentsContent
dictionary={dictionary.school.students}
lang={lang}
searchParams={searchParams}
/>
)
}Variants
| Variant | When |
|---|---|
| Server page with content delegate | Default for every dashboard route |
generateMetadata | Title needs runtime data (entity name, locale) |
| Static page | No params, no auth (marketing landing) |
| Auth page | Pre-tenant flows (login, register, reset) |
| Parallel data fetch | Multiple awaits — wrap in Promise.all |
Parallel fetching
const [dictionary, school, applicants] = await Promise.all([
getDictionary(lang),
getSchoolById(id),
getApplicantList(id),
])Anti-patterns
- Arrow function exports — use named function declarations.
- Inline data fetching — move to
queries.tsconsumed bycontent.tsx. - Re-fetching what the layout provides.
- Wrapping the body in
<Suspense>— useloading.tsx. - Loading the entire dictionary — pass the slice (
dictionary.school.students). - Different types for
generateMetadatavsPage— share thePropsinterface. - Inline try/catch — let
error.tsxhandle exceptions. - Using Next.js's
PagePropshelper — defineinterface Propsper page.
Naming
- File:
page.tsx. - Function:
Page. NeverLoginPage,Students,DashboardPage. - Default export, async by default.