404 This page could not be found — but only on production for dynamic routes
appears when:When a dynamic route has no generateStaticParams and dynamic = 'error' is set, or next.config uses output: 'export' which requires every param to be pre-rendered
404 on dynamic route after deploy
Next.js lets you choose static or dynamic rendering per route. Pick wrong and production returns 404 for URLs that work fine in dev.
export async function generateStaticParams() returning the IDs. If unknown, export const dynamic = "force-dynamic". If your next.config has output: "export", you must pre-render every valid ID or drop dynamic routes.Quick fix for 404 on dynamic route after deploy
01// app/users/[id]/page.tsx02// Option A: dynamic rendering — IDs unknown at build time03export const dynamic = "force-dynamic";04 05export default async function UserPage({06 params,07}: {08 params: Promise<{ id: string }>;09}) {10 const { id } = await params;11 const user = await fetchUser(id);12 if (!user) notFound();13 return <UserCard user={user} />;14}15 16// Option B: static generation for a known set of IDs17export async function generateStaticParams() {18 const users = await listUsers();19 return users.map((u) => ({ id: u.id }));20}21 22// Option C: allow any ID but pre-render the common ones23export const dynamicParams = true; // default; render unknown IDs on requestDeeper fixes when the quick fix fails
01 · Hybrid: pre-render top IDs, render rest on demand
01// app/products/[slug]/page.tsx02export const dynamicParams = true; // default — allow unknown slugs at request time03 04export async function generateStaticParams() {05 // Only pre-render the 100 most-viewed products at build06 const top = await db.products.findMany({07 where: { featured: true },08 take: 100,09 select: { slug: true },10 });11 return top.map((p) => ({ slug: p.slug }));12}13 14export default async function ProductPage({15 params,16}: {17 params: Promise<{ slug: string }>;18}) {19 const { slug } = await params;20 const product = await db.products.findUnique({ where: { slug } });21 if (!product) notFound();22 return <ProductDetail product={product} />;23}02 · Route-level static config for ISR
01// app/blog/[slug]/page.tsx — Incremental Static Regeneration02export const revalidate = 3600; // revalidate every hour03 04export async function generateStaticParams() {05 const posts = await listPosts();06 return posts.map((p) => ({ slug: p.slug }));07}08 09export default async function BlogPost({10 params,11}: {12 params: Promise<{ slug: string }>;13}) {14 const { slug } = await params;15 const post = await getPost(slug);16 if (!post) notFound();17 return <article>{post.content}</article>;18}03 · Playwright test: every real route returns 200
01// tests/routes.spec.ts — regression test for route 404s02import { test, expect } from "@playwright/test";03 04const SAMPLE_IDS = ["id-1", "id-2", "id-3"]; // real IDs from prod05 06test.describe("dynamic routes", () => {07 for (const id of SAMPLE_IDS) {08 test(`GET /users/${id} returns 200`, async ({ request }) => {09 const res = await request.get(`/users/${id}`);10 expect(res.status()).toBe(200);11 });12 }13 14 test("unknown user renders notFound()", async ({ request }) => {15 const res = await request.get("/users/definitely-not-real");16 expect(res.status()).toBe(404);17 expect(await res.text()).toContain("not found");18 });19});Why AI-built apps hit 404 on dynamic route after deploy
The Next.js App Router decides how to render each route at build time. For a dynamic route like app/users/[id]/page.tsx, Next looks at three signals: the generateStaticParams export, the dynamic export, and the output setting in next.config. Based on these, it chooses one of four behaviors: pre-render a fixed set of IDs at build, pre-render that set plus render unknowns on request, render every request fresh, or throw if any dynamic API is touched. The wrong combination causes production 404s that dev never sees.
The most common failure mode with AI-generated code is missing generateStaticParams combined with an implicit static render. The scaffold creates the dynamic route file with a page component but no export that tells Next how to generate params. On dev the route works because Next renders on demand — dev never pre-renders anything. On build, Next discovers the dynamic route, finds no generateStaticParams, defaults to dynamicParams mode, but then the deploy target (static export) cannot serve dynamic routes at all. Every request 404s.
The second failure mode is a generateStaticParams that returns an empty array. That happens when the build-time data source is not available — for example, the function queries a database that is not configured in the build env, so the query throws but is silently caught. Next then has zero params to pre-render and no fallback, so every URL 404s. Check your build log for warnings about empty param arrays.
The third failure is output: "export". Static export was popular when deploying to S3 or GitHub Pages. It produces pure HTML and JS with no server. Under static export, dynamic routes require every valid ID to be known at build time — there is no server to handle unknown IDs at request time. If your generateStaticParams returns only 10 IDs but your app has 10,000 users, only those 10 URLs will work. The other 9,990 will 404. The fix is to drop static export or switch to SSR mode on your host.
404 on dynamic route after deploy by AI builder
How often each AI builder ships this error and the pattern that produces it.
| Builder | Frequency | Pattern |
|---|---|---|
| Lovable | Every dynamic route scaffold | Omits generateStaticParams, assumes dev behavior applies to prod |
| Bolt.new | Common | Leaves output: 'export' from an earlier static scaffold after adding dynamic routes |
| Cursor | Common | Writes generateStaticParams with a db call that throws silently — empty array result |
| Base44 | Sometimes | Uses dynamic = 'error' as default because of a copied example |
| Replit Agent | Rare | Creates duplicate route files at [id] and [...slug] — conflict causes 404 |
Related errors we fix
Stop 404 on dynamic route after deploy recurring in AI-built apps
- →Pick the rendering strategy explicitly per dynamic route — never rely on defaults in production.
- →Default to export const dynamic = 'force-dynamic' when IDs are user-generated or unbounded.
- →Ship a Playwright test that hits a sample of real dynamic IDs in CI after every deploy.
- →Never combine output: 'export' with user-generated dynamic routes — the host has no server to render them.
- →Check the Vercel build log for the static/dynamic table on every deploy — catch regressions before users do.
Still stuck with 404 on dynamic route after deploy?
404 on dynamic route after deploy questions
Why does a dynamic route like /users/[id] 404 only in production?+
What is generateStaticParams and when do I need it?+
What is the difference between dynamic = 'error' and dynamic = 'force-dynamic'?+
Why does removing output: 'export' break my deploy?+
How much does an Afterbuild Labs routing audit cost?+
Ship the fix. Keep the fix.
Emergency Triage restores service in 48 hours. Break the Fix Loop rebuilds CI so this error cannot ship again.
Hyder Shah leads Afterbuild Labs, shipping production rescues for apps built in Lovable, Bolt.new, Cursor, Replit, v0, and Base44. our rescue methodology.