Web Host Croc

A Collection of Useful WordPress Must-Use (MU) Plugins

A Collection of Useful WordPress Must-Use (MU) Plugins


If you’ve been building WordPress sites for a while you probably have a folder of code snippets you copy into every new project. I know I do. Mine has been growing for years.

It’s always the same handful of fixes. Clean up the header output, switch off features the site is never going to use, tighten a security default or two, and get rid of the little admin annoyances clients always seem to notice. None of it takes more than a minute to write. The annoying part is writing it again on the next project.

Most tutorials tell you to drop this stuff in your theme’s functions.php file. That works fine until you switch themes, or hand the site over to a client who installs a different one, or update a child theme and lose the lot. A much better home for them is the mu-plugins folder.

Below are various handy must-use plugins I’ve put together over the years. Each one is a single file that does one specific thing. Copy the ones you want, ignore the rest, or grab the whole collection from GitHub at the end of the article.

In this article
What Are MU-Plugins?How to Use These SnippetsCleanup and PerformancePrivacy and Third-Party RequestsSecurity and HardeningComments and SpamAdmin ExperienceStaging and Demo SitesGet Them All on GitHubWrapping Up

What Are MU-Plugins?

MU stands for “must-use”. Any PHP file you drop into wp-content/mu-plugins/ gets loaded automatically on every request, before regular plugins, and there’s no activation step at all.

There are a few things worth knowing before you start dropping files in there:

You can’t deactivate them from the admin. There’s no activate or deactivate link. To turn one off you delete or rename the file. That’s actually a nice feature when you’re handing a site to a client and you don’t want them switching off your security tweaks by accident.

They load first. MU-plugins run before regular plugins, so they’re a good spot for anything that needs to define a constant or set up a filter early.

Only files in the root of the folder get loaded. WordPress doesn’t scan subdirectories. Drop in my-snippet/my-snippet.php and nothing happens. The file has to sit directly in mu-plugins/.

Activation hooks don’t fire. Anything using register_activation_hook to run setup code won’t work here. None of my snippets need it, but it’s worth knowing if you try moving a regular plugin into the folder.

They load alphabetically, so name your files sensibly if load order matters.

You won’t get update notifications, because there’s no repository to check against. You maintain them yourself.

You can see everything that’s loaded under Plugins → Must-Use in the admin. That’s why each of my files still has a proper plugin header. Without one the file still runs, it just shows up as an unnamed entry in the list.

If you want the full picture, the Must Use Plugins page in the Advanced Administration Handbook is the official reference. It covers things like changing the directory with WPMU_PLUGIN_DIR, and explains why the name is a leftover from WordPress MU rather than an accurate description of what the folder does.

How to Use These Snippets

Create the wp-content/mu-plugins/ folder if it doesn’t exist yet, then drop in whichever files you want. That’s it. There’s nothing to activate.

If you’d rather not use mu-plugins, all of these work fine as regular plugins. You can also paste the code into a child theme’s functions.php file or a code snippets plugin, just leave off the plugin header.

Heads up: A few of these are pretty aggressive and are meant for staging or demo sites, not production. I’ve flagged those individually, so read the notes before you install something that stops your contact form emails from sending.

Cleanup and Performance

These are the ones I install without really thinking about it. None of them change how the site works for visitors. They just stop WordPress doing work and storing data nobody asked for.

Clean Head

WordPress prints a bunch of markup into your <head> that most sites never use. An RSD link for remote publishing clients nobody has run in a decade. A Windows Live Writer manifest. Shortlinks, oEmbed discovery links, and a generator tag that tells everyone exactly which version of WordPress you’re on.

None of it is huge on its own. But it’s bytes on every single page load, and that generator tag is handing automated scanners a free hint. This one strips the lot.

<?php
/**
* Plugin Name: Clean Head
* Description: Removes unnecessary WordPress head output.
* Author: WPExplorer
* Version: 1.0.0
*/

defined( ‘ABSPATH’ ) || exit;

// Remove RSD link.
remove_action( ‘wp_head’, ‘rsd_link’ );

// Remove WordPress generator tag.
remove_action( ‘wp_head’, ‘wp_generator’ );

// Remove RSS feed links.
remove_action( ‘wp_head’, ‘feed_links’, 2 );
remove_action( ‘wp_head’, ‘feed_links_extra’, 3 );

// Remove Windows Live Writer manifest.
remove_action( ‘wp_head’, ‘wlwmanifest_link’ );

// Remove adjacent post links.
remove_action( ‘wp_head’, ‘adjacent_posts_rel_link’, 10 );
remove_action( ‘wp_head’, ‘adjacent_posts_rel_link_wp_head’, 10 );

