In-depth Next.js guides covering App Router, RSC, ISR, and deployment. Get code examples, optimization checklists, and prompts to accelerate development.
[!IMPORTANT]
Quick answer:searchParams in a page.tsx component opts your entire route into dynamic rendering — every page in that route segment, even ones that don't use it. The fix is route separation: move pages that need searchParams into a dedicated dynamic route, and remove it from your catch-all route entirely. Do not use export const dynamic = 'force-dynamic' — that silences the error while making performance worse. The steps below walk through the full architectural fix.
Accepting searchParams in a Next.js App Router page component opts that entire route into dynamic rendering. Not just the pages that use filtering — every page served through that route. If you have a catch-all route handling both static content pages and dynamic product pages, one searchParams prop forces all of them to render on every request.
The fix is architectural: separate routes by rendering requirements. Here's exactly how I restructured a Payload CMS + Next.js 16 site to keep 90% of pages static while maintaining full dynamic filtering for product variants — and why reaching for force-dynamic is the wrong instinct.
Why searchParams Disables Static Generation
Next.js cannot pre-render a page at build time if it depends on data that only exists at request time. searchParams — the query string values in the URL — is one of those request-time dependencies. The moment you accept it in a page component, Next.js has no choice: that route must render dynamically on every request.
This is by design, not a bug. The same rule applies to cookies(), headers(), and draftMode() — all of them are dynamic APIs that opt the route out of static generation.
If you've hit this error in your build output:
bash
"pages that use searchParams must be rendered dynamically"
You're in the right place. The rest of this article explains exactly how to fix it without giving up static generation for your entire site.
Next.js 15/16: searchParams Is Now Async
Before diving into the route separation fix, there's a related breaking change worth understanding if you're on Next.js 15 or 16.
In Next.js 15, params and searchParams in page.js became asynchronous Promises. The same applies to cookies(), headers(), and draftMode(). If you access these synchronously, you'll see:
bash
Cannot access Request information synchronously with Page or Layout or Route params or Page searchParams
The migration path is to make your component async and await the props:
[!IMPORTANT]
Making searchParams async does not make your route static. These are two separate concerns. Awaiting searchParams is required in Next.js 15/16 — but the route is still dynamic. You need route separation to restore static generation.
The Problem: searchParams Forces Dynamic Rendering on All Pages
I had a typical Next.js App Router setup with a catch-all route handling multiple page types. The route structure looked like this:
typescript
// File: src/app/(frontend)/[...slug]/page.tsxexportdefaultasyncfunctionSlugPage({
params: paramsPromise,
searchParams: searchParamsPromise,
}: {
params: Promise<{ slug?: string[] }>;
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
const params = await paramsPromise;
const searchParams = await searchParamsPromise;
const { slug } = params;
// Handle different page typesconst pageType = getPageTypeFromSlug(slug);
// Product pages used searchParams for variant filtering// But general pages, service pages, and project pages didn't need itconst page = awaitgetPageBySlug(slug);
return<>{renderPageBlocks(page, searchParams)}</>;
}
This single route handled everything: homepage, general pages, service pages, project pages, and product pages. The product pages needed searchParams for filtering product variants by color, size, and other attributes. But here's the critical issue I discovered in the Next.js documentation: any page component that accepts searchParams cannot be statically generated.
When I ran pnpm run build, the output confirmed my suspicions:
bash
Route (app) Size First Load JS
├ ƒ / 260 B 373 kB
├ ƒ /[...slug] 260 B 373 kB
├ ƒ /storitve/[slug] 260 B 373 kB
ƒ (Dynamic) server-rendered on demand
Every page marked with ƒ (Dynamic) meant zero static generation benefits. My homepage, about page, contact page—all being server-rendered on every request despite having completely static content.
If your first instinct is to add export const dynamic = 'force-dynamic' to silence this — stop. That's the wrong fix and it makes things worse. Jump to why →
Understanding the Static Generation Constraint
Next.js App Router has a strict rule: pages that use searchParams must be rendered dynamically because the search parameters are only known at request time. This makes complete sense for pages that genuinely need dynamic filtering or state management through URL parameters. But in my case, only product pages needed this functionality.
The problem was architectural. By handling all page types in a single catch-all route that accepted searchParams, I had inadvertently forced every page through dynamic rendering. The solution required separating concerns: static pages should live in one route structure, and dynamic pages requiring searchParams should have their own dedicated route.
Step 1: Analyzing Which Pages Actually Need searchParams
Before making any changes, I audited exactly which pages used the searchParams functionality. The breakdown looked like this:
The product pages needed searchParams because they implemented variant filtering. When users selected a color or size option, the ProductVariantSelector component updated the URL with query parameters, and the server component read those parameters to display the correct variant pricing and availability.
This 90/10 split made the solution clear: extract product pages into their own route and let the catch-all route be fully static.
Step 2: Creating a Dedicated Dynamic Route for Products
The first step was creating a new route specifically for product pages at /src/app/(frontend)/izdelki/[slug]/page.tsx. This route would be the only one accepting searchParams.
This dedicated product route maintains all the dynamic functionality needed for variant filtering. The key difference from the catch-all route is that this one expects a single slug string rather than an array, and it's specifically designed to handle product pages only.
Notice that we still use generateStaticParams() even though this route accepts searchParams. This generates static HTML for the base product URLs without query parameters. When users add filter parameters like ?color=red, Next.js dynamically renders those variations while still benefiting from static generation for the initial page load.
Step 3: Removing searchParams from the Catch-All Route
With product pages handled separately, I could now remove all searchParams logic from the main catch-all route. This required several coordinated changes.
First, I updated the Props interface to remove searchParams:
This constant maps URL prefixes to page types. Removing izdelki meant the catch-all route would no longer attempt to handle product pages, delegating that responsibility to the dedicated route.
I updated the type definitions to reflect the removal:
These changes systematically removed all product page handling and searchParams dependencies from the catch-all route, making it eligible for static generation.
Step 4: Updating Static Path Generation
With the route separation complete, I needed to ensure that the static path generation logic didn't create conflicts. The issue was that both routes had generateStaticParams() functions, and I needed to make sure they didn't overlap.
I updated the main static paths function to exclude product pages:
The critical change here is removing the product pages from the static paths generation for the catch-all route. This prevents the Next.js build error "The provided export path doesn't match the page" that occurs when multiple routes try to generate the same path.
I also needed to exclude the homepage from the catch-all route's static params since it's handled by a dedicated /page.tsx file. This is a common pattern in Next.js where the root page uses the catch-all route's logic but is defined separately.
Then I created a new function to generate product slugs for the dedicated route:
This function is called by the product route's generateStaticParams() to get all product slugs for static generation. By separating this logic, each route has clear ownership of its static path generation.
One tricky aspect of this refactoring was the homepage. In Next.js App Router, you typically have a root /page.tsx that handles the homepage separately from dynamic routes. My implementation used an elegant pattern to reuse the catch-all route logic:
This works because when Next.js calls the PageTemplate component from the root page, it automatically provides the params as an empty slug array, which the catch-all route interprets as the homepage. This approach avoids code duplication while maintaining clean separation of concerns.
However, after removing searchParams from the catch-all route, this pattern continued to work seamlessly without any changes needed. This demonstrates one of the benefits of the refactoring—the interfaces became simpler and more predictable.
Step 6: Updating Component Interfaces
The final implementation step involved updating all the render block components to make searchParams optional. This maintains backward compatibility while reflecting the new architecture.
I made the same change to RenderGeneralPageBlocks and RenderProjectPageBlocks. While these components no longer receive searchParams from the catch-all route, making it optional rather than removing it entirely provides flexibility for future use cases and maintains the component interface consistency.
For the product blocks, searchParams remains required since it's essential functionality:
After making all the structural changes, I ran into several TypeScript compilation errors that needed resolution. The first was in the preview route handler:
bash
Type error: Type 'typeof import("/src/app/(frontend)/next/preview/route")' does not satisfy the constraint 'RouteHandlerConfig<"/next/preview">'.
Types of property 'GET' are incompatible.
This error was unrelated to the route refactoring but surfaced during the build. The issue was that the preview route was using a custom request type instead of Next.js's NextRequest:
Using NextRequest resolves the type incompatibility and follows Next.js best practices for route handlers.
I also needed to update the TypeScript hints in RenderProjectPageBlocks to avoid unused @ts-expect-error directive warnings:
typescript
// File: src/blocks/RenderProjectPageBlocks.tsx
<Block
{...block}
// @ts-expect-error disableInnerContainer may not exist on all blocks
disableInnerContainer
// @ts-expect-error searchParams may not exist on all blocks
searchParams={searchParams}
/>
This properly documents why we're suppressing TypeScript errors for props that may not exist on all block component types.
code
## When This Fix Applies
Route separation is the right approach when all of the following are true:
- You have a catch-all route (e.g. `[...slug]`) that handles multiple page types
- Some page types need `searchParams`, but most don't
- You are seeing `ƒ (Dynamic)` markers in your build output on pages that should be static
- You want those pages pre-rendered and served from Vercel's edge cache
If your route exclusively handles pages where `searchParams` is always needed (a filtered product listing where every URL is dynamic), making that route dynamic is correct — route separation doesn't help. The fix only applies when you have a mixed route where static and dynamic pages are incorrectly grouped together.
## Don't Use force-dynamic to Fix This {#dont-use-force-dynamic}
The most common wrong answer you'll find on Stack Overflow and GitHub is this:
```typescript
// ❌ Wrong fix
export const dynamic = 'force-dynamic';
export default async function Page({ searchParams }) {
// ...
}
export const dynamic = 'force-dynamic' does silence the build error. But here's what it actually does:
Makes the entire route permanently dynamic — every request, every time, no exceptions
Sets all fetches on that route to no-store, disabling data caching too
Gives up static generation benefits not just for your dynamic pages, but for every page in that route segment
In the catch-all route scenario, this means your homepage, about page, and every static content page gets server-rendered on demand — exactly the problem you were trying to solve.
force-dynamic is the nuclear option. It has legitimate uses (routes that genuinely need per-request rendering with no caching at all), but it's not a fix for the searchParams static generation problem. It's just a way to stop seeing the error while accepting worse performance.
The correct fix is route separation: move the pages that need searchParams into their own dedicated route, as shown in the steps above.
After completing all the changes, I ran the production build to verify the optimization worked:
bash
pnpm run build
The build output showed exactly what I was hoping for:
bash
Route (app) Size First Load JS Revalidate
┌ ○ / 254 B 377 kB 15m
├ ● /[...slug] 254 B 377 kB 15m
├ ├ /politika-piskotkov 15m
├ ├ /pogoji-poslovanja 15m
├ └ [+14 more paths]
├ ● /izdelki/[slug] 20.9 kB 405 kB 15m
├ ├ /izdelki/cistilna-naprava-roeco-8-6000l 15m
├ ├ /izdelki/cistilna-naprava-roeco-5-4000l 15m
└ └ [+2 more paths]
○ (Static) prerendered as static content
● (SSG) prerendered as static HTML (uses generateStaticParams)
The symbols tell the story:
○ (Static) - The homepage is now fully static
● (SSG) - The catch-all route and product route both use static site generation
No more ƒ (Dynamic) markers on content pages
This represents a massive performance improvement. The homepage, general pages, service pages, and project pages are now pre-rendered at build time and served as static HTML. Product pages are also statically generated for their base URLs, with dynamic rendering only happening when users interact with variant filters and add search parameters to the URL.
Through this refactoring process, I learned several important lessons about Next.js App Router performance optimization:
Making searchParams async doesn't make your route static. The Next.js 15/16 async API change and the static generation opt-out are two separate concerns. Awaiting searchParams is required — but the route is still dynamic. You need route separation to restore static generation.
force-dynamic is a last resort, not a fix. If you're using it to silence searchParams build errors, you've accepted dynamic rendering for your entire route. Route separation is almost always the correct solution — it gives you static generation for 90% of your pages while preserving dynamic behavior exactly where it's needed.
searchParams has a global impact. The moment you accept searchParams in a page component, that entire route becomes ineligible for static generation. This isn't a bug—it's by design. Search parameters are request-time values, so Next.js must render the page dynamically to access them. Be very intentional about which pages truly need this functionality.
The 90/10 rule applies to routing. Don't let 10% of your pages that need dynamic behavior force the other 90% into dynamic rendering. Separate routes based on rendering requirements, not just on content organization.
Route separation improves performance without breaking functionality. You can still use generateStaticParams with routes that accept searchParams. The base URL gets statically generated, and only the query parameter variations render dynamically.
TypeScript strictness catches architectural issues. When I removed searchParams from the catch-all route, TypeScript immediately flagged every place where the interface had changed. This forced me to consciously update each component, preventing runtime errors from mismatched prop expectations.
The export path mismatch error reveals route conflicts. If you see "The provided export path doesn't match the page" during build, it means multiple routes are trying to generate the same static path. The solution is to ensure each route has exclusive ownership of its paths.
This refactoring took a few hours of careful implementation and testing, but the performance gains are substantial. Static generation means faster page loads, better SEO crawling, reduced server costs, and improved user experience for the vast majority of site visitors.
If your Next.js application uses a catch-all route with searchParams, audit which pages actually need that functionality. You might find, as I did, that a simple route separation unlocks significant performance improvements.