A generic 404 page loses visitors. A well-designed one redirects them to useful content and reinforces your brand. Here is how to build one.
Step 1: Create the Not Found Page
In the App Router, create not-found.tsx at the app root:
// app/not-found.tsx
import Link from "next/link";
export default function NotFound() {
return (
<main className="flex min-h-[70vh] items-center justify-center px-6">
<div className="text-center">
<p className="text-sm font-medium text-blue-600">404</p>
<h1 className="mt-2 text-4xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-5xl">
Page not found
</h1>
<p className="mt-4 text-lg text-gray-500 dark:text-gray-400">
The page you are looking for does not exist or has been moved.
</p>
<div className="mt-8 flex items-center justify-center gap-4">
<Link
href="/"
className="rounded-lg bg-blue-600 px-6 py-3 text-sm font-medium text-white hover:bg-blue-700"
>
Go home
</Link>
<Link
href="/contact"
className="rounded-lg border px-6 py-3 text-sm font-medium hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800"
>
Contact us
</Link>
</div>
</div>
</main>
);
}
Step 2: Add a Search Bar
Help visitors find what they were looking for:
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
function NotFoundSearch() {
const [query, setQuery] = useState("");
const router = useRouter();
function handleSearch(e: React.FormEvent) {
e.preventDefault();
if (query.trim()) {
router.push(`/blog?search=${encodeURIComponent(query)}`);
}
}
return (
<form onSubmit={handleSearch} className="mt-8 mx-auto max-w-md">
<div className="flex gap-2">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search our site..."
className="w-full rounded-lg border px-4 py-2 text-sm dark:border-gray-700 dark:bg-gray-800"
/>
<button
type="submit"
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
Search
</button>
</div>
</form>
);
}
Step 3: Add Popular Links
Guide visitors to your most valuable content:
const popularLinks = [
{ label: "Our Services", href: "/services", description: "See what we offer" },
{ label: "Blog", href: "/blog", description: "Read our latest articles" },
{ label: "Pricing", href: "/pricing", description: "View our plans" },
{ label: "Contact", href: "/contact", description: "Get in touch" },
];
function PopularLinks() {
return (
<div className="mt-12">
<h2 className="text-sm font-semibold text-gray-900 dark:text-white">
Popular pages
</h2>
<ul className="mt-4 divide-y dark:divide-gray-700">
{popularLinks.map((link) => (
<li key={link.href}>
<Link
href={link.href}
className="flex items-center justify-between py-3 text-sm hover:text-blue-600"
>
<div>
<span className="font-medium">{link.label}</span>
<span className="ml-2 text-gray-400">{link.description}</span>
</div>
<svg
className="h-4 w-4 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</Link>
</li>
))}
</ul>
</div>
);
}
Step 4: Full 404 Page
// app/not-found.tsx
import Link from "next/link";
import { NotFoundSearch } from "@/components/NotFoundSearch";
export default function NotFound() {
return (
<main className="flex min-h-[70vh] items-center justify-center px-6">
<div className="mx-auto max-w-lg text-center">
{/* Large 404 number */}
<div className="text-[120px] font-bold leading-none text-gray-100 dark:text-gray-800">
404
</div>
<h1 className="-mt-8 text-2xl font-bold text-gray-900 dark:text-white">
Page not found
</h1>
<p className="mt-3 text-gray-500 dark:text-gray-400">
Sorry, we could not find the page you are looking for. It may have been
moved or deleted.
</p>
{/* Search */}
<NotFoundSearch />
{/* Actions */}
<div className="mt-6 flex items-center justify-center gap-4">
<Link
href="/"
className="rounded-lg bg-blue-600 px-6 py-2.5 text-sm font-medium text-white hover:bg-blue-700"
>
Go home
</Link>
<Link
href="/contact"
className="text-sm font-medium text-blue-600 hover:text-blue-700"
>
Contact support
</Link>
</div>
{/* Popular links */}
<PopularLinks />
</div>
</main>
);
}
Step 5: Route-Specific Not Found Pages
Create not-found.tsx in specific route segments:
// app/blog/not-found.tsx
import Link from "next/link";
export default function BlogNotFound() {
return (
<div className="py-20 text-center">
<h1 className="text-2xl font-bold">Article Not Found</h1>
<p className="mt-2 text-gray-500">
This blog post may have been removed or the URL is incorrect.
</p>
<Link
href="/blog"
className="mt-6 inline-block rounded-lg bg-blue-600 px-6 py-2 text-sm text-white"
>
Browse all articles
</Link>
</div>
);
}
Trigger it from server components:
import { notFound } from "next/navigation";
export default async function BlogPost({ params }) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) {
notFound();
}
return <article>{/* post content */}</article>;
}
Step 6: Track 404 Errors
Log 404 hits to identify broken links and redirect opportunities:
// app/not-found.tsx
import { headers } from "next/headers";
export default async function NotFound() {
const headersList = await headers();
const referer = headersList.get("referer");
// Log to analytics (server-side)
console.log("404 hit:", {
referer,
timestamp: new Date().toISOString(),
});
return (
<main>
{/* 404 content */}
</main>
);
}
Design Tips
- Keep the page on-brand (use your regular layout and navigation)
- Use a clear headline that explains what happened
- Provide a search bar and popular links
- Include a direct path back to the homepage
- Consider humor only if it fits your brand
- Make sure the page returns a proper 404 HTTP status code
- Test for accessibility
Need Website Design Help?
We design engaging websites with thoughtful error handling and user experience. Contact us to improve your site.