Next.js Pages Router
Structure
This project uses Pages Router exclusively. All pages go in src/pages/.
- Regular pages:
src/pages/[filename].tsx → /filename route
- Dynamic routes:
src/pages/[id]/page.tsx → /[id] route
- API routes:
src/pages/api/[route].ts → /api/[route] endpoint
- Special files:
_app.tsx, _document.tsx in src/pages/
Creating Pages
Basic Page
tsx
1// src/pages/about.tsx
2export default function About() {
3 return <div>About page</div>;
4}
Dynamic Route
tsx
1// src/pages/users/[id].tsx
2import { useRouter } from 'next/router';
3
4export default function User() {
5 const router = useRouter();
6 const { id } = router.query;
7
8 return <div>User {id}</div>;
9}
Nested Dynamic Route
tsx
1// src/pages/posts/[id]/[slug].tsx
2import { useRouter } from 'next/router';
3
4export default function Post() {
5 const router = useRouter();
6 const { id, slug } = router.query;
7
8 return <div>Post {id} - {slug}</div>;
9}
Special Files
_app.tsx
Wraps all pages. Use for:
- Global providers
- Global styles
- Layout components
- Page-level state
tsx
1// src/pages/_app.tsx
2import "@/styles/globals.css";
3import type { AppProps } from "next/app";
4
5export default function App({ Component, pageProps }: AppProps) {
6 return <Component {...pageProps} />;
7}
_document.tsx
Customizes HTML document. Use for:
<html> and <body> attributes
- Custom fonts
- Analytics scripts
- Meta tags
tsx
1// src/pages/_document.tsx
2import { Html, Head, Main, NextScript } from "next/document";
3
4export default function Document() {
5 return (
6 <Html lang="en">
7 <Head />
8 <body className="antialiased">
9 <Main />
10 <NextScript />
11 </body>
12 </Html>
13 );
14}
Data Fetching
CRITICAL: Only pages handle server-side data fetching. Components, hooks, or other files should NOT make server calls directly. All data fetching happens in the page file using getServerSideProps, getStaticProps, or getStaticPaths.
For client-side data (lists, forms, mutations), use custom hooks that call the internal API via actions (src/lib/api/*/actions.ts) and query keys (src/lib/api/*/keys.ts). See the hooks skill. If your project integrates external APIs, see src/lib/TRANSFORMATIONS.md for the full flow.
getServerSideProps
The page component goes first, then the data fetching function. The page component's Props type MUST be inferred from getServerSideProps using InferGetServerSidePropsType.
tsx
1// src/pages/posts/[id].tsx
2import type { GetServerSideProps, InferGetServerSidePropsType } from "next";
3import { PageLayout } from "@/components";
4
5export const getServerSideProps: GetServerSideProps = async (context) => {
6 const { id } = context.params!;
7 const res = await fetch(`https://api.example.com/posts/${id}`);
8 const post = await res.json();
9
10 return {
11 props: { post },
12 };
13};
14
15// Type MUST be inferred from getServerSideProps
16export default function Post({ post }: InferGetServerSidePropsType<typeof getServerSideProps>) {
17 return (
18 <PageLayout
19 title={post.title}
20 description={post.content}
21 canonical={`/posts/${post.id}`}
22 >
23 <div>
24 <h1>{post.title}</h1>
25 <p>{post.content}</p>
26 </div>
27 </PageLayout>
28 );
29}
getStaticProps
The page component's Props type MUST be inferred from getStaticProps using InferGetStaticPropsType.
tsx
1// src/pages/posts/[id].tsx
2import type { GetStaticProps, GetStaticPaths, InferGetStaticPropsType } from "next";
3import { PageLayout } from "@/components";
4
5export const getStaticProps: GetStaticProps = async (context) => {
6 const { id } = context.params!;
7 const res = await fetch(`https://api.example.com/posts/${id}`);
8 const post = await res.json();
9
10 return {
11 props: { post },
12 revalidate: 60, // ISR: revalidate every 60 seconds
13 };
14};
15
16export const getStaticPaths: GetStaticPaths = async () => {
17 return {
18 paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
19 fallback: 'blocking', // or true, false
20 };
21};
22
23// Type MUST be inferred from getStaticProps
24export default function Post({ post }: InferGetStaticPropsType<typeof getStaticProps>) {
25 return (
26 <PageLayout
27 title={post.title}
28 description={post.content}
29 canonical={`/posts/${post.id}`}
30 >
31 <div>
32 <h1>{post.title}</h1>
33 <p>{post.content}</p>
34 </div>
35 </PageLayout>
36 );
37}
Type Inference
IMPORTANT: The page component's Props type MUST be inferred from getServerSideProps or getStaticProps using InferGetServerSidePropsType or InferGetStaticPropsType. Do not manually define the Props type.
tsx
1import type { GetServerSideProps, InferGetServerSidePropsType } from "next";
2
3export const getServerSideProps: GetServerSideProps = async () => {
4 return {
5 props: { post: { id: "1", title: "Hello" } },
6 };
7};
8
9// Type MUST be inferred from getServerSideProps
10export default function Post({ post }: InferGetServerSidePropsType<typeof getServerSideProps>) {
11 return <div>{post.title}</div>;
12}
Using Hooks in Pages
Pages can use custom hooks for client-side data fetching, mutations, or other client-side logic. Examples:
tsx
1// src/pages/users.tsx
2import type { GetServerSideProps, InferGetServerSidePropsType } from "next";
3import { useUsers } from "@/hooks/users";
4
5export const getServerSideProps: GetServerSideProps = async () => {
6 return { props: { initialUsers: [] } };
7};
8
9export default function UsersPage({ initialUsers }: InferGetServerSidePropsType<typeof getServerSideProps>) {
10 const { data: users, isLoading } = useUsers();
11
12 return (
13 <div>
14 {isLoading ? <p>Loading...</p> : <UserList users={users} />}
15 </div>
16 );
17}
tsx
1// src/pages/users/[id].tsx
2import type { GetServerSideProps, InferGetServerSidePropsType } from "next";
3import { useUser, useCreateUser } from "@/hooks/users";
4
5export default function UserPage({ userId }: InferGetServerSidePropsType<typeof getServerSideProps>) {
6 const { data: user } = useUser(userId);
7 const createMutation = useCreateUser();
8
9 const handleCreate = () => {
10 createMutation.mutate({ name: "New User" });
11 };
12
13 return (
14 <div>
15 <h1>{user?.name}</h1>
16 <button onClick={handleCreate}>Create User</button>
17 </div>
18 );
19}
20
21export const getServerSideProps: GetServerSideProps = async (context) => {
22 const { id } = context.params!;
23 return { props: { userId: id } };
24};
Note: Server-side data fetching (API calls) should only happen in getServerSideProps or getStaticProps. Hooks like useUsers(), useUser(), useCreateUser(), etc., are for client-side data fetching, mutations, or other client-side logic.
Client-Side Navigation
tsx
1import Link from 'next/link';
2import { useRouter } from 'next/router';
3
4export default function Navigation() {
5 const router = useRouter();
6
7 return (
8 <div>
9 <Link href="/about">About</Link>
10 <button onClick={() => router.push('/contact')}>
11 Go to Contact
12 </button>
13 </div>
14 );
15}
SEO with PageLayout
Use the PageLayout component for SEO metadata (static or dynamic):
tsx
1// Static SEO
2import { PageLayout } from "@/components";
3
4export default function AboutPage() {
5 return (
6 <PageLayout
7 title="About Us"
8 description="Learn more about our company"
9 keywords="about, company, team"
10 canonical="/about"
11 >
12 <div>About content</div>
13 </PageLayout>
14 );
15}
tsx
1// Dynamic SEO from props
2import type { GetServerSideProps, InferGetServerSidePropsType } from "next";
3import { PageLayout } from "@/components";
4
5export const getServerSideProps: GetServerSideProps = async () => {
6 return {
7 props: {
8 seo: {
9 title: "Dynamic Page",
10 description: "Dynamic description",
11 },
12 },
13 };
14};
15
16export default function DynamicPage({ seo }: InferGetServerSidePropsType<typeof getServerSideProps>) {
17 return (
18 <PageLayout {...seo}>
19 <div>Content</div>
20 </PageLayout>
21 );
22}
Important Notes
- Always use Pages Router - never App Router patterns
- Only pages handle server-side data fetching - components, hooks, or other files should NOT make server calls
- Page Props type MUST be inferred - use
InferGetServerSidePropsType<typeof getServerSideProps> or InferGetStaticPropsType<typeof getStaticProps>
- Pages are React components that export a default function
- Data fetching functions (
getServerSideProps, getStaticProps, getStaticPaths) go after the page component
- Pages can use hooks like
useUsers(), useUser(), useCreateUser(), etc. for client-side data fetching and mutations
- Use
PageLayout component for SEO metadata (static or dynamic)
- Use
useRouter from next/router (not next/navigation)
- Global styles go in
src/styles/globals.css and imported in _app.tsx
- TypeScript: Use
AppProps from next/app for _app.tsx
- The app includes
ErrorBoundary and QueryProvider in _app.tsx - they wrap all pages automatically