Documentation
M 404 Handler
M 404 Handler is a redirect manager and 404 toolkit for WordPress: unlimited rules with the status code and match mode you choose, an aggregated log of every broken link, and the option to serve a real WordPress page as your 404 response. It was built for the administrator who has just moved or rebuilt a site and needs the old URLs to keep working. No asset is enqueued on the front end, and no 404 is redirected anywhere until you switch the fallback on. Two things in this manual are not optional reading: broad rules reach the REST API and therefore the block editor, and two of the settings cannot be saved from the Settings screen at all.
Installing and activating
M 404 Handler installs like any other WordPress plugin. It needs WordPress 6.0 and PHP 7.4 or newer, and it requires no account, no licence key and no external service. Activation creates its database tables and writes its default settings, but it changes nothing your visitors can see.
- Upload the plugin folder to /wp-content/plugins/, or install the ZIP through Plugins, Add New Plugin, Upload Plugin.
- Activate it on the Plugins screen. On a multisite network, activating for the network installs the tables on every existing site, and any site created afterwards gets them automatically. That loop runs inside the single activation request with no limit on the number of sites, so on a large network it can time out part-way; the sites it did not reach create their tables on their own next page load, and rules there stay dead until then.
- Open the new 404 Handler menu in the admin sidebar. The plugin's row on the Plugins screen also gains two shortcuts, Redirects and Settings.
- Two tables are created: wp_m404_redirects and wp_m404_404_log, with your own table prefix in place of wp_.
- The autoloaded option m404_settings is created with the shipped defaults, alongside m404_rule_counts holding three zeroes and m404_db_version.
- A daily cron event named m404_cleanup is scheduled, first run one hour after activation. It deletes log rows not hit within the retention window, in batches of 1000. If the event is ever cleared, the plugin reschedules it on the next page load.
- 404 logging is on, retention is 30 days and IP anonymisation is on.
- Automatic 404 handling is off. The shipped mode is the theme's normal 404 page, which means the plugin does nothing to a 404 beyond logging it.
- No redirect rules exist yet, so the matcher does no database work at all on the front end.
Where the screens live in wp-admin
The plugin adds one top-level menu called 404 Handler, sitting at position 80 in the admin sidebar. Every one of its screens requires the manage_options capability, so only administrators can open them. Editors, authors and shop managers will not see the menu at all. The interface ships in English, so unless a translation is installed these are the exact labels you will see on a Lithuanian or Polish site.
- 404 Handler, then Redirects: the rule list, with an Add New button. URL: admin.php?page=m404-redirects
- 404 Handler, then 404 Log: the aggregated record of broken links. URL: admin.php?page=m404-logs
- 404 Handler, then Settings: every setting group, under the page heading 404 Handler Settings. URL: admin.php?page=m404-settings
- Screen Options on the two list screens offers Redirects per page and Log entries per page. Default 20, accepted range 1 to 500, stored per user.
The Redirects screen and the redirect form
Redirects is the plugin's home screen. Add New opens a five-field form, and a sixth row appears when you arrive from the 404 Log through Create redirect. Every rule you create lands in a standard WordPress list table with search, filters, sortable columns and bulk actions. Targets may be a site-relative path beginning with a slash, or a full http or https URL. Protocol-relative targets in the //host form are rejected on save, and so is any other scheme such as javascript. An Exact rule whose target normalises to its own source is refused outright. That last check covers Exact rules only, which matters more than it sounds: see the dangerous operations chapter.
- Source, required. For Exact and Prefix, a site-relative path such as /old-page/. For a regular expression, a pattern without delimiters such as ^/old-blog/([0-9]+)/(.+)$
- Match type: Exact match (the default), Prefix match, or Regular expression.
- Target, required. A path such as /new-page/ or a full URL such as https://example.com/new-page/.
- Redirect type: 301, 302, 303, 307 or 308. The form opens on 301, and 301 is also what the plugin falls back to if the submitted code is not one of the five.
- Status: an Enabled checkbox, ticked by default on a new rule. The redirect goes live the moment you save it.
- Sixth row, only when you came from the log: a checkbox offering to remove that URL from the 404 log after the redirect is created. It is ticked by default.
- List columns: Source, Target, Match, Code, Hits, Last hit, Status, Created. Source, Code, Hits, Last hit and Created are sortable.
- Filters above the table: status, redirect code and match type, plus a search box covering both source and target.
- Bulk actions: Enable, Disable, Reset hit counters, Delete. Row actions: Edit, Enable or Disable, Reset hits, Delete.
How matching works, and what it actually covers
The matcher runs on parse_request at priority 1, before WordPress builds its main query. It is skipped on admin screens, on admin-ajax.php, during WP-Cron and under WP-CLI. It is not skipped for REST API requests. The plugin checks the REST_REQUEST constant, but that constant is defined by WordPress core's own parse_request callback, which runs at priority 10, so at priority 1 it does not exist yet. Everything WordPress serves through its front controller therefore passes through the matcher, including /wp-json/ paths, /?rest_route= requests, /robots.txt, /wp-sitemap.xml, feeds and search URLs. If you have no enabled rules of a given type, that type is skipped entirely and costs nothing.
- Exact rules go first, resolved through a hashed index in a single query. The source is then compared again in PHP, so a hash collision cannot fire the wrong rule.
- Then prefix rules, longest source first. The comparison is a plain string test against the start of the path. It is not segment-aware.
- If a prefix target ends in a slash, the unmatched remainder of the request is appended: source /old-blog/ with target /blog/ turns /old-blog/hello into /blog/hello.
- Then regular expressions, oldest rule first. The pattern is stored without delimiters and matched against the path, and $1, $2 and so on in the target are substituted with the captured groups. Maximum pattern length is 500 characters.
- The first rule that produces a usable target wins, and the request stops there.
- Matching always happens on the path alone. Query strings are handled separately by the Query strings setting.
- A pattern that fails at runtime is silently skipped. A broken regular expression cannot take the site down, it simply never matches.
- Only Exact matching is a single indexed lookup. Prefix and regular expression rules are loaded in full from the database on every front-end request and compared in PHP, so a large regex rule set is not free.
Rules are matched before WordPress decides whether a URL exists, so a rule whose source is a live page will hide that page from visitors. And because REST requests are matched like any other, a broad rule silently takes the block editor and Site Health with it. If a working URL suddenly starts redirecting, or the editor stops saving, look for a rule that covers it before you look anywhere else.
Settings: Redirect matching
The first section of 404 Handler Settings governs how incoming URLs are compared against your rules. All three are global: they apply to every rule at once. The last two also decide how the stored lookup key of every Exact rule is computed, and for that reason they cannot be changed from this screen at all. Read the next section before you touch either of them.
- Query strings, labelled "Pass the original query string on to the redirect target". Default on. /old-page?utm_source=x lands on /new-page?utm_source=x. Where the target already carries the same parameter, the target's own value wins.
- Case sensitivity, labelled "Match URLs case-insensitively". Default off. When it is on, paths are lowercased before matching and every regular expression rule also gains the i flag.
- Trailing slashes, labelled "Ignore trailing slashes when matching". Default on, so /contact and /contact/ count as the same URL. It also strips the trailing slash off your stored prefix sources, which has consequences covered in the dangerous operations chapter.
Do not toggle "Match URLs case-insensitively" or "Ignore trailing slashes when matching" on this screen. Saving either change crashes the Settings screen with a PHP fatal error and stores nothing. The next section gives the only route that works, and the mandatory second step that goes with it.
The two settings that cannot be saved from the Settings screen
Case sensitivity and Trailing slashes are the only two settings whose change triggers extra work at save time: the plugin recomputes the stored lookup key of every Exact rule. That code path never completes. The sanitiser calls update_option() itself, update_option() re-applies the very same sanitiser through the sanitize_option_m404_settings filter that register_setting() attached to it, and the plugin's own settings cache still holds the old value, so the branch fires again, and again, without end. The request dies with a PHP fatal error on options.php.
- The symptom is a white screen or an HTTP 500 after pressing Save Changes on 404 Handler Settings, and only when you actually changed one of those two checkboxes.
- Nothing is corrupted. The write never reaches the database, so the setting keeps its previous value, every other setting on the page is untouched, and the front end carries on exactly as before. The recursion lives entirely inside that one options.php request.
- Every other setting on this screen saves normally, as long as those two checkboxes are left exactly as they are.
- The shell route works because register_setting() only runs on admin_init. Under WP-CLI the recursive filter is never attached, so the write completes.
- After changing either value you must recompute the Exact hashes yourself. Nothing else does it: the plugin recomputes a rule's key only when that rule is saved through the admin.
- Prefix and regular expression rules are unaffected. They normalise the path at match time and store no hash.
# Change either setting from the shell, never from the Settings screen.
wp option patch update m404_settings case_insensitive 1
wp option patch update m404_settings ignore_trailing_slash 0
# Mandatory second step: recompute the lookup key of every Exact rule.
# Requires the plugin to be active, so the class is loaded.
wp eval '(new M404_Redirect_Repository())->rehash_all();'
# No WP-CLI? Open and re-save every Exact rule under
# 404 Handler > Redirects. Saving a rule recomputes its own key.
Decide these two before you build your rule set. Changed later without a rehash, every Exact rule looks completely normal on the Redirects screen while matching nothing at all, and no message anywhere tells you why.
Settings: 404 handling
This section decides what happens to a visitor who reaches a URL that does not exist and matches no rule. It runs on template_redirect at priority 20, after WordPress core's own canonical redirect and old-slug redirect have already had their chance to rescue the URL, so only genuine 404s reach it.
- "When a 404 happens" offers three choices. The shipped default is "Show the theme's normal 404 page (do nothing)", meaning the plugin does nothing. The second is "Automatically redirect to a page or URL of your choice". The third is the option beginning "Show a custom 404 page", which serves a real page while keeping the 404 status; on screen its label runs longer and carries a note about SEO.
- "Redirect target" combines a page picker with a free text field. Defaults: no page selected, empty URL. The page selection wins over the URL, and the page must be published or the URL is used instead. If both are empty, visitors go to the homepage. The URL must be site-relative starting with a slash, or a full http or https URL; anything else is discarded on save.
- "Redirect status code" defaults to 302, which is the right answer for an automatic 404 redirect.
- "Custom 404 page" defaults to none. It must be a published page, not a post. The page is served with a real 404 header and no-cache headers, so search engines will not index it. If the chosen page is missing, trashed or still a draft, the theme's own 404 template quietly takes over.
- "Did you mean" suggestions, on by default. Up to five published posts or pages whose slug resembles the missing one. The plugin matches on the first three characters of the slug, examines at most 50 candidates and keeps only those at least 40 percent similar. A missing slug shorter than three characters produces nothing.
- Suggestions render only in "Show a custom 404 page" mode, and they are always appended to the end of the page content. The [m404_suggestions] shortcode does not move them: it adds a second copy where you place it. The plugin filters the_content at priority 20, after WordPress has already expanded shortcodes at priority 11, so its check for the shortcode never finds one. Use the shortcode only if you want the list twice.
- The no-cache headers are sent only in "Show a custom 404 page" mode. In the default mode and in automatic redirect mode the plugin adds no cache headers of its own, whatever the readme says.
[m404_suggestions]
Settings: 404 logging
The log keeps one row per unique URL, path plus query string, with a hit counter, so a bot hammering the same missing file cannot grow the table. Note that ignore patterns suppress logging only: an ignored URL is still redirected by the automatic fallback and still receives the custom 404 page. Note also that the patterns are tested against the path alone, while the row that gets stored is the path with its query string.
- Logging, labelled "Log 404 errors". Default on.
- Retention. Default 30 days, accepted range 0 to 3650. Rows not hit within that window are deleted once a day by the m404_cleanup cron event, in batches of 1000. Set 0 to keep the log forever.
- Privacy, labelled "Anonymize visitor IP addresses before storing them". Default on. Only REMOTE_ADDR is read; forwarded headers are deliberately ignored because they can be spoofed.
- Ignore patterns, a textarea with one pattern per line. An asterisk matches any run of characters and matching is case-insensitive. A pattern starting with a slash or an asterisk is matched against the whole path; a pattern starting with neither matches the end of the path.
- The shipped list has twenty patterns: *.php, *.asp, *.aspx, *.env*, *.git*, *.map, *.axd, /wp-content/*, /wp-includes/*, *.jpg, *.jpeg, *.png, *.gif, *.webp, *.svg, *.ico, *.css, *.js, *.woff*, *.ttf
- Each log row holds the URL, the hit count, the last referrer, the last user agent, the last IP address, and first seen and last seen timestamps. The URL and referrer are truncated at 2000 characters, the user agent at 255, the IP at 45.
First run: what to set, and in what order
The shipped defaults are already the right starting point, so resist the urge to configure everything on day one. Settle the two normalisation settings first, because they are the only ones that are painful to change later. Then get your known redirects in, let the log tell you what is actually broken, and only then decide what should happen to the 404s that remain.
- Decide now whether you want case-insensitive matching and whether trailing slashes should be ignored. The defaults are off and on. If you want anything else, set it from the shell as described earlier, before a single rule exists.
- Leave the rest of Settings untouched. Logging is on, retention is 30 days, IP anonymisation is on and nothing is redirected automatically. That is exactly what you want on day one.
- Enter your known redirects under Redirects, Add New. Use Exact match for single URLs. Build every rule as 302 first, verify it in a private browser window, and only then edit it to 301 for a permanent move.
- Use Prefix match for whole sections that moved. Put a trailing slash on both source and target when you want the rest of the URL carried over, and check that the source cannot also be the start of some other live URL.
- Leave regular expressions for last, and only where a prefix rule genuinely cannot do the job.
- After every prefix or regular expression rule, run three checks: open any post in the block editor and save it, request /robots.txt, and request /wp-sitemap.xml. All three must behave normally.
- Let the log fill for a week or two. Then sort the 404 Log by Hits and fix the top entries with the Create redirect row action, which prefills the form with the missing path.
- Add ignore patterns for whatever noise survives the shipped list, so the log stays readable.
- Only now choose a 404 fallback mode. "Show a custom 404 page" is the safe option because it keeps the 404 status. Choose the automatic redirect only if you accept that it hides every broken link from crawlers and from your own reports.
Dangerous operations: what can take the site down
This plugin sits in front of every front-end request, which is exactly what makes it useful and exactly what makes a careless rule expensive. Read this list before you create your first prefix or regular expression rule, and before you switch the 404 fallback to automatic redirection.
- A prefix rule whose source is a single slash matches every URL on the site and sends the entire front end somewhere else. The save-time self-redirect check only covers Exact rules, so nothing will stop you.
- A regular expression such as ^/(.*)$ does the same thing. Patterns are checked only for whether they compile, never for how much of your site they cover.
- Any rule whose source is an existing URL hides that URL, because rules are matched before WordPress decides what exists.
- A broad rule also catches /wp-json/ paths. So does a rule on the single slash itself, because a /?rest_route= request has nothing but a slash in its path. wp-admin still looks completely normal, but the block editor cannot load or save a post and Site Health fails. The error messages say nothing about redirects.
- A broad rule also catches /robots.txt, /wp-sitemap.xml and your feeds. Redirecting those quietly de-indexes the site, and unlike a broken page nobody notices for weeks.
- Prefix matching is a plain string comparison, and with "Ignore trailing slashes" on the stored source /old-blog/ is normalised to /old-blog. That rule therefore also catches /old-blogger and /old-blog-archive, and with a slash-terminated target it rewrites /old-blogging into /blog/ging. Live pages disappear and the target is garbage.
- Redirect loops. The runtime guard only rejects a target whose normalised path equals the current path, so it does not catch a chain. A prefix rule with source /shop and target /shop/new/ re-matches its own output and produces /shop/new/new/new/ without end. An unanchored regular expression does it too: the pattern blog with the target blog-new turns /blog into /blog-new, which the same pattern still matches. Two rules pointing at each other give the visitor ERR_TOO_MANY_REDIRECTS.
- Any 301 is unrecallable once it has been cached. That applies to a redirect rule saved as 301 exactly as much as to the automatic 404 fallback set to 301. Browsers, proxies and CDNs keep honouring it long after you have fixed the rule. The rule form opens on 301, so this is the easy mistake to make.
- Choosing automatic redirection at all makes every broken link invisible to crawlers, which see a working page instead of a 404. The log still records them, but your SEO tooling will not.
- Enabling a prefix or regular expression rule whose target points at an external host adds that host to WordPress's allowed redirect hosts for the whole site, for as long as the rule stays enabled. Every wp_safe_redirect call anywhere on the site will then accept that host. Exact rules do not do this.
Build every new rule as 302, verify it in a private window, and only then switch it to 301. A wide-open prefix or regular expression blocks real visitors from your whole site and breaks the block editor at the same time. A 301 loop that has already shipped is fixed on the server the moment you disable the rule, but visitors who already hit it stay stuck until they clear their browser cache.
Emergency recovery: everything redirects, or the editor stopped saving
The honest version of the reassurance is narrower than "you cannot lock yourself out". Page loads in wp-admin and wp-login.php never run the matcher, because neither of them calls wp(), so you can always log in and reach the plugin's own screens and undo whatever a rule is doing. What a broad rule does reach is everything wp-admin does over the REST API: the block editor, Site Health, the mobile app. So this chapter covers two different symptoms, and one of them does not look like a redirect problem at all.
- Log in at /wp-login.php and open 404 Handler, then Redirects. No rule affects that page.
- Filter the list by match type Prefix match, then Regular expression. A site-wide redirect will almost always be one of those two, and so will a broken editor.
- Use the Disable row action on the suspect rule rather than Delete, so you can still inspect it afterwards. Disabling takes effect immediately.
- Open any post in the block editor and save it. If the editor was complaining that the response was not valid JSON, or that publishing failed, and it now saves, that rule was catching /wp-json/.
- Request /robots.txt and /wp-sitemap.xml directly and confirm they return their own content rather than a redirect.
- If the front end is still wrong, open Settings and set "When a 404 happens" back to "Show the theme's normal 404 page (do nothing)".
- If wp-admin itself is unreachable for some unrelated reason, run one of the commands below over SSH, or rename the plugin folder containing m-404-handler.php over SFTP. Renaming the folder deactivates the plugin and leaves every rule, the whole log and all settings intact.
# Deactivate the plugin completely; all rules and logs are kept.
wp plugin deactivate m-404-handler
# On a multisite network, add --network.
# Or keep it active and make every rule stop matching. Temporary only.
wp option update m404_rule_counts '{"exact":0,"prefix":0,"regex":0}' --format=json
# Or turn off only the automatic 404 redirect.
wp option patch update m404_settings fallback_mode none
# Or disable one rule directly in SQL (use your own table prefix).
# Disabling in SQL takes effect at once.
wp db query "UPDATE wp_m404_redirects SET enabled = 0 WHERE id = 12;"
# But RE-ENABLING in SQL does nothing while the cached counts say the
# type has no rules. Refresh them, or the rule stays dead:
wp eval '(new M404_Redirect_Repository())->refresh_counts();'
The m404_rule_counts trick is temporary only. The plugin recalculates that option the moment any rule is saved, enabled, disabled or deleted, so every rule will spring back to life. It also cuts both ways: while a type's count is zero, no rule of that type can match no matter what the enabled column says, so anything you change directly in SQL needs the counts refreshed afterwards. Fix the real rule before you touch the Redirects screen again.
Deletions that cannot be undone
There is no trash, no undo and no export anywhere in this plugin. Everything below removes data immediately and permanently, and some of it removes far more than the button suggests.
- "Delete all log entries" on the 404 Log screen empties the entire log table in a single statement. A browser confirmation box is the only guard.
- That statement is TRUNCATE TABLE, which needs the DROP privilege. Several managed hosts withhold it from the site's database user. The plugin discards the result and prints "The 404 log has been emptied." either way, so reload the 404 Log screen and confirm it is actually empty before you rely on it.
- Delete on individual log rows and the bulk Delete on the same screen. Neither asks for confirmation.
- Delete on the Redirects screen. The row action asks for confirmation, but the bulk action does not, so a mis-clicked checkbox can wipe a page of rules with two clicks.
- Both list screens are rendered inside a GET form, so bulk actions travel in the query string. A bulk Delete of a page of rules lands, complete with its nonce, in your browser history, in the server access log and in any proxy log in between, and stays replayable for the lifetime of that nonce.
- "Reset hit counters" sets the hit count to zero and clears the last hit date for the selected rules, which is how you lose the evidence of which redirects are still needed. It is the one bulk action that does not refresh the cached rule counts, because it does not need to.
- Lowering Retention. At the next daily run of m404_cleanup, every log row not hit within the new window is deleted. Going from 365 days to 7 throws away a year of data within 24 hours, with no warning at the moment you save.
- The "Remove this URL from the 404 log after creating the redirect" checkbox on the redirect form, which is ticked by default whenever you arrive there from the log.
If the rule list or the 404 log matters to you, take a database backup before any bulk operation. Once the two tables are changed there is nothing in the plugin that can bring the data back.
Troubleshooting
A short list of what actually goes wrong in practice, and what to check first in each case.
- A redirect does nothing. Check that the rule is Enabled, then check that no earlier rule already matched: exact rules run first, then the longest prefix, then regular expressions in creation order.
- The block editor cannot save, or Site Health fails, and it started right after you added a rule. A prefix or regular expression rule is catching /wp-json/. Disable it, then re-test.
- Every Exact rule stopped matching at once, and nothing on screen explains it. Someone changed Case sensitivity or Trailing slashes without rehashing. Run the rehash command, or re-save each Exact rule.
- One rule works and another silently does not, and re-saving any rule fixes it. The plugin keeps a cached count of enabled rules per type in the option m404_rule_counts, and if that count is zero the whole type is skipped. Open any rule and save it to force a recount.
- You enabled a rule directly in SQL and nothing happened. The cached counts were not refreshed. Save any rule through the admin, or refresh the counts from the shell.
- The 404 log stays empty. Either logging is off, or the URLs are caught by the ignore patterns (the shipped list already excludes .php, images, CSS, JS, fonts and everything under /wp-content/ and /wp-includes/), or a full-page cache or CDN is serving the 404 without WordPress ever running. If your CDN caches everything, exclude 404 responses.
- Old log entries never disappear. Retention runs only through the daily m404_cleanup cron event. If WP-Cron is disabled and no system cron replaces it, nothing purges. Confirm the event exists with wp cron event list.
- The custom 404 page shows the theme's 404 instead. The selected page must exist, be of post type page, and be published. A draft or a trashed page falls back silently.
- Suggestions never appear. They only render in "Show a custom 404 page" mode, they need a missing slug of at least three characters, and they need a published post or page whose slug is at least 40 percent similar.
- A regular expression saves but never fires. The pattern is stored without delimiters and matched against the path only, so it needs a leading slash in the pattern, as in ^/old-blog/.
- The menu is missing for a colleague. All three screens require the manage_options capability, so only administrators see them.
- A 404 Handler error notice appears on a wp-admin page you did not expect it on. The plugin prints whatever text is in the m404_error query parameter, on any admin screen, with no nonce and no referrer check. The text is escaped so it cannot run code, but anyone who gets you to click a crafted link can put their own words inside a genuine-looking WordPress notice. Treat a notice that arrived from a link as untrusted, and never act on a phone number or address it gives you.
Privacy and data
The plugin makes no outbound connection of any kind. There is no phone-home, no external update check, no analytics and no licence validation, and there is no code in it that sends email, so it cannot affect your site's mail delivery in any way. Everything it knows stays in your own database.
- Stored in wp_m404_redirects: source, match type, target, status code, enabled flag, hit count, last hit time, created and updated timestamps. No visitor data.
- Stored in wp_m404_404_log: the missing URL including its query string, a hit counter, the last referrer, the last user agent, the last IP address, and first seen and last seen timestamps.
- IP addresses pass through WordPress's own anonymisation function when "Anonymize visitor IP addresses" is on, which is the shipped default. Only REMOTE_ADDR is read, never a forwarded header.
- Switching anonymisation on later does not clean up what is already stored. The function is applied at write time only, so existing rows keep their full IP addresses until that exact URL is hit again or the retention window deletes the row. Clear the log or wait out the retention window, and treat the old rows as personal data until you do.
- If you would rather store nothing about visitors at all, switch Logging off. Redirects keep working exactly as before.
- The log records only URLs that produced a genuine 404 and that match none of your ignore patterns.
- If you turn anonymisation off, the log holds a full IP address and a user agent, which is personal data. Say so in your privacy notice, and keep the retention window short.
- Log rows you delete in bulk travel as a GET request, so the URLs of the rows you selected appear in your browser history and in the server access log. That is worth knowing if the logged URLs themselves are sensitive.
Uninstalling: what goes and what stays
Deactivating and deleting do very different things here, and the difference is the whole of your redirect history. Deactivate when you want the plugin to stop working. Delete only when you are certain you will not need the rules again.
- Deactivating stops all matching and unschedules the daily m404_cleanup event on the current site. Both tables, every rule, the whole log and all settings stay exactly as they were, and reactivating picks up where you left off.
- On a multisite network the deactivation hook only clears the cron event for the one site it runs on. Every other site in the network keeps its scheduled m404_cleanup event, which then fires daily with no callback attached. It is harmless but untidy; clear it per site with wp cron event delete m404_cleanup if you care.
- Deleting the plugin from the Plugins screen runs the uninstaller, which drops wp_m404_redirects and wp_m404_404_log, deletes the options m404_settings, m404_rule_counts and m404_db_version, and clears the cron event. On a multisite network it does all of this on every site in the network.
- That multisite loop runs in a single request with no limit on the number of sites. On a large network the request can time out part-way, leaving the tables dropped on the sites already processed and intact on the rest, with no resume path other than reinstalling and deleting again. Verify afterwards that no m404_ tables remain.
- What survives a delete: the per-user screen option values m404_redirects_per_page and m404_logs_per_page remain in the user meta table.
- Nothing is written outside the database. No files are created anywhere in wp-content. The [m404_suggestions] shortcode, however, stops being registered, and WordPress does not strip a shortcode it no longer knows: the literal text [m404_suggestions] then shows up on the page for visitors. Take it out of the page before you deactivate or delete the plugin.
Deleting the plugin destroys every redirect rule you have ever created, along with the entire 404 log. If there is any chance you will reinstall it, deactivate instead, or take a database dump of the two tables first. On a multisite network take that dump regardless, because a timed-out uninstall leaves the network half cleaned.
Automatic updates
The plugin checks majevski.com for new releases and offers them through the normal WordPress update screen — the same prompt, changelog popup and one-click install as any other plugin. Checks are cached, never slow a page down, and fail open: if majevski.com cannot be reached, the site simply carries on and tries again later. An update package is only ever accepted from majevski.com over HTTPS, and an older version is never offered. Versions before 1.1.0 predate the update channel, so they cannot see new releases — install 1.1.0 or newer manually once, and every later release arrives on its own.
Want something like this built?
Everything on this page was designed, built and is run by one person. If you need the same for your business, tell me what you have in mind.
Book a call opens in a new tab