404 Page

Learn how to create custom 404 pages for unmatched routes in Bini.js.

Bini.js provides a built-in 404 page, but you can create custom not-found.tsx files to display your own UI when a route is not found.

Global 404 Page

Create a not-found.tsx file in the root of your app directory to handle all unmatched routes globally.

src/app/
├── layout.tsx
├── page.tsx
└── not-found.tsx         ← Global 404 page
app/not-found.tsx
// src/app/not-found.tsx
export default function NotFound() {
  return (
    <div className="flex flex-col items-center justify-center min-h-screen">
      <h1 className="text-4xl font-bold text-white mb-4">404</h1>
      <p className="text-slate-400 mb-6">The page you're looking for doesn't exist.</p>
      <Link to="/" className="text-cyan-400 hover:underline">
        Return Home
      </Link>
    </div>
  )
}

This page will be shown for any unmatched route, such as /non-existent or /blog/invalid-post.

Nested 404 Pages

You can create route-specific 404 pages by placing not-found.tsx in subdirectories. The closest 404 page to the matched route will be used.

src/app/
├── not-found.tsx             ← Global 404 (fallback)
├── blog/
│   ├── not-found.tsx         ← Blog-specific 404
│   ├── page.tsx
│   └── [slug]/
│       └── page.tsx
└── admin/
    ├── not-found.tsx         ← Admin-specific 404
    └── page.tsx
app/blog/not-found.tsx
// src/app/blog/not-found.tsx
export default function BlogNotFound() {
  return (
    <div className="py-12 text-center">
      <h1 className="text-3xl font-bold text-white mb-3">Post Not Found</h1>
      <p className="text-slate-400 mb-6">The blog post you're looking for doesn't exist.</p>
      <Link to="/blog" className="text-cyan-400 hover:underline">
        View all posts
      </Link>
    </div>
  )
}
URL404 Page Used
/blog/non-existentapp/blog/not-found.tsx
/admin/invalidapp/admin/not-found.tsx
/completely/wrongapp/not-found.tsx (global)

Programmatic 404

You can manually trigger a 404 page from within your components when data is not found.

app/blog/[slug]/page.tsx
// src/app/blog/[slug]/page.tsx
export default function BlogPost() {
  const { slug } = useParams()
  
  // Fetch post data
  const post = getPost(slug)
  
  // If post doesn't exist, show 404
  if (!post) {
    return (
      <div className="py-12 text-center">
        <h1 className="text-3xl font-bold text-white mb-3">Post Not Found</h1>
        <p className="text-slate-400 mb-6">The post "{slug}" doesn't exist.</p>
        <Link to="/blog" className="text-cyan-400 hover:underline">
          View all posts
        </Link>
      </div>
    )
  }
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  )
}

404 with Layout

404 pages are automatically wrapped with the layout chain of the route they belong to.

src/app/
├── layout.tsx                 ← Root layout (wraps everything)
├── not-found.tsx              ← Global 404 (wrapped by root layout)
└── blog/
    ├── layout.tsx             ← Blog layout
    ├── not-found.tsx          ← Blog 404 (wrapped by root + blog layouts)
    └── page.tsx

This means your 404 pages automatically inherit headers, footers, and other shared UI from layouts.

app/blog/layout.tsx
// src/app/blog/layout.tsx
export default function BlogLayout() {
  return (
    <div>
      <header className="mb-8">
        <h1 className="text-2xl font-bold">Blog</h1>
      </header>
      <main><Outlet /></main>
    </div>
  )
}

// The blog/not-found.tsx will automatically have the "Blog" header!

Styling 404 Pages

You can create rich, styled 404 pages with images, animations, and interactive elements:

app/not-found.tsx
// src/app/not-found.tsx
export default function NotFound() {
  return (
    <div className="min-h-[60vh] flex flex-col items-center justify-center text-center px-4">
      {/* Animated 404 */}
      <div className="relative mb-8">
        <h1 className="text-9xl font-bold text-slate-800">404</h1>
        <div className="absolute inset-0 flex items-center justify-center">
          <span className="text-6xl">🔍</span>
        </div>
      </div>
      
      {/* Message */}
      <h2 className="text-3xl font-bold text-white mb-3">Page Not Found</h2>
      <p className="text-slate-400 max-w-md mb-8">
        The page you're looking for might have been removed, renamed, or doesn't exist.
      </p>
      
      {/* Actions */}
      <div className="flex gap-4">
        <Link 
          to="/" 
          className="px-6 py-3 bg-cyan-500 text-black font-medium rounded-lg hover:bg-cyan-400 transition-colors"
        >
          Go Home
        </Link>
        <button 
          onClick={() => window.history.back()} 
          className="px-6 py-3 border border-slate-700 text-white font-medium rounded-lg hover:bg-slate-900 transition-colors"
        >
          Go Back
        </button>
      </div>
      
      {/* Search suggestion */}
      <p className="text-slate-500 text-sm mt-8">
        Looking for something specific? Try using the navigation menu above.
      </p>
    </div>
  )
}

Static Export & 404

When using npm run export for static hosting, Bini.js generates a 404.html file:

SituationGenerated 404.html
not-found.tsx existsCopy of index.html with your custom 404 page
No not-found.tsxRedirect script that preserves the URL and redirects to /

This ensures your SPA works correctly on static hosts like GitHub Pages, Netlify, and Vercel.