// Remove shortlinks.
remove_action( ‘wp_head’, ‘wp_shortlink_wp_head’, 10 );
remove_action( ‘template_redirect’, ‘wp_shortlink_header’, 11 );

// Remove emoji assets.
remove_action( ‘wp_head’, ‘print_emoji_detection_script’, 7 );
remove_action( ‘wp_print_styles’, ‘print_emoji_styles’ );

// Remove REST API discovery link.
remove_action( ‘wp_head’, ‘rest_output_link_wp_head’ );

// Remove oEmbed discovery links.
remove_action( ‘wp_head’, ‘wp_oembed_add_discovery_links’ );
remove_action( ‘wp_head’, ‘wp_oembed_add_host_js’ );

// Remove generator version from feeds.
add_filter( ‘the_generator’, ‘__return_empty_string’ );

Heads up: Two of these are worth a second thought. Removing feed_links kills RSS autodiscovery, so drop those two lines if anyone actually subscribes to your feed. And removing rest_output_link_wp_head only removes the discovery link, it doesn’t disable the REST API. Some tools rely on that link to find your endpoints.

Disable WP Emoji Support

WordPress ships a JavaScript file that converts emoji into images for browsers that can’t render them. In 2026 basically every browser can. So what you’re left with is an extra script, an extra stylesheet, and a DNS prefetch to s.w.org on every page.

This one goes further than the emoji lines in Clean Head. It also strips the emoji handling out of the classic editor, feeds and outgoing emails, plus the resource hint.

<?php
/**
* Plugin Name: Disable WP Emoji Support
* Description: Disables WordPress’s custom emoji support.
* Author: WPExplorer
* Version: 1.0.0
*/

defined( ‘ABSPATH’ ) || exit;

add_action( ‘init’, function() {
remove_action( ‘wp_head’, ‘print_emoji_detection_script’, 7 );
remove_action( ‘admin_print_scripts’, ‘print_emoji_detection_script’ );
remove_action( ‘wp_print_styles’, ‘print_emoji_styles’ );
remove_action( ‘admin_print_styles’, ‘print_emoji_styles’ );
remove_filter( ‘the_content_feed’, ‘wp_staticize_emoji’ );
remove_filter( ‘comment_text_rss’, ‘wp_staticize_emoji’ );
remove_filter( ‘wp_mail’, ‘wp_staticize_emoji_for_email’ );

add_filter( ‘tiny_mce_plugins’, function( $plugins ) {
if ( is_array( $plugins ) ) {
return array_diff( $plugins, [ ‘wpemoji’ ] );
}

return [];
} );

// Strip out any URLs referencing the WordPress.org emoji location
add_filter( ‘wp_resource_hints’, function( $urls, $relation_type ) {
if ( ‘dns-prefetch’ == $relation_type ) {
$emoji_svg_url_bit=”https://s.w.org/images/core/emoji/”;
foreach ( $urls as $key => $url ) {
if ( strpos( $url, $emoji_svg_url_bit ) !== false ) {
unset( $urls[$key] );
}
}
}
return $urls;
}, 10, 2 );
} );

add_filter( ’emoji_svg_url’, ‘__return_false’ );

Heads up: This overlaps with Clean Head. Pick one or the other for the emoji stuff, or just leave the duplicate remove_action calls in. They’re harmless.

Disable Attachment Pages

Every image you upload gets its own page at a URL like /my-post/img_4021/. There’s nothing on it but the image. Google calls that thin content, and on an image-heavy site you can end up with thousands of them.

This plugin 301 redirects any attachment page to its parent post. If there’s no parent it sends them to the homepage.

<?php
/**
* Plugin Name: Disable Attachment Pages
* Description: Redirects attachment pages to their parent post or page, or to the homepage when no parent exists.
* Author: WPExplorer
* Version: 1.0.0
*/

defined( ‘ABSPATH’ ) || exit;

add_action(
‘template_redirect’,
function() {
if ( ! is_attachment() ) {
return;
}

$parent_id = wp_get_post_parent_id( get_queried_object_id() );

$url = $parent_id
? get_permalink( $parent_id )
: home_url( ‘/’ );

wp_safe_redirect( $url, 301 );
exit;
}
);

Heads up: Newer installs have a wp_attachment_pages_enabled option that’s already switched off, but WordPress leaves it alone on sites that were set up before that change. This covers you either way.

Disable Image Sizes

WordPress creates a copy of every image you upload at each registered size. Between core, your theme and your plugins, one upload can easily turn into a dozen files. On a big media library that’s real disk space and real backup time.

This stops the generation completely. It also disables the big image threshold, so your originals stay exactly as you uploaded them.

