Not Found (404)
Custom 404 UI with not-found.tsx - special file, no URL, nearest-wins.
Overview
Bini.js ships a built-in 404. Add not-found.tsx in any folder for custom UI when no route matches. It is a special file - it does not create a URL. Nearest-wins applies.
| File | Creates URL? | Purpose |
|---|---|---|
| app/page.tsx | Yes - / | Home page |
| app/not-found.tsx | No - special file | Global 404 fallback |
| app/blog/not-found.tsx | No - special file | Blog-specific 404 |
Global 404 Page
Create not-found.tsx at the root of app/ to handle all unmatched routes.
export default function NotFound() {
return (
<div className="flex min-h-screen flex-col items-center justify-center">
<h1 className="text-4xl font-bold">404</h1>
<p className="mb-6">The page you're looking for doesn't exist.</p>
<a href="/">Return Home</a>
</div>
)
}/non-existent or /blog/invalid-post when no closer not-found exists.Nested 404 Pages
Place not-found.tsx in subdirectories for segment-specific 404 UI. The closest file to the unmatched path wins.
export default function BlogNotFound() {
return (
<div className="py-12 text-center">
<h1 className="text-3xl font-bold">Post Not Found</h1>
<p className="mb-6">The blog post you're looking for doesn't exist.</p>
<a href="/blog">View all posts</a>
</div>
)
}| URL | 404 Page Used |
|---|---|
| /blog/non-existent | app/blog/not-found.tsx |
| /admin/invalid | app/admin/not-found.tsx |
| /completely/wrong | app/not-found.tsx (global) |
Programmatic 404
When a route matches but data is missing, return your own not-found UI from the page component.
export default function BlogPost({
params,
}: {
params: { slug: string }
}) {
const post = getPost(params.slug)
if (!post) {
return (
<div className="py-12 text-center">
<h1>Post Not Found</h1>
<p>The post "{params.slug}" doesn't exist.</p>
<a href="/blog">View all posts</a>
</div>
)
}
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}404 with Layout
not-found.tsx is rendered inside the layout chain of the segment it belongs to. Headers and sidebars stay visible.
export default function BlogLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div>
<header className="mb-8">
<h1 className="text-2xl font-bold">Blog</h1>
</header>
<main>{children}</main>
</div>
)
}blog/not-found still shows the Blog header from blog/layout.Styling 404 Pages
Build any UI you want - copy, links, and actions are all client-side.
export default function NotFound() {
return (
<div className="flex min-h-[60vh] flex-col items-center justify-center px-4 text-center">
<h1 className="text-9xl font-bold">404</h1>
<h2 className="mb-3 text-3xl font-bold">Page Not Found</h2>
<p className="mb-8 max-w-md text-neutral-500">
The page you're looking for might have been removed or doesn't exist.
</p>
<div className="flex gap-4">
<a
href="/"
className="rounded-lg bg-black px-6 py-3 text-white dark:bg-white dark:text-black"
>
Go Home
</a>
<button
onClick={() => window.history.back()}
className="rounded-lg border px-6 py-3"
>
Go Back
</button>
</div>
</div>
)
}Complete Example
| File | Creates URL? | Purpose |
|---|---|---|
| app/page.tsx | Yes - / | Home page |
| app/not-found.tsx | No | Global 404 |
| app/blog/not-found.tsx | No | Blog 404 |
| app/dashboard/not-found.tsx | No | Dashboard 404 |