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
  1. What Are MU-Plugins?
  2. How to Use These Snippets
  3. Cleanup and Performance
  4. Privacy and Third-Party Requests
  5. Security and Hardening
  6. Comments and Spam
  7. Admin Experience
  8. Staging and Demo Sites
  9. Get Them All on GitHub
  10. Wrapping 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.

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="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.

Similar Posts