<?php
/**
* Plugin Name: Disable Image Sizes
* Description: Disables WordPress from generating intermediate image sizes.
* Author: WPExplorer
* Version: 1.0.0
*/

defined( ‘ABSPATH’ ) || exit;

/**
* Disable the big image size threshold.
*/
add_filter( ‘big_image_size_threshold’, ‘__return_false’ );

/**
* Disable generated intermediate image sizes.
*/
add_filter( ‘intermediate_image_sizes_advanced’, ‘__return_empty_array’ );

Heads up: This is a niche one, not a general speed win. With no intermediate sizes there’s no srcset, so mobile visitors download your full-resolution originals. That’s a lot worse than the disk space you just saved. It only really makes sense if something else handles resizing for you, like an image CDN or an offloading service. On a normal site you’re better off trimming the registered sizes you don’t use.

Limit Post Revisions

By default WordPress keeps every revision of every post forever. On a site with a few hundred long posts that have been edited over the years, revisions can easily take up more rows in wp_posts than your actual content does. And they get dragged along in every backup and every migration.

Five is plenty to recover from a mistake without letting the table get out of hand.

<?php
/**
* Plugin Name: Limit Post Revisions
* Description: Caps the number of revisions stored per post.
* Author: WPExplorer
* Version: 1.0.0
*/

defined( ‘ABSPATH’ ) || exit;

/**
* Limit stored revisions.
*
* Return 0 to disable revisions entirely, or branch on $post->post_type to
* use different limits per post type.
*/
add_filter( ‘wp_revisions_to_keep’, function ( $num, $post ) {
return 5;
}, 10, 2 );

Heads up: This only applies to revisions created from now on. The existing ones stay put until you clean them out with WP-CLI or a maintenance plugin. You could also use the WP_POST_REVISIONS constant in wp-config.php, but I prefer the filter because it gets passed the post object. That means you can keep more revisions for posts than pages, or switch them off entirely for a custom post type.

Privacy and Third-Party Requests

Each of these stops your site sending something to someone else without anyone deciding it should. That’s partly a speed thing, since every external request is a dependency on another company staying online. It’s also a compliance thing. Data you never send is data you never have to document.

Disable AI

WordPress 7.0 added AI features to core along with a wp_supports_ai() function. Any plugin or theme that wants to use the core AI client is supposed to check it first. There’s a matching wp_supports_ai filter, so you can switch the whole thing off in one line.

Plenty of sites shouldn’t have AI on by default. Client contracts and NDAs often say you can’t send content to third-party providers without approval, and GDPR wants a documented lawful basis before personal data goes near a model. If it’s on by default, an editor can wire up a connector before anyone has looked at any of that.

This covers core plus the two third-party offenders I run into most.

<?php
/**
* Plugin Name: Disable AI
* Description: Disables AI features in WordPress and supported plugins.
* Author: WPExplorer
* Version: 1.0.0
*/

defined( ‘ABSPATH’ ) || exit;

/**
* Disable WordPress AI support.
*/
add_filter( ‘wp_supports_ai’, ‘__return_false’, 99 );

/**
* Jetpack.
*/
add_filter( ‘jetpack_ai_enabled’, ‘__return_false’, 99 );

/**
* Elementor.
*/
add_filter( ‘get_user_option_elementor_enable_ai’, ‘__return_zero’ );

Heads up: The core filter only covers code that actually calls wp_supports_ai(). Plugins that bundle their own AI integration ignore it completely and have to be switched off in their own settings, which is why Jetpack and Elementor get separate lines. If you want something stronger, define WP_AI_SUPPORT as false in wp-config.php instead. That runs earlier and another plugin can’t override it.

Disable Avatars

Every avatar on the page is an HTTP request to Gravatar. On a post with fifty comments that’s fifty requests to a domain you don’t control, which is both a speed cost and a rendering dependency on someone else’s uptime.

There’s a privacy side to it too. Requesting an avatar sends a hash of the commenter’s email address to Automattic, along with the visitor’s IP and the page they’re on. That’s the sort of thing you’d rather not have to write into a privacy policy for a feature nobody asked for.

<?php
/**
* Plugin Name: Disable Avatars
* Description: Turns off avatars so no requests are made to Gravatar.
* Author: WPExplorer
* Version: 1.0.0
*/

defined( ‘ABSPATH’ ) || exit;

/**
* Disable avatars regardless of the stored setting.
*/
add_filter( ‘option_show_avatars’, ‘__return_false’ );

/**
* Hide the avatar settings from Settings > Discussion.
*/
add_action( ‘admin_init’, function () {
global $wp_settings_fields;
unset( $wp_settings_fields[‘discussion’][‘avatars’] );
} );

