How to make a custom 404 page — without getting a “soft 404” from Google

A nice "Nothing here" page can hurt your site. Sounds odd — here's why.
When an address doesn't exist, the server should answer with a 404 code. The browser doesn't care: it just shows the page. But Google looks at the code. If the page says "not found" while the server answered 200 — "all good" — Google calls it a soft 404 and flags it in Search Console as a problem.
In single-page React apps this happens by default. So let's build a custom 404 page step by step — and check right away that the code is correct. We covered what those numbers mean in what are HTTP status codes.
Step 1. Decide what goes on the page
A good 404 page answers two questions: what happened, and where to go now.
- Plain words, no blame. "This page doesn't exist. It may have moved, or there's a typo in the address."
- A way forward. A link to the home page, two or three main sections, search if you have it.
- Your site's design. The header and menu stay in place. People should see they're still on your site.
Google's advice adds one more item: give people a way to report a broken link. A line like "Found a broken link? Let us know" with an email or a form is enough. You'll learn about broken addresses before they start losing visitors.
No automatic redirects after five seconds: people won't have time to understand what happened.
Step 2. Static site: a 404.html file
If your site is plain HTML files, one file does the job.
- Create
404.htmlin the root of the folder you deploy. - Netlify picks it up automatically. Its docs say exactly that: if your site has a
404.html, it's shown for any address that doesn't match a file. No redirect rules needed. - GitHub Pages — same thing:
404.htmlin the root. If you write in Markdown, name the file404.mdand addpermalink: /404.htmlat the top. - Your own nginx server — one line in the site config:
error_page 404 /404.html;
The response code stays 404, and people see your page. If you're still choosing where to host, there's a comparison of Vercel vs Netlify.
Step 3. Next.js: the not-found file
In Next.js with the App Router, the 404 page is the file app/not-found.tsx:
import Link from 'next/link';
export default function NotFound() {
return (
<main>
<h1>This page doesn't exist</h1>
<p>It may have moved, or there's a typo in the address.</p>
<Link href="/">Back to home</Link>
</main>
);
}
Unknown addresses land here on their own. But there's a second case: the address looks valid, but there's no data. For example, /blog/getting-started, when no such post exists in the database. Then call notFound():
import { notFound } from 'next/navigation';
export default async function Post({ params }) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound();
return <article>{post.title}</article>;
}
Next.js adds noindex to that page by itself. One subtlety from the docs: call notFound() before the first <Suspense>. If the page has already started streaming, the response code stays 200 — only the noindex works.
Step 4. React or Vite SPA: the "*" route
In a single-page app, the server returns the same index.html with a 200 code for any address. The router in the browser figures out the address. Add a catch-all for anything unknown at the end of your routes:
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Routes>
People will see your page. But the server has already answered 200 — that's the soft 404. Google's documentation offers two ways out:
- Redirect with a script to an address where the server honestly answers
404, for example/not-found. - Add
noindexfrom the page code so Google doesn't index it:
const meta = document.createElement('meta');
meta.name = 'robots';
meta.content = 'noindex';
document.head.appendChild(meta);
The second option is simpler: run this code when the NotFound component appears on screen.
Step 5. Check the response code
Open a terminal and request an address that definitely doesn't exist:
curl -I https://example.com/this-page-does-not-exist
The first line of the response should contain 404. No terminal? DevTools, Network tab: reload the page and check the Status column of the very first request.
For an SPA, also check the Elements tab: the 404 page's <head> should now contain <meta name="robots" content="noindex">.
Once your site is in search, look at Search Console, in the page indexing report. It has a separate status for soft 404s. If your addresses show up there, go back to steps 3 and 4. And if the site doesn't appear in search at all, start with why my site isn't showing up in Google.
What you get
Any address that doesn't exist opens your page: in your site's design, with clear text and links onward. People don't hit a grey hosting placeholder and close the tab.
And the server — or noindex in an SPA — honestly tells Google this page doesn't exist. Junk addresses with typos stay out of search.
Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.
Do I need noindex if the server already answers 404?
No. Google doesn't add pages with a 404 code to search anyway. noindex is for places where you can't return an honest code — single-page apps.
I added 404.html on Netlify, but it doesn't show up. Why?
Most likely you have an SPA with the rule /* /index.html 200. With it, every address matches a file — index.html — so 404.html never gets its turn. The "not found" page is then drawn by the router, as in step 4.
A page was renamed. Should the old address return 404 too?
No. If the content moved to a new address, Google recommends a permanent redirect — a 301 code. People and search engines go straight to the new page. Keep 404 for things that are gone for good.





