How to Fix Next.js 404 Page Not Found: The Ultimate Troubleshooting Guide
Few things in web development are as universally frustrating as deploying a fresh build, clicking a link, and staring straight into the void of a “404 Page Not Found” error. If you are currently pulling your hair out trying to figure out exactly how to fix nextjs 404 page not found issues, take a deep breath. You are in good company.
Next.js is an incredible framework, but its evolution from the Pages Router to the App Router, combined with complex static generation and caching layers, means there are dozens of ways a route can silently break. Sometimes it works perfectly on localhost, only to crash spectacularly in production. Other times, dynamic routes simply refuse to resolve.
In this comprehensive guide, we are going to dive deep into the root causes of Next.js routing failures. We will walk through step-by-step solutions—starting from the most common architectural blunders and moving down to niche edge cases—with copy-paste-ready code examples. Let’s get your application back online.
Understanding the Root Cause of Next.js 404 Errors
Before we can fix the problem, we need to understand why Next.js decides to serve a 404 page. At its core, Next.js uses a file-system-based router. When a request hits your server, Next.js looks for a corresponding file in your app (or pages) directory that matches the requested URL.
A 404 error occurs when:
1. The framework cannot find a matching file/folder structure.
2. A server-side redirect or rewrite rule is misconfigured.
3. Dynamic route parameters are not being generated or parsed correctly.
4. The build cache is corrupted and serving stale routing manifests.
5. Middleware is intercepting and aborting the request prematurely.
Identifying which of these scenarios is happening is half the battle.
Step 1: Verify App Router vs. Pages Router Architecture
The single most common reason developers encounter a Next.js 404 error in 2026 is mixing up the routing paradigms. Next.js currently supports both the modern App Router (app/ directory) and the legacy Pages Router (pages/ directory).
Check Your Directory Structure
If you create a file in the wrong directory, Next.js will not register the route.
Incorrect Setup (The 404 Trap):
You want an /about route, but you put it in the pages directory while your project is configured to use the app directory, or vice versa.
Correct App Router Setup:
my-next-app/
├── app/
│ ├── about/
│ │ └── page.tsx <-- Notice it must be named page.tsx
│ ├── layout.tsx
│ └── page.tsx
Correct Pages Router Setup:
my-next-app/
├── pages/
│ ├── about.tsx <-- Notice the file name IS the route
│ └── index.tsx
The Missing page.tsx File
If you are using the modern App Router, simply creating a folder named dashboard is not enough. Next.js will look for a page.tsx, page.jsx, page.js, or page.ts file inside that folder. If it doesn’t find one, it throws a 404, even if the folder exists.
Fix: Always ensure your UI components are exported from a page.tsx file.
// app/dashboard/page.tsx
export default function Dashboard() {
return <h1>Welcome to the Dashboard</h1>;
}
Step 2: Troubleshoot Dynamic Routing and Parameters
Dynamic routes are incredibly powerful, but they are a notorious source of 404 errors. In the App Router, you define a dynamic route using brackets, like [id] or [slug].
Catch-all vs. Optional Catch-all Routes
Understanding the difference between [...slug] and [[...slug]] is critical.
app/shop/[...slug]/page.tsxmatches/shop/clothes,/shop/clothes/shirts, but not/shop.app/shop/[[...slug]]/page.tsxmatches/shop/clothes,/shop/clothes/shirts, and/shop.
If you are getting a 404 on the parent route, you likely used a single bracket catch-all instead of a double bracket optional catch-all.
Forgetting generateStaticParams
If you are statically generating pages (SSG) or using Incremental Static Regeneration (ISR), you must tell Next.js which dynamic paths actually exist. If a user navigates to a path that wasn’t generated at build time, and dynamicParams is set to false, they will hit a 404.
The Fix: Ensure you export generateStaticParams and configure dynamicParams correctly.
// app/blog/[slug]/page.tsx
// 1. Tell Next.js which slugs to build at build time
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then((res) => res.json());
return posts.map((post: any) => ({
slug: post.slug,
}));
}
// 2. If true (default): tries to generate new pages on-demand.
// If false: returns a strict 404 for paths not returned by generateStaticParams
export const dynamicParams = true;
export default function Page({ params }: { params: { slug: string } }) {
return <div>My Post: {params.slug}</div>;
}
Step 3: Audit next.config.js Rewrites and Redirects
Sometimes your code is flawless, but your routing configuration is hijacking the request. Open your next.config.mjs or next.config.js file and look at your rewrites and redirects.
The Rewrite Priority Trap
Rewrites are applied in order. If you have a poorly written wildcard rewrite, it might intercept requests meant for your actual pages.
Problematic Configuration:
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
return [
{
// This catches EVERYTHING, including your real pages!
source: '/:path*',
destination: 'https://external-api.com/:path*',
},
{
// This will never be reached because the rule above caught '/dashboard'
source: '/dashboard',
destination: '/dashboard-secure',
},
];
},
};
export default nextConfig;
The Fix: Order your rewrites from most specific to least specific. Use exact path matching instead of wildcards wherever possible.
Step 4: Debug Next.js Middleware Interception
Next.js Middleware (middleware.ts) allows you to run code before a request is completed. It is heavily used for authentication and localization. However, a simple logic error in your middleware matcher can result in a 404 or an infinite redirect loop that eventually crashes into a 404.
Fixing the Matcher Configuration
If your middleware doesn’t properly exclude static assets and API routes, it will break your application’s routing.
Incorrect Matcher:
// middleware.ts
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], // Often mistyped!
};
If you exclude assets but forget to exclude your API routes, API calls will fail. Worse, if you accidentally exclude your actual pages, Next.js will bypass the router and return a 404.
Correct Middleware Setup:
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Example: Simple auth check
const token = request.cookies.get('token');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
// Best practice matcher: runs middleware on everything EXCEPT static files and api routes
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
Step 5: Clear the Build Cache (The Developer’s IT Crowd Solution)
Have you ever spent hours debugging code, only to realize “it works on my machine” because your local environment is out of sync? Next.js relies heavily on aggressive caching (the .next folder) to speed up builds.
When upgrading Next.js versions (e.g., moving from Next 14 to Next 15/16), or when changing dynamic route structures, stale cache files frequently cause stubborn 404 errors on local environments and during CI/CD pipelines.
How to Clear the Next.js Cache Properly
Run the following commands in your terminal to wipe the slate clean:
# 1. Delete the build output folder
rm -rf .next
# 2. Clear Next.js cache (Usually inside node_modules)
rm -rf node_modules/.cache
# 3. Restart your development server
npm run dev
For Production CI/CD Deployments:
If your deployed site on Vercel, AWS, or Netlify is throwing 404s after a deployment, trigger a completely fresh build without cache. On Vercel, you can force this by adding an environment variable NEXT_FORCE_NO_CACHE=1 or simply redeploying without using the existing build cache via the dashboard.
Step 6: Handle Static Export (output: export) Quirks
If you are building a purely static site (SPA/SSG) to host on GitHub Pages, AWS S3, or Nginx, you might be using the output: 'export' setting in your next.config file.
Static exports do not have a Node.js server running to handle dynamic requests. Therefore, if a user types yoursite.com/blog/post-1 directly into their browser, the hosting provider looks for a directory named /blog/post-1 with an index.html file. If it doesn’t find it, the host throws a 404.
Fixing Trailing Slashes
The easiest way to make static exports compatible with strict hosting providers is to enforce trailing slashes.
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export', // You are generating static HTML
trailingSlash: true, // Forces /blog/post-1 to become /blog/post-1/
};
export default nextConfig;
By adding trailingSlash: true, Next.js will generate a folder structure where /blog/post-1/index.html exists, guaranteeing that strict web servers can serve the file without throwing a 404.
Step 7: Client-Side Navigation vs. Hard Refreshes
Sometimes the 404 isn’t a Next.js configuration error at all, but rather a misunderstanding of how React handles routing.
If you have an <a> tag instead of a Next.js <Link> component, clicking it causes a hard refresh. A hard refresh asks the server to resolve the URL. If the URL relies on client-side state or a query parameter that the server doesn’t know about, you will get a 404.
Fixing Navigation Links
Always use the next/link component for internal routing.
Bad (Causes 404 on dynamic states):
export default function Navigation() {
return (
<a href="/dashboard/user/123">View Profile</a>
);
}
Good (Preserves Next.js Router context):
import Link from 'next/link';
export default function Navigation() {
return (
<Link href="/dashboard/user/123">
View Profile
</Link>
);
}
If you must use programmatic navigation (e.g., after a form submit), use the useRouter hook:
“`tsx
‘use client’;
import { useRouter } from ‘next/navigation’;
import { useState } from ‘react’;
export default function SearchBar() {
const router = useRouter();
const [query, setQuery] = useState(”);
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
if (query) {
// Navigates client-side, preventing a hard server 404
router.push(/search?q=${query});
}
};
return (