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.
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/hotels/[id].tsx
import type { GetServerSideProps, InferGetServerSidePropsType } from "next";
import { useHotels } from "@/hooks/hotels";
export const getServerSideProps: GetServerSideProps = async (context) => {
const { id } = context.params!;
return { props: { hotelId: id } };
};
export default function HotelPage({ hotelId }: InferGetServerSidePropsType<typeof getServerSideProps>) {
const { data: hotel } = useHotels.getById(hotelId);
const createMutation = useHotels.create();
const handleCreate = () => {
createMutation.mutate({ name: "New Hotel" });
};
return (
<div>
<h1>{hotel?.name}</h1>
<button onClick={handleCreate}>Create Hotel</button>
</div>
);
}
Note: Server-side data fetching (API calls) should only happen in getServerSideProps or getStaticProps. Hooks like useUsers(), useHotels(), useHotels.create(), 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 onClick={() => 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(),useHotels(),useHotels.create(), 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