Development

Custom 404 page: how to configure and implement correctly

Article cover: custom 404 page — how to configure and implement

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.

Google does not penalize sites for having 404 pages. Problems come not from the 404 code itself, but from wrong implementation: when 404 returns 200 OK, when internal links point to 404 URLs, or when thousands of pages are deleted without 301 redirects.

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).

~74%

Sites without a custom 404

Most sites show the platform's default 404 instead of a branded page with navigation

Weeks

Time to deindex

Google typically removes a properly returning 404 from the index within a few weeks. Speed it up via Search Console

Soft 404

Most common mistake

CMS returns 200 OK instead of 404 — Google indexes empty pages and dilutes the site's topical focus

410

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".

Critical mistake: returning HTML with a "Page not found" message under HTTP status 200 OK. Google sees a "successful" response and queues the page for indexing. As a result, hundreds of meaningless URLs with identical content appear in the index.

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:

BASH
# 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-url
Quick browser check: open DevTools (F12) → Network tab → navigate to the 404 page → look at the Status column of the first request. It must show 404, not 200.

Soft 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.

StatusHTTP codeWhat it means for GoogleWhen to use
Hard 404404Resource not found; Google removes from index within a few weeksPage deleted or never existed
Soft 404200 (error!)Google sees a "successful" response and indexes empty contentNever — this is an implementation bug, not an intentional choice
Gone410Resource permanently deleted; Google deindexes significantly faster than 404Bulk page deletion (old products, obsolete sections)
Soft 404 is not a deliberate developer choice — it is a bug. It most commonly occurs in WordPress and other CMSes where the 404 template is embedded in the main layout, which always returns 200. Check your 404 page right now using curl or DevTools.

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.

How a custom 404 page keeps users on the site.

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.

The most effective e-commerce solution: show popular products or categories on the 404 page. A user who followed a broken link to a specific product is very likely interested in that category.

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]/.

TSX
// 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.

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} />;
}
In Pages Router (Next.js <13), the custom 404 page is different: create 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.
TSX
// 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.

NGINX
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;
    }
}
The 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.

APACHE
# .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>
Be careful with SPA routing in Apache and Nginx: serving all 404 routes as 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
<?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();

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.

Step 1Find them via Google Search Console

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.

Step 2Crawl the site with Screaming Frog

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.

Step 3Check server logs

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.

Step 4Fix or set up redirects

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).

Code implementation example for the 404 page:
BASH
# 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)
Do not set up 301 redirects from all 404 URLs to the homepage — this is called a "redirect chain to homepage" and Google treats it as a soft 404. A redirect is only justified when the target page is topically close to the deleted one. If there's no replacement — a proper 404 is the right answer.

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.

ToolWhat it monitorsFrequencyFree
Google Search Console404s discovered by Googlebot during crawlingUpdated dailyYes
Screaming Frog + schedulerAll 404s during a full site crawlWeekly/monthlyUp to 500 URLs
Server logs (goaccess, awstats)All 404s in real time from access.logReal timeYes
Ahrefs / Semrush Site Audit404s + incoming external links pointing to 404sWeekly automatedPaid
UptimeRobot / Better UptimeUptime monitoring of key URLsEvery 1–5 minutesBasic 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.

Set up alerts in Google Search Console: Settings → Email notifications. GSC will send a notification if the number of 404s spikes sharply — for example, after a deploy that accidentally broke routing.

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
Check your 404 page right now: navigate to 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.

No. A page returning HTTP 404 will not be indexed by Google — regardless of whether noindex is present. Google simply won't add it to the index. If you return 200 OK with noindex, Google may temporarily process the page as indexable before reconsidering. The correct HTTP code matters more than any meta tags.
Google will remove it from the index on its own — usually within 1–4 weeks after Googlebot revisits the URL. To speed up removal: use Search Console → URL Removal tool. If there's a topically close replacement page — set up a 301 redirect: it passes link equity and updates rankings faster.
It should be. A branded 404 page with navigation keeps users on the site and reduces bounce rate. Returning HTTP 404 in the header does not prevent the page from being visually appealing. The key is returning the correct HTTP code and giving the user a clear next step.
For most sites — yes, one universal 404 page is fine. For e-commerce you can add customisation: detect from the URL pattern which category the user came from and show relevant products. But this complexity is only justified when 404 pages receive significant traffic.
Crawl budget is the number of pages Googlebot is willing to crawl on your site in a given period. If internal links point to 404 URLs, the bot will revisit those dead addresses repeatedly instead of crawling live pages. For sites with millions of URLs this is critical: remove all internal links pointing to 404s.