Heads up: Filtering the option instead of just unticking the box means it stays off even if someone toggles the setting later. That’s why I hide the setting as well. If avatars still show up, check your theme. Anything calling get_avatar() with force_display set to true will still render one.

Disable Yoast Dashboard Widget

Yoast SEO adds a dashboard widget with an SEO overview at the top and a feed of posts from Yoast.com underneath. The feed is the problem. Rendering it means an uncached HTTP request out to Yoast every time the dashboard loads, and that request includes your WordPress and PHP versions as query parameters. So you’re reporting your server setup to a third party every time someone glances at the admin.

The scripts and styles load whether the widget renders or not, so just removing the meta box leaves them behind.

<?php
/**
* Plugin Name: Disable Yoast Dashboard Widget
* Description: Removes the Yoast SEO dashboard widget along with its scripts and styles.
* Author: WPExplorer
* Version: 1.0.0
*/

defined( ‘ABSPATH’ ) || exit;

/**
* Remove the widget.
*
* Runs late so that it fires after Yoast has registered the meta box.
*/
add_action( ‘wp_dashboard_setup’, function () {
remove_meta_box( ‘wpseo-dashboard-overview’, ‘dashboard’, ‘normal’ );
}, PHP_INT_MAX );

/**
* Dequeue the widget assets, which are enqueued whether the widget renders
* or not.
*/
add_action( ‘admin_enqueue_scripts’, function ( $hook_suffix ) {
if ( ‘index.php’ !== $hook_suffix ) {
return;
}

wp_dequeue_script( ‘yoast-seo-dashboard-widget’ );
wp_dequeue_style( ‘yoast-seo-wp-dashboard’ );
wp_dequeue_style( ‘yoast-seo-monorepo’ );
}, PHP_INT_MAX );

Heads up: This relies on Yoast internals that could get renamed in any release. If the widget comes back after an update, check the meta box ID and the three asset handles against the current version. And to be fair to Yoast, they’re not unusual here, just the most common example. Plenty of plugins phone home from the dashboard and the same approach works on any of them once you know the handles.

Security and Hardening

None of these replace a firewall, decent passwords or staying updated. They just switch off features a lot of sites never use. A feature nobody uses is a feature nobody is keeping an eye on.

Disable User Enumeration

WordPress gives away usernames in four different places, and most hardening advice only closes one of them:

The XML sitemap includes an author provider.

The REST API answers /wp-json/wp/v2/users for anyone who asks.

Author archives put the username right in the URL, and ?author=1 redirects to it so you can walk the IDs one by one.

The login form returns a different error for a bad username than for a bad password, so you can check whether an account exists.

A valid username is half of a login. Closing one of those four and calling it done is arguably worse than doing nothing, because it feels like you solved the problem. So this one closes all four.

<?php
/**
* Plugin Name: Disable User Enumeration
* Description: Prevents WordPress from exposing usernames via sitemaps, the REST API, author archives and login errors.
* Author: WPExplorer
* Version: 1.0.0
*/

defined( ‘ABSPATH’ ) || exit;

/**
* Remove the user sitemap provider.
*/
add_filter( ‘wp_sitemaps_add_provider’, function ( $provider, $name ) {
if ( ‘users’ === $name ) {
return false;
}
return $provider;
}, 10, 2 );

/**
* Remove the REST API user endpoints for logged out requests.
*/
add_filter( ‘rest_endpoints’, function ( $endpoints ) {
if ( is_user_logged_in() ) {
return $endpoints;
}

unset(
$endpoints[‘/wp/v2/users’],
$endpoints[‘/wp/v2/users/(?P<id>[\d]+)’]
);

return $endpoints;
} );

/**
* Return a 404 for author archives.
*
* Runs before redirect_canonical() so that ?author=1 requests 404 rather than
* being redirected to /author/username/, which would leak the name in the
* Location header.
*/
add_action( ‘template_redirect’, function () {
if ( ! is_author() ) {
return;
}

global $wp_query;

$wp_query->set_404();
status_header( 404 );
nocache_headers();
}, 0 );

/**
* Return a generic login error.
*
* Stops the login screen from confirming whether a username exists.
*/
add_filter( ‘login_errors’, function () {
return __( ‘Login failed. Please check your credentials and try again.’ );
} );

The author archive handler runs on template_redirect at priority 0, and that priority is doing real work. Core’s canonical redirect runs on the same hook at priority 10. Getting in first means ?author=1 returns a 404 instead of a 301 with the username sitting in the Location header.

