Development
Custom 404 page: how to configure and implement correctly

The 404 page is the first thing a user sees after following a broken link. How to return HTTP 404, avoid soft 404, and configure the page in Next.js, Nginx and Apache — with code and a checklist.
When a user follows a broken link, the server must return a 404 Not Found status. But in real projects this rule is broken surprisingly often: the CMS returns 200 OK, a redirect leads to the homepage, or the "404" page technically returns the right code but looks like a blank screen. Each of these situations is a distinct problem for both SEO and the user.
A properly configured 404 page solves three problems at once: it signals to Google that the URL does not exist; it keeps the user on the site and helps them find the right section; and it protects crawl budget from wasted passes. Let's work through each aspect in order.
What is a 404 page
404 Not Found is the HTTP response code a server returns when the requested resource does not exist. The code means the server is working fine and the route was resolved, but the requested document is absent. This distinguishes 404 from 500 (server error) and 503 (service unavailable).
Sites without a custom 404
Most sites show the platform's default 404 instead of a branded page with navigation
Time to deindex
Google typically removes a properly returning 404 from the index within a few weeks. Speed it up via Search Console
Most common mistake
CMS returns 200 OK instead of 404 — Google indexes empty pages and dilutes the site's topical focus
Deindexed faster
Google removes 410 (Gone) from the index noticeably faster than 404 — use it for bulk page deletions
A 404 page exists on two levels. The HTTP level is the response code in the header that the search crawler sees. The visual level is the HTML content the user sees. Both levels matter equally: a wrong HTTP code is an SEO problem; poor visual design is a UX problem.
The main rule: always return HTTP 404
The first and most important rule: an error page must return HTTP status 404 (or 410) — never 200. This is called a "hard 404" as opposed to a "soft 404".
You can check the HTTP status of any page directly in the browser: F12 → Network → reload the page → find the first request → Status column. Or via curl in the terminal:
# Check HTTP status of a page
curl -s -o /dev/null -w "%{http_code}" https://example.com/non-existent-url
# Expected output: 404
# Check full response headers
curl -I https://example.com/non-existent-urlSoft 404 and 410: the difference
Beyond the "hard 404" there are two related cases that are often confused in practice: soft 404 and the 410 status. Understanding the difference is important for managing deleted pages correctly.
| Status | HTTP code | What it means for Google | When to use |
|---|---|---|---|
| Hard 404 | 404 | Resource not found; Google removes from index within a few weeks | Page deleted or never existed |
| Soft 404 | 200 (error!) | Google sees a "successful" response and indexes empty content | Never — this is an implementation bug, not an intentional choice |
| Gone | 410 | Resource permanently deleted; Google deindexes significantly faster than 404 | Bulk page deletion (old products, obsolete sections) |
Google also classifies as soft 404 any redirect from a 404 URL to the homepage or another irrelevant page. From the search engine's perspective: the user searched for A, landed on B, and B's content does not answer the query. This degrades the quality signal of the target page.
What a good 404 page should contain
A user who lands on a 404 is already mildly frustrated. The page's job is to ease that frustration and give the person a clear next step. An effective 404 page follows a recognisable structure.
Clear error message
Explain the situation in plain language: "Page not found" or "This page doesn't seem to exist". Avoid technical jargon and error codes in the visible text.
Search box
The single most effective element on a 404 page. The user already knows what they're looking for — give them the ability to find it right here, without going back to Google.
Navigation to key sections
Links to the homepage, popular sections, or sitemap. They help users get their bearings and continue their journey through the site.
Back button
Many users want to return to where they came from. A back link or button reduces the bounce rate and improves UX.
What not to do on a 404 page: automatically redirect the user to the homepage via JavaScript (window.location.href = '/'). Google may treat this as a soft 404 — the user expected A, received B, content is irrelevant. Server-level 301 redirects from 404 URLs are only appropriate when a genuine replacement page exists.
Implementation: Next.js, Nginx, Apache
How you configure a custom 404 page depends on your stack. Let's cover the three most common setups: Next.js App Router, Nginx, and Apache.
Next.js App Router
In Next.js 13+ with App Router, a custom 404 page is created via a not-found.tsx file inside the app/ directory. For i18n (e.g. with next-intl) the file lives in app/[locale]/.
// app/[locale]/not-found.tsx
import Link from 'next/link';
export default function NotFound() {
return (
<main style={{ textAlign: 'center', padding: '4rem 1rem' }}>
<h1>404 — Page Not Found</h1>
<p>The link may be outdated or the address was mistyped.</p>
<Link href="/">Back to homepage</Link>
</main>
);
}
// Next.js automatically returns HTTP 404 for this file.For programmatic 404 generation (e.g. in a dynamic route app/[locale]/blog/[slug]/page.tsx), use the notFound() function from next/navigation. Calling this function immediately stops rendering and returns HTTP 404 + renders not-found.tsx.
// app/[locale]/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';
import { getArticleDoc } from '@/messages/blog-articles/registry';
export default async function ArticlePage({ params }: { params: { slug: string } }) {
const article = getArticleDoc(params.slug);
// If slug does not exist — return HTTP 404 and render not-found.tsx
if (!article) {
notFound();
}
return <ArticleRenderer article={article} />;
}pages/404.tsx. Next.js automatically routes to it and returns HTTP 404. The notFound() function is unavailable in Pages Router — use { notFound: true } in getStaticProps or getServerSideProps instead.// Pages Router: pages/blog/[slug].tsx (legacy approach)
export async function getServerSideProps({ params }) {
const article = getArticleDoc(params.slug);
if (!article) {
// Next.js will return HTTP 404 and show pages/404.tsx
return { notFound: true };
}
return { props: { article } };
}Nginx
In Nginx, a custom 404 page is configured via the error_page directive inside the server block. Make sure the 404.html file exists at the specified path and that serving it does not trigger an internal redirect to 200.
server {
listen 80;
server_name example.com;
root /var/www/html;
# Custom page for 404 errors
error_page 404 /404.html;
# The location for the file itself — returns 404, not 200
location = /404.html {
internal;
}
# Custom pages for other errors
error_page 500 502 503 504 /50x.html;
location = /50x.html {
internal;
}
}internal directive blocks direct external access to /404.html — the page is only accessible as an error response. Without it, users could open /404.html directly and receive a 200, creating a content duplicate.Apache / .htaccess
For Apache, the custom 404 is configured via ErrorDocument — either in the main config or in .htaccess.
# .htaccess in the site root
# Custom 404 page
ErrorDocument 404 /404.html
# You can also point to a PHP file
# ErrorDocument 404 /404.php
# Custom pages for other errors
ErrorDocument 403 /403.html
ErrorDocument 500 /500.html
# Enable mod_rewrite for SPA routing
<IfModule mod_rewrite.c>
RewriteEngine On
# For React/Vue SPA: all non-existent routes → index.html
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ /index.html [L]
</IfModule>index.html with a 200 code causes search engines to index non-existent pages. Either the SPA must add <meta name="robots" content="[noindex](/glossary/noindex)"> to 404 pages client-side, or use SSR/SSG with proper HTTP status codes.PHP (WordPress and other CMSes)
In WordPress, the custom 404 page is the 404.php file in your theme. WordPress automatically returns HTTP 404 for non-existent URLs when permalinks are configured correctly. If the CMS returns 200 instead of 404, the problem is usually in the .htaccess or server configuration.
<?php
// themes/your-theme/404.php
// WordPress automatically returns HTTP 404
get_header();
?>
<main class="error-404">
<h1>Page Not Found</h1>
<p>Try using search:</p>
<?php get_search_form(); ?>
<p>Or return to the <a href="<?php echo home_url(); ?>">homepage</a>.</p>
</main>
<?php
get_footer();Finding and fixing broken links
A properly configured 404 page is only half the job. The other half is finding all links that point to non-existent pages and resolving them. Broken links are the primary cause of 404 errors appearing in the index.
Search Console → Indexing → Pages → "Not found (404)". This is the free and most complete source: Google shows only the 404s it discovered during crawling — exactly the ones that matter for SEO.
Run a site crawl in Screaming Frog SEO Spider. Filter: Response Codes → 4xx. The tool shows not just the 404 URLs but also the source pages — exactly which pages contain broken links.
Logs contain 100% of all requests, including those Google hasn't discovered yet. Filter: grep ' 404 ' access.log | awk '{print $7}' | sort | uniq -c | sort -rn | head -20 — top 20 URLs returning 404.
For each 404 found, the decision is one of three: set up a 301 to the relevant replacement URL (if a similar page exists), remove all internal links pointing to it, or leave the 404 as-is (if no links point to it — Google will remove it from the index automatically).
# Find top-20 broken URLs in Nginx/Apache logs
grep ' 404 ' /var/log/nginx/access.log \
| awk '{print $7}' \
| sort \
| uniq -c \
| sort -rn \
| head -20
# Example output:
# 143 /old-product-page
# 87 /blog/removed-article
# 34 /wp-login.php (attack attempts — not critical)Monitoring 404 errors
A one-time audit is good. But new 404s appear constantly: pages get renamed, products deleted, external sites link to non-existent URLs. It's important to set up regular monitoring.
| Tool | What it monitors | Frequency | Free |
|---|---|---|---|
| Google Search Console | 404s discovered by Googlebot during crawling | Updated daily | Yes |
| Screaming Frog + scheduler | All 404s during a full site crawl | Weekly/monthly | Up to 500 URLs |
| Server logs (goaccess, awstats) | All 404s in real time from access.log | Real time | Yes |
| Ahrefs / Semrush Site Audit | 404s + incoming external links pointing to 404s | Weekly automated | Paid |
| UptimeRobot / Better Uptime | Uptime monitoring of key URLs | Every 1–5 minutes | Basic free tier |
For large sites (10,000+ pages) server logs are especially valuable: they cover 100% of requests, including bots, users, and scripts. Search Console only shows what Googlebot found during crawling — a subset of the full picture.
Correct 404 page checklist
HTTP status
- The 404 page returns HTTP status 404 (verified via curl or DevTools)
- No soft 404: the error message page does not return 200 OK
- No JavaScript redirect from non-existent URLs to the homepage
- 410 Gone is used for bulk-deleted pages with no replacement
- 301 redirects are configured only for URLs that have a relevant replacement page
Page content
- Clear, plain-language error message with no technical jargon
- A search box or link to site search is present
- A link to the homepage is present
- Links to 2–4 popular sections or categories are present
- Page is styled consistently with the site brand (header, footer, fonts)
Implementation
- Next.js: not-found.tsx file created; notFound() is called for non-existent slugs
- Nginx: error_page 404 directive configured; 404.html file is marked as internal
- Apache: ErrorDocument 404 is set in .htaccess or main config
- WordPress: 404.php exists in the active theme
- SPA (React/Vue without SSR): noindex meta tag is added client-side on 404 pages
Monitoring
- Google Search Console is connected and email alerts are configured
- Regular crawling via Screaming Frog or equivalent (at least monthly)
- Server logs are accessible and analyzed for high-traffic sites
- All internal links pointing to 404 URLs have been found and fixed
yourdomain.com/test-404-check (a non-existent URL), open DevTools → Network, find the first request, look at the status. If it shows 200 — you have a soft 404 and need to fix the server configuration.FAQ
Answers to common questions about configuring 404 pages that come up during audits and development.