Title: Confident 404 Redirect
Author: plugforge
Published: <strong>September 22, 2026</strong>
Last modified: September 22, 2026

---

Search plugins

![](https://ps.w.org/confident-404-redirect/assets/banner-772x250.png?rev=3707015)

![](https://ps.w.org/confident-404-redirect/assets/icon-256x256.png?rev=3707015)

# Confident 404 Redirect

 By [plugforge](https://profiles.wordpress.org/plugforge/)

[Download](https://downloads.wordpress.org/plugin/confident-404-redirect.0.1.0.zip)

 * [Details](https://wordpress.org/plugins/confident-404-redirect/#description)
 * [Reviews](https://wordpress.org/plugins/confident-404-redirect/#reviews)
 *  [Installation](https://wordpress.org/plugins/confident-404-redirect/#installation)
 * [Development](https://wordpress.org/plugins/confident-404-redirect/#developers)

 [Support](https://wordpress.org/support/plugin/confident-404-redirect/)

## Description

When a URL 404s, Confident 404 Redirect looks for the post that URL was probably
meant to reach. If it is confident, it redirects. If it is not, the visitor gets
a normal 404 page by default, or whatever fallback you choose instead.

The usual approach is to send every missing URL to the homepage. That returns HTTP
200 for a page that does not exist, which search engines read as a soft 404, and
it hides broken links instead of surfacing them. You can switch it on here, but 
it is not the default.

#### What it does

 * Matches the requested slug against your post slugs, including WordPress’ own `-
   2` duplicate suffix; a directory’s index file, `/about/index.php` or `/services/
   default.aspx`, is read as the directory it indexes, and one at the root names
   the root, which is left alone like the root itself
 * Reads a page number WordPress itself routed off a post or page address – `/old-
   slug/page/2/`, `/old-slug/2/`, `/old-slug/comment-page-2/` – as a page of `old-
   slug`, matches on the slug before it and redirects to the page itself, since 
   the corrected post may not have that page; an address WordPress did not route
   that way is read as it stands
 * Matches against post titles when the slug has changed completely
 * When two posts share a slug or a title, the rest of the requested path decides
   between them: a request one letter off below `/north/` goes to the team page 
   under `/north/`, and a tie the path cannot settle is never redirected
 * Redirects only above a confidence threshold you set, anywhere from 50% to 100%,
   and 90% by default
 * Limits matching to the post types you choose, or searches every public type a
   visitor can open
 * Optional fallback for everything else: an honest 404, 410 Gone, the homepage,
   or a URL of your own that WordPress will redirect to. A fallback redirect is 
   sent as a temporary 302, whatever status the matched redirects carry: nothing
   was found at the missing address, so nothing permanent is said about it. An address
   of this site spelled without a scheme takes the site’s own scheme
 * Takes over WordPress’ own permalink guessing by default, and hands it back if
   you would rather keep it
 * Carries the recognised campaign tags (a fixed list: `utm_source` and its eight
   siblings, plus `gclid`, `fbclid` and the other click identifiers) across same-
   origin redirects, so attribution survives the move; everything else in the request
   query stays behind, no request parameter is appended to a redirect on another
   origin – another host or port, or a downgrade to http; the https upgrade of the
   same host is the one origin change the tags cross, because WordPress’s own canonical
   redirect keeps the scheme a request came on – and a query flooded with parameters
   is treated as noise. The actual Location produced by WordPress’ redirect filters
   is checked again, so a late filter cannot move a locally carried query to another
   origin. A query already configured on the destination URL remains part of that
   destination
 * Path exclusions with wildcards, matched against the path after the site’s own
   address: on a site at `example.org/blog/`, `/shop/*` covers `/blog/shop/`, and
   a line spelled with the site’s own path is flagged on the settings screen, with
   the spelling that would match; a full address of this site – its own host and
   port, on either scheme, an internationalised domain in its punycode or its own
   alphabet where PHP’s intl extension is available – is stored as the path it names,
   and one naming this host on another port, or another site altogether, is flagged
   too, as is a fallback URL WordPress no longer redirects to; a doubled leading
   slash before a plain name is read as the path it is, an address of this host 
   outside the site’s own directory is flagged as such, and a line that would match
   every path is named on the screen as switching the plugin off

#### What it does not do

 * **No front-end assets.** No stylesheet and no script are loaded on any public
   page.
 * **No `.htaccess` changes.** The plugin never writes to your server configuration.
 * **No dashboard notices.** Nothing appears outside its own settings screen.
 * **No ads, no upsell popups, no review nags.**
 * **No database writes on the public site outside a 404.** A page that loads normally
   costs nothing at all; the first eligible 404 of the day may refresh one cached
   count of your content, and so may opening the plugin’s settings screen, which
   shows that count.

#### Always excluded

Anything that asks for a file rather than a page. A URL ending in a recognised extension(
one to fifteen ASCII characters, at least one of them a letter) is left alone unless
the extension is one a page can have, such as `.html`, `.php` or `.aspx` from a 
site migrated off another system, so a format nobody anticipated is excluded rather
than answered with HTML.

Also anything under the uploads directory and WordPress’s own directories wherever
the site keeps them – its own directory when it lives in one, and the admin, includes
and content directories – feeds, sitemaps, the embed and trackback endpoints and
the endpoints plugins register (a shop’s account pages, AMP), any path with a hidden
dot-prefixed directory in it, `robots.txt`, `favicon.ico`, `.well-known`, `wp-admin`,`
wp-json`, REST, XML-RPC, cron, WP-CLI, the site root however it is spelled (`/index.
php?p=123` included), and any request that is not GET or HEAD. A path is left alone
when its percent escapes are malformed, decoding would introduce a new separator,
or the decoded spelling contains a backslash, invalid UTF-8, a control or line separator,
or one of the invisible characters WordPress itself strips from a slug – the zero-
width and bidi characters, the soft hyphen, the byte order mark. The plugin refuses
the whole spelling rather than repairing it into another path. The marks WordPress
keeps in a slug, such as the variation selector an emoji ends in, are part of the
address and stay valid.

#### For developers

Filters: `confident_404_redirect_engines`, `confident_404_redirect_match`, `confident_404_redirect_target`,`
confident_404_redirect_carried_params`, `confident_404_redirect_post_types`, `confident_404_redirect_candidate_limit`,`
confident_404_redirect_scan_limit`, `confident_404_redirect_ambiguity_margin`.

    ```
    confident_404_redirect_post_types may narrow, restore or reorder the searchable public post types. Values outside that public, viewable set are discarded.
    ```

A redirect target is sent exactly in the spelling it was judged in: an absolute 
URL on a host WordPress accepts, on a port a socket can have, or a path with a leading
slash. A relative path without one would have to be resolved against the request
that just 404ed, so it is refused instead.

A callback that throws – on any hook this plugin fires, or on a WordPress filter
it consults, and whatever it throws, the plugin’s own `Read_Failure` included – 
ends the request the way it would anywhere in WordPress. Core has no way to unwind
a hook frame, and this plugin does not pretend to: a swallowed throw would leave
the dispatcher mid-frame for the rest of the request, with the later callbacks on
that hook never run and `current_filter()` answering a hook that had finished. What
the plugin does absorb is what never crossed a hook – a third-party engine that 
throws from its own code, or a `Match_Result` built with an impossible score – and
those stand the whole verdict down for that request, no redirect, no suggestion,
no fallback and no action, the same as a read the database refused.

Actions: `confident_404_redirect_before`, `confident_404_redirect_no_match`. The
before-redirect action fires only after the plugin has applied WordPress’ redirect
filters and accepted their final Location and status as safe; its URL and status
are the final values after those filters and URL sanitisation. The no-match action
fires before the configured fallback, which may still redirect – as a temporary 
302, unless a `wp_redirect_status` filter says otherwise – or return 410. Neither
the action nor the fallback runs for a page the plugin found but may not send – 
a confident match whose redirect is refused or cancelled, whether as a loop, as 
an address WordPress will not accept, or by a filter’s answer, and an engine or 
match filter whose winner has such an address and could have been sent – past the
confidence threshold, and not beaten by a readable candidate: that page exists, 
and the request ends as an honest 404. Among equally close candidates, one with 
such an address counts at the tie’s own score: the rest of the path might have named
it, and that cannot be checked without its address, so the request ends as a 404
where, with every address readable, a tie the path leaves standing would take the
fallback. A refused candidate the plugin would not have sent anyway is set aside,
and the rest of the verdict, fallback included, goes on without it. Where a refusal
does end the verdict, the `confident_404_redirect_match` filter is still asked, 
with null, and a result it returns replaces the refused page only when it is safe
and confident enough to send.

When no redirect is sent, a safe candidate can still be useful to the 404 template.
Usually it stayed below the confidence threshold; a safe original candidate also
remains available when WordPress or a target filter cancels or rewrites the redirect.
A refused unsafe or same-page URL is never published as a suggestion. A theme can
offer a surviving candidate from `404.php`:

    ```
    $match = PlugForge\Confident404\Redirector::suggestion();

    if ( $match ) {
        printf(
            '<p>Did you mean <a href="%s">%s</a>?</p>',
            esc_url( $match->url() ),
            esc_html( $match->title() )
        );
    }
    ```

The result also carries `score()`, `object_id()` and `engine()`. It is null when
the plugin redirected, found nothing worth offering, found a page whose address 
it may not send, or could not get an answer from the database and declined to guess.

Matching engines implement a single interface, so adding your own takes a few lines.
The contract it has to keep: `should_run()` and `find()` may throw `Read_Failure`
to say the database could not answer, and the whole verdict stands down for that
request, fallback included; any other exception thrown from the engine’s own code
stands it down the same way rather than reaching a Location header, while one thrown
from inside a hook the engine ran propagates as it would anywhere; and a `Match_Result`
score has to be a finite number from 0 to 100 – the constructor refuses anything
else, and an engine that tries counts as a verdict nobody could reach. An engine
whose winner has an address the plugin may not send throws `Refused_Target::scored()`
with the most that winner could have earned – for a tied candidate, the tie’s own
score before any demotion for the tie – rather than returning null: null lets the
next engine answer for a page it was never about. The verdict then ends as an honest
404 when that score could have been sent and no readable candidate clearly beats
it, and the refusal is set aside otherwise.

## Screenshots

[⌊The settings screen: what is matched, how confident a match has to be before a
redirect is sent, and what happens when no redirect is.⌉⌊The settings screen: what
is matched, how confident a match has to be before a redirect is sent, and what 
happens when no redirect is.⌉[

The settings screen: what is matched, how confident a match has to be before a redirect
is sent, and what happens when no redirect is.

## Installation

 1. Install and activate the plugin.
 2. Go to **Settings  Confident 404 Redirect**.
 3. The defaults are the recommended setup, so you do not have to change anything.

## FAQ

### Why not redirect everything to the homepage?

The visitor asked for a specific page and the homepage is not it. A 200 response
for a URL that does not exist is also a soft 404, which tells search engines your
site is padded with thin pages. If you want that behaviour anyway, it is one setting
away. It is not the default.

### Does it slow my site down?

No. Nothing runs on a page that loads normally. A unique exact slug hit costs one
indexed lookup, plus a permalink read for whatever it found – one query on a plain
permalink structure, a few more where the structure asks for categories or ancestors–
unless that post is already in the cache. A slug several posts share pays that permalink
read per candidate.

A miss costs more. The near-match pass reads up to four indexed prefix scans, widest
first – the first word of the slug, then two, three, and the slug missing its last
character – and stops at the first that comes back whole and contains the narrower
ones – which the prefixes of one slug normally do, though a slug filter can make
them not, and then the ladder reads on; on a site where the first word is rare that
is one scan. Title matching, where it runs, adds one more: a single bounded read
of the titles themselves – one row past the scan threshold at most, which is how
it notices a corpus that outgrew the guard and stands down – scored in PHP, which
is also what lets a Latin URL find the Cyrillic title a slug plugin transliterated
it from. A URL that matches nothing at all costs up to seven queries in the measured
baseline, and a slug a dozen posts share, where each candidate’s permalink is read,
measured at about thirteen – both numbers from a plain permalink structure, which
is what the benchmarks run; a category-based structure adds its own reads to every
permalink.

Once a day, the first 404 that gets as far as title matching also refreshes a cached
count of your content, which adds a few queries and, on a site of twenty thousand
posts, tens of milliseconds. When the cache retains that count, later 404s read 
it; when storage cannot retain it, title matching alone stands down after the count
and asks again later instead of paying for an unguarded title scan – slug matching,
your fallback and the no-match action carry on, because the database did answer.
A count the database itself refuses is different: that stands the whole matcher 
down for the request, fallback included, because an unanswered read is not a 404
verdict. A 404 that resolves on a unique, perfect slug match never asks for the 
count at all – anything less certain still consults it before title matching weighs
in.

### Why did it not redirect when there was an obvious match?

Several things can stop it, and they all come down to the same rule: a permanent
redirect to the wrong page is worse than a 404. The three most common are these.

The score was below your confidence threshold, which you can lower under Settings.

Or the two best candidates were too close to tell apart, whether the slug engine,
the title engine, or one of each found them. Two pages called `Contact` score the
same; two slugs one character away from what was asked for score within a point 
or two of each other, and that gap measures their length rather than how close they
are. Two engines finding the same post still count as one candidate. A close pair
of different posts identifies neither, so neither earns a permanent redirect. The
rest of the requested path can still settle it: two pages slugged or titled alike
under two parents are told apart by the parent the request was made below; a request
at the root prefers the candidate at the root, so a site whose translations share
a slug under a language directory still gets its default-language near misses; two
candidates at one address – a post and a page sharing a slug – are one destination
and no tie; a request on one language domain prefers the translation on that domain;
when one of them has an address the plugin would not send nothing is offered at 
all; and the comparison ignores ASCII case, as WordPress does when it routes a path.`
confident_404_redirect_ambiguity_margin` sets how wide the gap has to be, and zero
restores acting on anything that is not an exact draw.

Or the URL pointed into a crowded group: a slug that more than five posts share 
exactly, or a prefix of the requested slug – down to its first word – that more 
slugs share than the plugin reads at once, twenty-five unless `confident_404_redirect_candidate_limit`
says otherwise. There the plugin can only see a sample of the group, and the post
you wanted may not be in it, so the closest it saw is offered as a suggestion and
never sent. Either kind of crowd holds back every other match for that request too,
a title match included: a title unique among those posts is no evidence which of
them the visitor meant, and a slug the plugin did not read may be closer than the
title. On a site where more slugs than that share the first word of a mistyped address,
this means such a typo is offered as a suggestion rather than redirected.

When there is a usable near miss, the plugin offers it to your 404 template instead,
so a theme can still ask the visitor whether that was what they meant. That includes
a near miss the plugin deliberately held below its score floor – a crowded slug,
a tie, a truncated sample – which is offered but never redirected. A title whose
raw similarity falls below the floor is noise and is never offered; nor is a URL
that is unsafe or resolves back to the same request, or a match abandoned because
the database could not answer.

### Does it work with caching plugins?

Yes, with one caveat that belongs to the cache rather than the plugin: WordPress
can only redirect a 404 it actually serves. Most page caches pass 404s through, 
which is their common default, and the plugin adds nothing to pages that are cached.
A server cache or CDN configured to cache 404 or redirect responses will keep serving
its stored copy until it expires or is purged.

### Does it work with HyperDB or LudicrousDB?

Yes. The plugin watches its own database reads through wpdb’s `query` filter and
its query counter, which every drop-in built on wpdb keeps. wpdb’s `ready` flag 
is read only on a stock wpdb: those drop-ins never set it, and reading it there 
once stood the plugin down on every request behind them. A connection that dies 
mid-request is read as an unanswered read, not as an empty site: WordPress reports
that failure to nobody once the template stage has begun, and the plugin reads the
result handle it leaves behind.

### Will it redirect my images, or my JSON and CSV files?

No. Anything under this site’s own uploads directory – the one on this site’s host
and port; uploads moved to another host, or another port of this host, are another
site’s – is excluded, and so is any URL ending in a recognised file extension, unless
that extension is one a page can have. The rule works that way round on purpose:
a format nobody thought of is left alone rather than answered with a page.

### The settings screen says the site address cannot be read. What does that mean?

The plugin reads the site’s home address once per request and judges every path 
and its own directories against it, while a redirect target and the loop check are
judged against the origin the visitor actually requested, so it has to be a plain
http or https address of an ASCII host, on a port a socket can have, with no credentials.
An internationalised domain is spelled in punycode there, as WordPress itself expects;
a `home_url` filter has to answer an address. Until it does, the plugin stands down–
no redirect, no fallback – and, under the default guessing setting, hands WordPress
its own permalink guessing back rather than hold it down for nothing.

### Why is title matching switched off on my site?

WordPress does not index post titles, so searching them means reading every title,
on every 404 that gets that far. The cost grows with your content – tens of milliseconds
a few thousand items in, and around a second at the twenty-thousand ceiling on the
benchmark site – and crawlers produce 404s in bulk, so above a threshold of 2,000
items the plugin stands the title engine down. Slug matching keeps working.

The count is of the content the plugin is set to search, so narrowing the post types
narrows it too. The threshold is filterable through `confident_404_redirect_scan_limit`,
up to a hard ceiling of twenty thousand. The title pass costs the corpus size per
404, linearly, times a per-title price that grows with title length – measured at
that ceiling over ordinary titles, a miss across twenty thousand titles takes around
a second on the benchmark site, and a corpus of titles hundreds of characters long
can multiply that – so raising the threshold trades 404 latency for reach, and the
ceiling is where that trade stops being offered.

## Reviews

There are no reviews for this plugin.

## Contributors & Developers

“Confident 404 Redirect” is open source software. The following people have contributed
to this plugin.

Contributors

 *   [ plugforge ](https://profiles.wordpress.org/plugforge/)

[Translate “Confident 404 Redirect” into your language.](https://translate.wordpress.org/projects/wp-plugins/confident-404-redirect)

### Interested in development?

[Browse the code](https://plugins.trac.wordpress.org/browser/confident-404-redirect/),
check out the [SVN repository](https://plugins.svn.wordpress.org/confident-404-redirect/),
or subscribe to the [development log](https://plugins.trac.wordpress.org/log/confident-404-redirect/)
by [RSS](https://plugins.trac.wordpress.org/log/confident-404-redirect/?limit=100&mode=stop_on_copy&format=rss).

## Changelog

#### 0.1.0

 * First release: slug and title matching, a confidence threshold, post type limits,
   fallback modes, path exclusions, recognised campaign tags carried across same-
   origin redirects, and control over WordPress’ built-in permalink guessing.

## Meta

 *  Version **0.1.0**
 *  Last updated **1 day ago**
 *  Active installations **Fewer than 10**
 *  WordPress version ** 6.2 or higher **
 *  Tested up to **7.1.2**
 *  PHP version ** 7.4 or higher **
 * Tags
 * [404](https://wordpress.org/plugins/tags/404/)[404 redirect](https://wordpress.org/plugins/tags/404-redirect/)
   [broken links](https://wordpress.org/plugins/tags/broken-links/)[redirect](https://wordpress.org/plugins/tags/redirect/)
   [seo](https://wordpress.org/plugins/tags/seo/)
 *  [Advanced View](https://wordpress.org/plugins/confident-404-redirect/advanced/)

## Ratings

No reviews have been submitted yet.

[Your review](https://wordpress.org/support/plugin/confident-404-redirect/reviews/#new-post)

[See all reviews](https://wordpress.org/support/plugin/confident-404-redirect/reviews/)

## Contributors

 *   [ plugforge ](https://profiles.wordpress.org/plugforge/)

## Support

Got something to say? Need help?

 [View support forum](https://wordpress.org/support/plugin/confident-404-redirect/)