Heads up: If you have real author archives you want indexed, delete that block and keep the rest. The generic login error is worth thinking about too if your site has a lot of non-technical users. It removes the difference between “you typed your password wrong” and “that account doesn’t exist”, which is the whole point, but it does make support requests vaguer.

Disable XML-RPC

XML-RPC is the remote publishing interface WordPress had long before the REST API existed. Almost nothing uses it now, but xmlrpc.php is still sitting there on every install accepting requests. It’s one of the most reliably brute-forced files on the internet, mostly because system.multicall lets someone try hundreds of logins in a single HTTP request.

The pingback method is the other half of it. It can be pointed at a third party and used to bounce traffic off your server, with your site as the unwitting participant.

<?php
/**
* Plugin Name: Disable XML-RPC
* Description: Disables the XML-RPC interface, the pingback methods and the pingback advertising header.
* Author: WPExplorer
* Version: 1.0.0
*/

defined( ‘ABSPATH’ ) || exit;

/**
* Disable XML-RPC methods that require authentication.
*/
add_filter( ‘xmlrpc_enabled’, ‘__return_false’ );

/**
* Remove the pingback methods.
*
* These stay available even when xmlrpc_enabled is false because they do not
* require authentication.
*/
add_filter( ‘xmlrpc_methods’, function ( $methods ) {
unset(
$methods[‘pingback.ping’],
$methods[‘pingback.extensions.getPingbacks’]
);
return $methods;
} );

/**
* Remove the X-Pingback header.
*/
add_filter( ‘wp_headers’, function ( $headers ) {
unset( $headers[‘X-Pingback’] );
return $headers;
} );

/**
* Remove the pingback URL from bloginfo() output.
*/
add_filter( ‘bloginfo_url’, function ( $output, $show ) {
if ( ‘pingback_url’ === $show ) {
return ”;
}
return $output;
}, 10, 2 );

Heads up: The xmlrpc_enabled filter on its own isn’t enough, and this is the bit most snippets get wrong. It only disables methods that need authentication. The pingback methods never needed a login in the first place, so they keep working. That’s why I unset them separately. If your host lets you block xmlrpc.php at the server level, do that too, since it stops the request before PHP even runs.

Disable Application Passwords

Application passwords let external tools authenticate against the REST API without using a real password. They’re a genuinely good feature if you’re using them. If you’re not, they’re an authentication method sitting open on every account on the site, and one your client has never heard of and is never going to audit.

On a site where nothing is making REST requests from outside, switching them off removes a whole category of credential you’d otherwise have to think about.

<?php
/**
* Plugin Name: Disable Application Passwords
* Description: Disables application passwords and removes the section from user profiles.
* Author: WPExplorer
* Version: 1.0.0
*/

defined( ‘ABSPATH’ ) || exit;

add_filter( ‘wp_is_application_passwords_available’, ‘__return_false’ );

Heads up: Check before you use this one. Mobile apps, headless frontends, deployment scripts, uptime monitors and some backup and migration plugins all authenticate this way. If the site quietly depends on it, something will break and the error message won’t make it obvious why.

Disable File Editor

The built-in plugin and theme editors let anyone with the right capability edit live PHP on your production server from a browser. If an admin account ever gets compromised, that editor is the quickest route from stolen password to permanent backdoor.

The usual advice is to define DISALLOW_FILE_EDIT in wp-config.php, and that’s still the best option if you can get to that file. Plenty of managed hosts don’t give you access, so this does it from mu-plugins instead with a fallback for when the constant is already defined.

<?php
/**
* Plugin Name: Disable File Editor
* Description: Disables the built-in WordPress plugin and theme file editors.
* Author: WPExplorer
* Version: 1.0.0
*/

defined( ‘ABSPATH’ ) || exit;

// Disable plugin and theme file editors.
if ( ! defined( ‘DISALLOW_FILE_EDIT’ ) ) {
define( ‘DISALLOW_FILE_EDIT’, true );
} else {
add_filter( ‘file_mod_allowed’, function( $allowed, $context ) {
if ( in_array( $context, array( ‘capability_edit_themes’, ‘capability_edit_plugins’ ), true ) ) {
return false;
}

return $allowed;
}, 10, 2 );
}

That else branch matters more than it looks. Some hosts define DISALLOW_FILE_EDIT as false in a mu-plugin of their own, and once a constant is defined you can’t redefine it. The file_mod_allowed filter gives you a second shot at it.

Disable User Registration

The Anyone can register setting under Settings → General is one checkbox. One misclick, or one plugin doing something unexpected, and your site is open to public registration. Bots find that checkbox remarkably fast.

Filtering the option means registration is off no matter what’s in the database, and it stays off even if someone ticks the box.

