Next.js Pages Router
Structure
This project uses Pages Router exclusively. All pages go in src/pages/.
- Regular pages:
src/pages/[filename].tsx→/filenameroute - Dynamic routes:
src/pages/[id]/page.tsx→/[id]route - API routes:
src/pages/api/[route].ts→/api/[route]endpoint - Special files:
_app.tsx,_document.tsxinsrc/pages/
Creating Pages
Basic Page
// src/pages/about.tsx
export default function About() {
return <div>About page</div>;
}
Dynamic Route
// src/pages/users/[id].tsx
import { useRouter } from 'next/router';
export default function User() {
const router = useRouter();
const { id } = router.query;
return <div>User {id}</div>;
}
Nested Dynamic Route
// src/pages/posts/[id]/[slug].tsx
import { useRouter } from 'next/router';
export default function Post() {
const router = useRouter();
const { id, slug } = router.query;
return <div>Post {id} - {slug}</div>;
}
Special Files
_app.tsx
Wraps all pages. Use for:
- Global providers
- Global styles
- Layout components
- Page-level state
// src/pages/_app.tsx
import "@/styles/globals.css";
import type { AppProps } from "next/app";
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
_document.tsx
Customizes HTML document. Use for:
<html>and<body>attributes- Custom fonts
- Analytics scripts
- Meta tags
// src/pages/_document.tsx
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="en">
<Head />
<body className="antialiased">
<Main />
<NextScript />
</body>
</Html>
);
}
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.
// src/pages/posts/[id].tsx
import type { GetServerSideProps, InferGetServerSidePropsType } from "next";
import { PageLayout } from "@/components";
export const getServerSideProps: GetServerSideProps = async (context) => {
const { id } = context.params!;
const res = await fetch(`https://api.example.com/posts/${id}`);
const post = await res.json();
return {
props: { post },
};
};
// Type MUST be inferred from getServerSideProps
export default function Post({ post }: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<PageLayout
title={post.title}
description={post.content}
canonical={`/posts/${post.id}`}
>
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
</PageLayout>
);
}
getStaticProps
The page component's Props type MUST be inferred from getStaticProps using InferGetStaticPropsType.
// src/pages/posts/[id].tsx
import type { GetStaticProps, GetStaticPaths, InferGetStaticPropsType } from "next";
import { PageLayout } from "@/components";
export const getStaticProps: GetStaticProps = async (context) => {
const { id } = context.params!;
const res = await fetch(`https://api.example.com/posts/${id}`);
const post = await res.json();
return {
props: { post },
revalidate: 60, // ISR: revalidate every 60 seconds
};
};
export const getStaticPaths: GetStaticPaths = async () => {
return {
paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
fallback: 'blocking', // or true, false
};
};
// Type MUST be inferred from getStaticProps
export default function Post({ post }: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<PageLayout
title={post.title}
description={post.content}
canonical={`/posts/${post.id}`}
>
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
</PageLayout>
);
}
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.
import type { GetServerSideProps, InferGetServerSidePropsType } from "next";
export const getServerSideProps: GetServerSideProps = async () => {
return {
props: { post: { id: "1", title: "Hello" } },
};
};
// Type MUST be inferred from getServerSideProps
export default function Post({ post }: InferGetServerSidePropsType<typeof getServerSideProps>) {
return <div>{post.title}</div>;
}
Using Hooks in Pages
Pages can use custom hooks for client-side data fetching, mutations, or other client-side logic. Examples:
// src/pages/users.tsx
import type { GetServerSideProps, InferGetServerSidePropsType } from "next";
import { useUsers } from "@/hooks/users";
export const getServerSideProps: GetServerSideProps = async () => {
return { props: { initialUsers: [] } };
};
export default function UsersPage({ initialUsers }: InferGetServerSidePropsType<typeof getServerSideProps>) {
const { data: users, isLoading } = useUsers();
return (
<div>
{isLoading ? <p>Loading...</p> : <UserList users={users} />}
</div>
);
}
// src/pages/users/[id].tsx
import type { GetServerSideProps, InferGetServerSidePropsType } from "next";
import { useUser, useCreateUser } from "@/hooks/users";
export default function UserPage({ userId }: InferGetServerSidePropsType<typeof getServerSideProps>) {
const { data: user } = useUser(userId);
const createMutation = useCreateUser();
const handleCreate = () => {
createMutation.mutate({ name: "New User" });
};
return (
<div>
<h1>{user?.name}</h1>
<button User</button>
</div>
);
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const { id } = context.params!;
return { props: { userId: id } };
};
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
import Link from 'next/link';
import { useRouter } from 'next/router';
export default function Navigation() {
const router = useRouter();
return (
<div>
<Link href="/about">About</Link>
<button => router.push('/contact')}>
Go to Contact
</button>
</div>
);
}
SEO with PageLayout
Use the PageLayout component for SEO metadata (static or dynamic):
// Static SEO
import { PageLayout } from "@/components";
export default function AboutPage() {
return (
<PageLayout
title="About Us"
description="Learn more about our company"
keywords="about, company, team"
canonical="/about"
>
<div>About content</div>
</PageLayout>
);
}
// Dynamic SEO from props
import type { GetServerSideProps, InferGetServerSidePropsType } from "next";
import { PageLayout } from "@/components";
export const getServerSideProps: GetServerSideProps = async () => {
return {
props: {
seo: {
title: "Dynamic Page",
description: "Dynamic description",
},
},
};
};
export default function DynamicPage({ seo }: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<PageLayout {...seo}>
<div>Content</div>
</PageLayout>
);
}
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>orInferGetStaticPropsType<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
PageLayoutcomponent for SEO metadata (static or dynamic) - Use
useRouterfromnext/router(notnext/navigation) - Global styles go in
src/styles/globals.cssand imported in_app.tsx - TypeScript: Use
AppPropsfromnext/appfor_app.tsx - The app includes
ErrorBoundaryandQueryProviderin_app.tsx- they wrap all pages automatically
Converted and distributed by TomeVault — claim your Tome and manage your conversions.