2026年11月12日

2026年11月12日

Customize the WordPress 404 Page: 404.php, Redirects and Logging

This article is a localized edition of the Japanese original. Read the Japanese original

When someone opens a URL that does not exist, WordPress shows the page using the theme's 404 template and returns HTTP status code 404. The default works, but adding a search form and paths to your posts lets readers look for what they wanted without leaving.

The code in this article was syntax-checked with WordPress 6.9 and PHP 8.3 as of November 2026.

Which template is used

Theme type 404 template Where to edit it
Classic theme 404.php in the theme folder (index.php if it is missing) Edit the file
Block theme (Twenty Twenty-Five and others) templates/404.html Appearance → Editor → Templates → "404"

With a block theme, you can rebuild it without code: open the 404 template in the Site Editor and add a "Search" block or a "Latest Posts" block. The examples below are for classic themes.

If you use a commercial theme, editing the theme directly means your changes are lost on update, so put 404.php in a child theme. A child theme's 404.php takes precedence over the parent theme's. How templates are chosen is explained in the article on the template hierarchy.

Creating 404.php

Create 404.php in the theme (or child theme) folder. Match class names such as .site-main to your theme's index.php.

<?php
/**
 * Template for the 404 page
 */
get_header();
?>
<main id="main" class="site-main error-404">

    <header class="page-header">
        <h1 class="page-title">Page not found</h1>
        <p>The page you are looking for may have been moved or deleted, or the URL may be wrong.</p>
    </header>

    <section class="error-404-search">
        <h2>Search this site</h2>
        <?php get_search_form(); ?>
    </section>

    <?php
    $recent = new WP_Query(
        array(
            'posts_per_page'      => 5,
            'post_status'         => 'publish',
            'ignore_sticky_posts' => true,
            'no_found_rows'       => true,
        )
    );
    if ( $recent->have_posts() ) :
        ?>
        <section class="error-404-recent">
            <h2>Recent posts</h2>
            <ul>
                <?php
                while ( $recent->have_posts() ) :
                    $recent->the_post();
                    ?>
                    <li>
                        <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
                        <time datetime="<?php echo esc_attr( get_the_date( 'c' ) ); ?>"><?php echo esc_html( get_the_date() ); ?></time>
                    </li>
                <?php endwhile; ?>
            </ul>
        </section>
        <?php
        wp_reset_postdata();
    endif;
    ?>

    <section class="error-404-categories">
        <h2>Categories</h2>
        <ul>
            <?php
            wp_list_categories(
                array(
                    'orderby'    => 'count',
                    'order'      => 'DESC',
                    'show_count' => true,
                    'title_li'   => '',
                    'number'     => 10,
                )
            );
            ?>
        </ul>
    </section>

    <p class="error-404-home">
        <a href="<?php echo esc_url( home_url( '/' ) ); ?>">Back to the home page</a>
    </p>

</main>
<?php
get_footer();

The body gets the class error404, so you can style the 404 page alone with body.error404.

Checking that the status code is 404

WordPress sends status code 404 itself when it determines that no matching content exists. You do not need to call status_header( 404 ) again in 404.php or functions.php.

To check, request a URL that does not exist with curl (replace example.com with your domain).

curl -s -o /dev/null -w "%{http_code}\n" https://example.com/no-such-page-12345/

If it prints 404, everything is fine. If it prints 200, check whether a plugin or the theme is forwarding 404s to the home page or elsewhere. Returning 200 for a page that does not exist, or redirecting every 404 to the home page, is treated by search engines as a "soft 404."

Search engines do not index pages that return status code 404, so you do not need to add a noindex meta tag to the 404 page either.

Redirecting old URLs to new ones

Redirects WordPress does automatically

WordPress does the following two things without any setup.

  • Posts whose slug changed: When you change the slug of a published post, the old slug is recorded and requests for the old URL are redirected to the new URL with a 301.
  • URL guessing: If there is a post whose slug resembles the end of the URL that returned 404, WordPress redirects to that post. To stop this guessing, return false from the do_redirect_guess_404_permalink filter.

Keeping your own redirect map

When a site restructure changes URL patterns in bulk, redirect with a map. Write it in functions.php or, so it does not depend on the theme, in a file placed in wp-content/mu-plugins/.

<?php
add_action( 'template_redirect', function () {
    if ( ! is_404() ) {
        return;
    }

    // The path without the query string (for example /old-page/)
    $path = wp_parse_url( $_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH );
    if ( ! is_string( $path ) ) {
        return;
    }

    // old path => new path
    $redirects = array(
        '/old-page/'          => '/new-page/',
        '/old-category/post/' => '/new-category/post/',
    );

    if ( isset( $redirects[ $path ] ) ) {
        wp_safe_redirect( home_url( $redirects[ $path ] ), 301 );
        exit;
    }
} );
  • If WordPress is installed in a subdirectory (such as /blog/), $path includes /blog, so match the left side of the map to that.
  • Use 301 for permanent moves and 302 for temporary ones.
  • If the map grows beyond a few dozen entries, or you want to edit it in the admin screen, the Redirection plugin is easier to manage. Redirection also has a 404 log.

Logging URLs that return 404

You can first check which URLs return 404 in the server's access log. If you cannot see the server logs, log them with PHP.

Write the log somewhere not reachable from the web (outside the public directory). If you put it inside wp-content, anyone who knows the URL can read the log. Define the log location in wp-config.php (adjust the path to your server).

// Add above the "That's all, stop editing!" line in wp-config.php
define( 'MYTHEME_404_LOG', '/home/USER/logs/404.log' );
<?php
add_action( 'template_redirect', function () {
    if ( ! is_404() || ! defined( 'MYTHEME_404_LOG' ) ) {
        return;
    }

    // Remove control characters such as line breaks and cut overly long values
    $clean = function ( $value ) {
        $value = preg_replace( '/[\x00-\x1F\x7F]/', '', (string) $value );
        return substr( $value, 0, 500 );
    };

    $line = sprintf(
        "%s\t%s\t%s\t%s\n",
        gmdate( 'Y-m-d H:i:s' ),
        $clean( $_SERVER['REQUEST_URI'] ?? '' ),
        $clean( $_SERVER['HTTP_REFERER'] ?? '-' ),
        $clean( $_SERVER['HTTP_USER_AGENT'] ?? '-' )
    );

    error_log( $line, 3, MYTHEME_404_LOG ); // 3 = append to the given file
}, 20 );
  • The priority is 20 so logging runs after the redirect handling (priority 10). URLs that were redirected are not logged.
  • Create the log folder beforehand and make it writable by PHP.
  • Automated attack traffic also produces lots of 404s, so delete the log regularly or rotate it with the server's logrotate or similar.

Summary

  • Rebuild the page with 404.php in a classic theme, or with the 404 template in the Site Editor in a block theme.
  • Put a search form, recent posts and category links on the 404 page, and make it easy to get back to the home page.
  • WordPress returns the 404 status code itself, so there is no need to add status_header( 404 ) or noindex. Just check with curl that 404 comes back.
  • Redirect old URLs with wp_safe_redirect() in template_redirect, or use the Redirection plugin if there are many.

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