<?php
/**
* Plugin Name: Disable User Registration
* Description: Disables user registration.
* Author: WPExplorer
* Version: 1.0.0
*/

defined( ‘ABSPATH’ ) || exit;

// Disable user registration.
add_filter( ‘option_users_can_register’, ‘__return_false’ );

Heads up: This is for single-site installs. Multisite handles registration through its own network setting, so use the Network Settings screen there instead.

Comments are either the best part of a site or a permanent moderation tax. Which one you get mostly depends on how much of the machinery around them you leave switched on.

There are plenty of plugins that rip comments out of WordPress completely. This one is more specific, and it’s what I use on the Total theme demos. Existing comments stay visible so visitors can see how the theme styles them, but nobody can actually post anything.

It’s surprisingly handy outside of demos too. Archived blogs, docs sites and portfolios often want the old discussion to stay readable without leaving the door open to spam.

<?php
/**
* Plugin Name: Disable Comments
* Description: Prevents comment submissions.
* Author: WPExplorer
* Version: 1.0.0
*/

defined( ‘ABSPATH’ ) || exit;

/**
* Prevent comment submissions.
*
* Keeps comments visible for theme demos but blocks visitors
* from creating comments.
*/
add_filter( ‘preprocess_comment’, function ( $comment_data ) {
if ( ! is_user_logged_in() ) {
wp_die(
‘Comments are disabled on live demos. This site is for preview purposes only.’,
‘Demo Site’,
array(
‘response’ => 403,
‘back_link’ => true,
)
);
}
return $comment_data;
} );

/**
* Add honeypot field to catch automated submissions.
*/
function wpexdc_comment_honeypot() {
if ( ! is_user_logged_in() ) {
echo ‘<input type=”hidden” name=”total_demo_comment_check” value=”1″>’;
}
}
add_action( ‘comment_form_logged_in_after’, ‘wpexdc_comment_honeypot’ );
add_action( ‘comment_form_after_fields’, ‘wpexdc_comment_honeypot’ );

Logged-in users can still comment, which keeps it usable for internal review. If you want to block everyone, just remove the is_user_logged_in() check.

Disable Trackbacks

Trackbacks and pingbacks were a good idea in 2005. These days they’re almost entirely a spam vector. Closing them site-wide gets rid of a whole category of moderation work.

<?php
/**
* Plugin Name: Disable Trackbacks
* Description: Disables trackbacks and pingbacks.
* Author: WPExplorer
* Version: 1.0.0
*/

defined( ‘ABSPATH’ ) || exit;

// Disable trackbacks and pingbacks.
add_filter( ‘pings_open’, ‘__return_false’ );

Heads up: This closes pings on new and existing content, but it doesn’t touch the XML-RPC pingback endpoint, which is a separate way into the same feature. Pair it with the next plugin if you want the whole thing gone.

Obfuscate Email Shortcode

Putting an email address on a page as plain text is an open invitation to scrapers. The usual workarounds involve JavaScript, or writing it out as “hello [at] example [dot] com”, which is ugly and annoying for actual humans.

WordPress has a built-in function for this that hardly anyone uses. antispambot() encodes the address as HTML entities. Browsers decode them without any fuss, so visitors see and click a normal email address, while a scraper reading the raw HTML just gets a wall of entity codes. This wraps it in a shortcode.

<?php
/**
* Plugin Name: Obfuscate Email Shortcode
* Description: Provides the [obfuscate_email] shortcode, which outputs an email address as HTML entities so scrapers can’t read it as plain text.
* Version: 1.0.0
* Author: WPExplorer
*/

defined( ‘ABSPATH’ ) || exit;

/**
* Renders an obfuscated email address, optionally as a mailto link.
*
* [obfuscate_email email=”hello@example.com”]
* [obfuscate_email email=”hello@example.com” text=”Email us”]
* [obfuscate_email email=”hello@example.com” link=”false”]
*
* The text attribute is the link label and is ignored when link is false.
*
* @param array $atts Shortcode attributes.
* @return string
*/
add_shortcode( ‘obfuscate_email’, function ( $atts ) {
$atts = shortcode_atts(
array(
’email’ => ”,
‘text’ => ”,
‘link’ => ‘false’,
‘class’ => ”,
),
$atts,
‘obfuscate_email’
);

$email = sanitize_email( trim( $atts[’email’] ) );

if ( ! is_email( $email ) ) {
return ”;
}

// antispambot() encodes the address as HTML entities, which browsers decode
// in both the href and the link text, so its output is not escaped again.
if ( ! filter_var( $atts[‘link’], FILTER_VALIDATE_BOOLEAN ) ) {
return $atts[‘class’]
? sprintf(
‘<span class=”%1$s”>%2$s</span>’,
esc_attr( $atts[‘class’] ),
antispambot( $email )
)
: antispambot( $email );
}

return sprintf(
‘<a href=”https://www.wpexplorer.com/wordpress-mu-plugins-collection/mailto:%1$s”%2$s>%3$s</a>’,
antispambot( $email ),
$atts[‘class’] ? ‘ class=”‘ . esc_attr( $atts[‘class’] ) . ‘”‘ : ”,
$atts[‘text’] ? esc_html( $atts[‘text’] ) : antispambot( $email )
);
} );

