Any time you read a request-specific value (cookies, headers, search params), the route turns dynamic — it cannot be prerendered. Knowing this avoids surprise build failures.
1// app/search/page.tsx
2type SP = Promise<{ q?: string; page?: string }>;
3
4export default async function Search({ searchParams }: { searchParams: SP }) {
5 const { q = "", page = "1" } = await searchParams;
6 const results = await searchCourses(q, Number(page));
7 return <SearchResults results={results} query={q} />;
8}Reading searchParams makes the page dynamic — that's correct here.
1import { cookies } from "next/headers";
2
3// Read
4const session = (await cookies()).get("session")?.value;
5
6// Write (only inside Server Actions or Route Handlers)
7(await cookies()).set({
8 name: "session",
9 value: token,
10 httpOnly: true,
11 secure: true,
12 sameSite: "lax",
13 path: "/",
14 maxAge: 60 * 60 * 24 * 30,
15});
16
17// Delete
18(await cookies()).delete("session");1import { headers } from "next/headers";
2
3const h = await headers();
4const ua = h.get("user-agent");
5const ip = h.get("x-forwarded-for")?.split(",")[0]?.trim();headers() is read-only. To set response headers, return them from a Route Handler.
For previewing unpublished CMS content:
1import { draftMode } from "next/headers";
2
3const { isEnabled } = await draftMode();Toggle from a route handler:
1// app/api/draft/route.ts
2import { draftMode } from "next/headers";
3
4export async function GET() {
5 (await draftMode()).enable();
6 return Response.redirect(new URL("/", request.url));
7}Any of these calls flip the page to dynamic rendering:
cookies()headers()draftMode()searchParams accessfetch with cache: "no-store"If you want a page to stay static, push these calls into a Client Component, an Action, or a separate dynamic route segment.
Combine a static page with a small dynamic Server Component using Suspense:
1// app/page.tsx — static
2import { Suspense } from "react";
3
4export default function Home() {
5 return (
6 <>
7 <Hero />
8 <Suspense fallback={<UserBadgeSkeleton />}>
9 <UserBadge /> {/* dynamic — reads cookies */}
10 </Suspense>
11 <Footer />
12 </>
13 );
14}
15
16// components/UserBadge.tsx — dynamic
17import { cookies } from "next/headers";
18
19export async function UserBadge() {
20 const session = (await cookies()).get("session")?.value;
21 if (!session) return <SignInPrompt />;
22 const user = await getUserBySession(session);
23 return <span>Hi, {user.name}</span>;
24}Enable PPR on the route, and the shell prerenders while the badge streams in per-request — best of both worlds.