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
falsefrom thedo_redirect_guess_404_permalinkfilter.
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/),$pathincludes/blog, so match the left side of the map to that. - Use
301for permanent moves and302for 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
20so 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.phpin 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 )ornoindex. Just check with curl that404comes back. - Redirect old URLs with
wp_safe_redirect()intemplate_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)