Here’s how you’d use it:

[obfuscate_email email=”hello@example.com”]
[obfuscate_email email=”hello@example.com” link=”true” text=”Email us”]
[obfuscate_email email=”hello@example.com” link=”true” class=”contact-link”]

The link attribute defaults to false, so you get a plain obfuscated address unless you ask for a mailto: link. The text attribute sets the link label and gets ignored when link is off.

Heads up: This raises the bar, it doesn’t make you invulnerable. A scraper that renders the page or decodes entities will still find the address. But it works very well against the simple regex-based harvesters that make up most of the problem.

Admin Experience

These don’t touch the frontend at all. They’re about handing over a site that feels finished, and about not having to scroll past three upgrade prompts to get to your own content.

Disable Admin Bar

The admin toolbar is genuinely useful, right up until it starts interfering with your frontend. It pushes the html element down by 32 pixels, which breaks sticky headers, full-height hero sections and anything using 100vh. It’s also in the way when you’re taking screenshots or reviewing a design.

One filter turns it off for everyone on the frontend and leaves the admin alone.

<?php
/**
* Plugin Name: Disable Admin Bar
* Description: Disables the WordPress admin toolbar on the frontend for logged in users.
* Author: WPExplorer
* Version: 1.0.0
*/

defined( ‘ABSPATH’ ) || exit;

add_filter( ‘show_admin_bar’, ‘__return_false’ );

If you’d rather keep it for admins and hide it from everyone else, swap __return_false for a closure that returns current_user_can( ‘manage_options’ ).

Disable WP Events and News Dashboard Widget

The Events and News widget pulls WordPress community events based on the visitor’s location, plus news from the official blog. It’s a nice idea, but it means an external HTTP request on the dashboard and clients tend to find it confusing at best.

<?php
/**
* Plugin Name: Disable WP Events News Dashboard Widget
* Description: Removes the WordPress Events and News widget from the dashboard.
* Author: WPExplorer
* Version: 1.0.0
*/

defined( ‘ABSPATH’ ) || exit;

add_action( ‘wp_dashboard_setup’, function() {
remove_meta_box( ‘dashboard_primary’, ‘dashboard’, ‘side’ );
} );

The same remove_meta_box approach works on the other core dashboard widgets. Swap in dashboard_quick_press, dashboard_activity or dashboard_site_health with the right context argument.

Hide Admin Notices

Open the plugins screen on a site running twenty plugins and you’ll find upgrade prompts, review requests, discount banners and setup wizards stacked several deep before you get to anything useful. It’s probably the most common complaint I hear from clients, and it makes a site you built look unfinished through no fault of your own.

This hides all of it from anyone who can’t manage options. Admins still see the notices that matter, editors and authors get a clean screen.

<?php
/**
* Plugin Name: Hide Admin Notices
* Description: Hides admin notices from users who cannot manage options.
* Author: WPExplorer
* Version: 1.0.0
*/

defined( ‘ABSPATH’ ) || exit;

add_action( ‘admin_head’, function () {
if ( current_user_can( ‘manage_options’ ) ) {
return;
}

remove_all_actions( ‘admin_notices’ );
remove_all_actions( ‘all_admin_notices’ );
remove_all_actions( ‘network_admin_notices’ );
remove_all_actions( ‘user_admin_notices’ );
}, 1 );

Heads up: This is a blunt instrument. It removes every callback on those hooks, including the legitimate ones, so a plugin reporting a form error through admin_notices will fail silently for non-admins. Test it against whatever your editors actually use. If you want something more surgical, target specific plugin callbacks by name instead of calling remove_all_actions().

Media Library File Size

The Media Library list view tells you the date, the author and the post an image is attached to. It doesn’t tell you how big the file is. When you’re trying to work out why the uploads folder has ballooned, that’s exactly the column you want.

Since WordPress 6.0 the file size is already sitting in the attachment metadata, so showing it costs nothing. No extra queries, no extra data, just a column reading something that’s already there.

<?php
/**
* Plugin Name: Media Library File Size
* Description: Adds a file size column to the Media Library using stored attachment metadata.
* Author: WPExplorer
* Version: 1.0.0
*/

