Fix empty WordPress /page/ URLs: a plugin that returns 404 for pages that do not exist
Published Updated 3 min read
Every WordPress archive paginates, and /page/2/, /page/3/ and beyond exist from the moment a theme is active. WordPress puts no ceiling on them. A site with 15 posts at 5 per page has three real archive pages, yet /page/4/ up to any number you like still resolves, returning a near-empty loop that looks like the front page or a blank archive shell depending on the theme. Neither sends a 404.
Crawlers probe these URLs routinely, which costs in two ways. Every request to /page/400/ is crawl budget not spent on real content, and on small sites or shared hosting that adds up. And if /page/4/ renders the same shell as /page/1/ with an empty loop, Google sees two near-identical documents. It generally copes without penalising the site, but it is work that serves nobody.
Heavy bot floods of hundreds of requests a minute belong in a CDN WAF rule or the nginx config, since a plugin still starts PHP for every request. For everyday crawl hygiene, though, a plugin that sends the right 404 header and loads your theme's 404 page fits: no server access, a thirty-second install, any host.
Why WordPress does not return 404 for /page/999/
WordPress registers rewrite rules for /page/N/ without checking the number. The main query runs, finds no posts for that page and hands over to template loading, and what happens next is up to the theme. A careful theme checks have_posts() and falls through to its 404 template; a simpler one renders the archive anyway. Either way, WordPress does not send the 404 status code crawlers use to decide whether to keep a URL.
A blog-loop front page is worse. WordPress sometimes answers any /page/N/ request with the first page of posts instead of noticing the number exceeds the total, so a crawler asking for /page/200/ gets the homepage's first ten posts with a 200 status: duplicate content with the wrong signal.
Checking the page number before the template loads
The plugin hooks the wp action, which runs after the main query and before any template. By then $wp_query->max_num_pages holds the real page count for the current archive. If get_query_var('paged') is higher, the plugin calls $wp_query->set_404(), sends a 404 status with nocache_headers() and loads the template from get_404_template(): your theme's 404.php, falling back to the parent theme or WordPress's default. To a crawler it is indistinguishable from a native 404.
Because it relies on is_paged() and max_num_pages for whatever query is running, it covers category, tag, author, date and custom post type archives as well as the front page loop. The core behaviour needs no configuration.
The noindex option: when 404 is the wrong choice
One optional checkbox adds a noindex meta tag to every paginated page after page 1. The out-of-range 404 applies whether or not it is ticked.
If your archive has 20 real pages, those pages have content and must not 404. With noindex they are still served, and crawlers still follow their links, but search engines are told not to list the pages themselves. Link equity flows through; the paginated URLs just stay out of results.
It suits archives where paginated pages repeat posts already reachable elsewhere. 80 posts at 40 per page gives two real pages, the second largely duplicating indexed content, so noindex there makes sense. 500 posts at 10 per page means page 12 may hold posts that surface nowhere else, and noindexing it loses them for no gain.
When a plugin is not enough
A Cloudflare WAF rule matching /page/* stops requests before they reach your server: no PHP, no database query, no WordPress bootstrap. Under hundreds or thousands of pagination requests a minute, that is where to block, because this plugin still needs a full WordPress load before the 404 fires.
Use the plugin for correct HTTP semantics without touching server configuration, to stop Search Console reporting crawl errors on these URLs, or when your host gives you no access to nginx or CDN rules. Use the WAF or server rule to cut load during active bot traffic. Running both is fine.
What you get
Included
- Returns 404 with the correct HTTP status header and your theme's 404 template for any page number beyond the actual archive count
- Works on all archive types: category, tag, author, date, and the front page loop
- Optional noindex meta tag for paginated pages beyond page 1, for sites that want in-range paginated pages to exist but not be indexed
- No configuration required for the core 404 behaviour: installs and works immediately
Not included
- Query Loop block pagination, which uses query parameters like ?query-7-page=2 rather than /page/ URLs
- Paginated single posts that use the nextpage tag inside post content
- Server-level or CDN-level blocking: a full PHP process still starts before the 404 fires
- Logging which out-of-range page numbers are being requested or by whom
- Multisite installations: the plugin runs per-site with no cross-network view
Questions
I use the Query Loop block, which paginates with ?query-7-page=2. Does this help?
No. It handles rewrite-based pagination, which uses the paged query variable. Query Loop pagination uses a different variable and is not covered.
Can out-of-range requests redirect to the last real page instead?
Not as built. Replace the set_404 block with wp_redirect(get_pagenum_link($max), 301) followed by exit for a permanent redirect to the last real page. A 404 is usually the cleaner signal for crawlers, but a redirect spares the rare human with a bookmarked deep URL an error page.
Build it in Steem
The button opens the builder with this guide's prompt filled in, so you can change anything before it runs. Nothing is built, and nothing is charged, until you send it.
Read the prompt (265 words)
Build a WordPress plugin called WP Pagination Guard. Plugin Name: WP Pagination Guard. Version: 1.0.0. Description: Return 404 for out-of-range paginated archive pages to prevent thin content and crawl waste. Single PHP file. At the top level (outside the class): add_action('plugins_loaded', function() { (new WP_Pagination_Guard())->init(); }); Class WP_Pagination_Guard: public init(): add_action('wp', [$this, 'guard_pagination']); add_action('wp_head', [$this, 'maybe_noindex'], 1); add_action('admin_menu', [$this, 'add_menu']); add_action('admin_init', [$this, 'register_settings']); public guard_pagination(): if (!is_paged()) return; $page = (int) get_query_var('paged'); if ($page < 2) return; global $wp_query; $max = (int) $wp_query->max_num_pages; if ($max < 1) $max = 1; if ($page > $max) { $wp_query->set_404(); status_header(404); nocache_headers(); include get_404_template(); exit; } public maybe_noindex(): $opts = get_option('wp_pagination_guard_options', []); if (!empty($opts['noindex_paged']) && is_paged()) { echo '<meta name="robots" content="noindex, follow">' . "\n"; } public add_menu(): add_options_page('Pagination Guard', 'Pagination Guard', 'manage_options', 'wp-pagination-guard', [$this, 'render_page']); public register_settings(): register_setting('wp_pagination_guard', 'wp_pagination_guard_options', ['sanitize_callback' => [$this, 'sanitize_options']]); public sanitize_options($input): return ['noindex_paged' => !empty($input['noindex_paged']) ? '1' : '0']; public render_page(): $opts = wp_parse_args(get_option('wp_pagination_guard_options', []), ['noindex_paged' => '0']); Output a div.wrap with h1 'Pagination Guard'. Show a paragraph: 'Pages beyond your actual archive count already return 404 automatically. The setting below controls how in-range paginated pages appear to search engines.' Then a form with method post, settings_fields('wp_pagination_guard'), a table.form-table with one row: label 'Noindex paginated pages', containing a label with a checkbox input[name=wp_pagination_guard_options[noindex_paged] value=1, checked if $opts['noindex_paged'] is '1'] and description text 'Add a noindex meta tag to all paginated pages (page 2 and beyond). Use this when paginated archives should be accessible but should not appear in search results.' submit_button('Save Settings'). No database table, no JavaScript, no front-end output besides the optional meta tag.
Steem