defined( ‘ABSPATH’ ) || exit;

/**
* Add the file size column.
*/
add_filter( ‘manage_media_columns’, function ( $columns ) {
$columns[‘file_size’] = __( ‘File Size’ );
return $columns;
} );

/**
* Display the file size.
*/
add_action( ‘manage_media_custom_column’, function ( $column_name, $post_id ) {
if ( ‘file_size’ !== $column_name ) {
return;
}
$metadata = wp_get_attachment_metadata( $post_id );
if ( empty( $metadata[‘filesize’] ) ) {
echo ‘—’;
return;
}
echo esc_html( size_format( $metadata[‘filesize’] ) );
}, 10, 2 );

Heads up: You’ll see an em dash for uploads from before WordPress 6.0 and for some file types where core never recorded a size. I’ve deliberately left the column unsortable. The size lives inside a serialized array, which you can’t order on numerically, and making it sortable means writing the size out to its own meta key for every attachment on the site. That’s a lot of stored data for a column you’re mostly scanning rather than sorting.

Staging and Demo Sites

Read this bit twice before you install anything from it. Both of these are the right call in the environment they’re meant for and actively harmful on a live site.

Disable Emails

Nothing ruins a Monday morning like finding out your staging site has been sending real order confirmations to real customers for a week. This short-circuits wp_mail() so no email leaves the site at all.

It’s the first thing I install on any staging clone or local copy of a live site.

<?php
/**
* Plugin Name: Disable Emails
* Description: Disables all outgoing emails.
* Author: WPExplorer
* Version: 1.0.0
*/

defined( ‘ABSPATH’ ) || exit;

/**
* Prevent all outgoing emails.
*/
add_filter( ‘pre_wp_mail’, ‘__return_false’ );

Heads up: Don’t put this on production. It blocks password resets, new user notifications, WooCommerce order emails, contact form submissions and everything else, silently. Only use it where you’re certain nothing needs to send mail. And if you’d rather see what would have been sent instead of just dropping it, a mail logging plugin is the better tool.

Disable Password Reset

Password reset emails are one of the more common ways a site gets probed. They also cause real problems on demo sites, where one visitor resetting the shared demo account locks everyone else out.

It’s also the right move on sites where authentication happens somewhere else, like an SSO or LDAP setup, where the built-in reset flow can leave people with a password that doesn’t do anything.

<?php
/**
* Plugin Name: Disable Password Reset
* Description: Disables password reset functionality.
* Author: WPExplorer
* Version: 1.0.0
*/

defined( ‘ABSPATH’ ) || exit;

/**
* Hide the lost password link on the login screen.
*/
add_filter( ‘lost_password_html_link’, ‘__return_empty_string’ );

/**
* Disable password reset requests.
*/
add_action( ‘login_init’, function () {
if (
isset( $_GET[‘action’] )
&& in_array( $_GET[‘action’], array( ‘lostpassword’, ‘retrievepassword’ ), true )
) {
wp_die(
‘Password reset functionality is disabled.’,
‘Demo Site’,
array(
‘response’ => 403,
)
);
}
} );

Heads up: This is a genuine lockout risk on a normal site. Only use it where you’ve got another way back in. Admins can still set passwords manually from the user profile screen.

Get Them All on GitHub

All of these live in a single repository so you can browse the source and pull in whatever you need without copying and pasting out of a blog post.

MU Plugins GitHub Repo

Download it as a ZIP and copy across the files you want. That’s deliberately the only instruction I’m giving. Several of these are meant for staging sites rather than production, so installing all of them at once isn’t something I’d recommend on a site that matters.

If you’d rather work from a clone, keep it somewhere outside your web root and copy files across from there. Cloning straight into wp-content/mu-plugins/ leaves a .git directory sitting in a publicly reachable folder, and it’ll fail outright on hosts that already keep their own files in there.

Wrapping Up

None of these snippets are complicated, and that’s sort of the point. They’re mostly a few lines each, solving problems you’ve probably solved before. The difference is where they live. Stick them in mu-plugins and a theme switch won’t wipe them out.

If you’re not sure where to start, go with the safe ones. Clean Head, Disable Admin Bar, Disable File Editor and Disable Trackbacks are fine on pretty much any site. Then add the more targeted ones as you need them, and keep Disable Emails on your staging environments where it belongs.

I don’t run comments here, so if you’ve got a question about any of these, spot a bug, or keep something in your own mu-plugins folder that isn’t in the collection, the issues page on GitHub is the place for it.



Source link

Leave a Comment

Your email address will not be published. Required fields are marked *