Introduction
Imagine never losing your custom tweaks after a core update. I remember the first time I dove into WordPress, hacking core files to change a tiny bit of CSS—only to watch my work disappear the moment I hit “Update.” That frustration drove me to discover hooks and filters, the powerful tools that let you plug custom code into WordPress without ever touching core.
Instead of scrambling to reapply my edits every time a new version rolled out, I learned to hook into exact moments—like when a post is published—or filter the very data that WordPress outputs. Hooks and filters became my safety net, transforming tedious maintenance into a smooth, reliable workflow.
In this guide, I’ll walk you through exactly how hooks and filters work, share real-world examples you can copy & paste, and reveal best practices I’ve picked up along the way. By the end, you’ll know how to:
- Spot the difference between actions (run code at specific events) and filters (modify data on the fly)
- Register your own hooks in themes or plugins with clean, future-proof code
- Drop in 11 practical snippets to add features like custom dashboards, deferred scripts, and more
- Organize and document your customizations so you never lose track of what’s happening
- Tackle advanced tips for debugging and optimizing your hooks
Ready to stop hacking core files and start working smarter? Let’s dive in.
What Are Hooks & Filters?
When I explain hooks and filters to friends, I use the Lego analogy: WordPress is the big, pre-built set, and hooks are the little connection points where you can snap on your own pieces. Here’s the quick rundown:
Feature | Actions | Filters |
---|---|---|
Job | Run extra code at a specific moment (“Hey, a post just published—do something!”) | Catch some data on its way out, tweak it, and hand it back |
Core Function | do_action() | apply_filters() |
Register With | add_action() | add_filter() |
Must Return a Value? | No—fire and forget | Yes—return the (possibly changed) data |
Why bother? Because hooks and filters:
- Protect your customizations. They live in a theme or plugin, so core updates won’t steam-roll them.
- Keep things tidy. All your tweaks are in one place, not scattered across random files.
- Boost performance. You only load code when it’s actually needed.
If you keep two words in mind, you’ll never mix them up: actions do, filters change.
Getting Started with Actions
Let’s roll up our sleeves and fire off our first action. We’ll send ourselves an email every time a new post goes live—handy if you manage a multi-author blog.
1. Write the callback function. This is the bit that actually does the work.
function notify_admin_on_publish( $post_id ) {
$post = get_post( $post_id );
$title = $post->post_title;
$link = get_permalink( $post_id );
wp_mail(
'me@mydomain.com',
"🎉 New Post Published: {$title}",
"Check it out here: {$link}"
);
}
2. Hook it in with add_action()
.
add_filter( 'the_content', 'add_reader_disclaimer', 20 );
the_content
hands you the entire post body right before WordPress prints it.- Priority 20 runs our filter after the built-in ones (like shortcodes).
3. Test it. Open a post; scroll to the bottom. Disclaimer delivered. No template edits required, no hard-coded HTML to forget later.
That’s the entire filter workflow: grab the data, tweak it, give it back. Keep that flow in mind and you’ll never hit the dreaded “white screen” from forgetting a return
.
11 Real-World Snippets You Can Drop In Today
Below are the exact copy-and-paste tricks I lean on when clients ask, “Can WordPress do ___?” Steal them, tweak them, ship them.
Heads-up: Put these in a small site-specific plugin or your child theme’s functions.php
. Keep the main theme clean.
1. Change the Excerpt Length
function custom_excerpt_length( $length ) {
return 30; // words
}
add_filter( 'excerpt_length', 'custom_excerpt_length', 20 );
Why: Tighter teaser paragraphs boost click-through from archive pages.
2. Disable Emoji Bloat
function strip_wp_emojis() {
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'wp_print_styles', 'print_emoji_styles' );
}
add_action( 'init', 'strip_wp_emojis' );
Why: Shaves a few requests and kilobytes for faster first paint.
3. Register a Hero Image Size
function hero_image_size() {
add_image_size( 'hero', 1600, 600, true );
}
add_action( 'after_setup_theme', 'hero_image_size' );
add_filter( 'image_size_names_choose', function ( $sizes ) {
$sizes['hero'] = 'Hero (1600 × 600)';
return $sizes;
} );
Why: Guarantees full-width banners crop the same on every page.
4. Include Your “Portfolio” CPT in RSS
function portfolio_in_feed( $query ) {
if ( $query->is_feed() && ! isset( $query->query_vars['post_type'] ) ) {
$query->set( 'post_type', [ 'post', 'portfolio' ] );
}
}
add_action( 'pre_get_posts', 'portfolio_in_feed' );
Why: Your subscribers see new case studies without any extra clicks.
5. Drop a “Site Stats” Dashboard Widget
function add_site_stats_widget() {
wp_add_dashboard_widget( 'site_stats', 'Site Stats', function () {
echo '<p><strong>Posts:</strong> ' . wp_count_posts()->publish . '</p>';
echo '<p><strong>Users:</strong> ' . count_users()['total_users'] . '</p>';
} );
}
add_action( 'wp_dashboard_setup', 'add_site_stats_widget' );
Why: Clients love opening the admin and seeing fresh numbers.
6. Auto-Build a Table of Contents
function inject_toc( $content ) {
if ( is_singular( 'post' ) && preg_match_all( '/<h2>(.*?)<\/h2>/', $content, $found ) ) {
$toc = '<div class="toc"><h3>On this page</h3><ul>';
foreach ( $found[1] as $i => $title ) {
$slug = 'section-' . ( $i + 1 );
$content = preg_replace(
"/<h2>{$title}<\/h2>/",
"<h2 id=\"{$slug}\">{$title}</h2>",
$content,
1
);
$toc .= "<li><a href=\"#{$slug}\">{$title}</a></li>";
}
$toc .= '</ul></div>';
$content = $toc . $content;
}
return $content;
}
add_filter( 'the_content', 'inject_toc', 5 );
Why: Longer posts get instant jump links—great for UX and featured snippets.
7. Serve a Random Quote via the REST API
function quote_endpoint() {
register_rest_route( 'demo/v1', '/quote', [
'methods' => 'GET',
'callback' => function () {
$quotes = [ 'Stay hungry, stay foolish.', 'Ship early, ship often.', 'Code is poetry.' ];
return [ 'quote' => $quotes[ array_rand( $quotes ) ] ];
},
] );
}
add_action( 'rest_api_init', 'quote_endpoint' );
Why: Tiny companion apps or static sites can pull fun content from WordPress.
8. Defer All Front-End JavaScript
function defer_js( $tag, $handle ) {
return ! is_admin() ? str_replace( '<script ', '<script defer ', $tag ) : $tag;
}
add_filter( 'script_loader_tag', 'defer_js', 10, 2 );
Why: Lets HTML paint first, bumping up your PageSpeed scores.
9. Nightly Cleanup of Expired Transients
if ( ! wp_next_scheduled( 'cleanup_transients' ) ) {
wp_schedule_event( strtotime( '02:00:00' ), 'daily', 'cleanup_transients' );
}
add_action( 'cleanup_transients', function () {
global $wpdb;
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '\_transient\_%' AND option_value < UNIX_TIMESTAMP()" );
} );
Why: Keeps the options table slim, avoiding mystery slowdowns months later.
10. Brand the Login Logo Link
add_filter( 'login_headerurl', fn() => home_url() );
Why: Clicking the logo sends users back to your homepage, not WordPress.org.
11. Add a Body Class for Logged-In Users
function logged_in_body_class( $classes ) {
if ( is_user_logged_in() ) {
$classes[] = 'user-logged-in';
}
return $classes;
}
add_filter( 'body_class', 'logged_in_body_class' );
Why: Target special styling or tracking scripts only for members.
Pick the ones that solve today’s pain points, keep the rest in your toolbox, and remember: always test on staging first. In the next section we’ll tighten everything up with best practices so your newfound superpowers never bite back.
Best Practices for Hooks & Filters
How I keep my sites fast, sane, and update-proof.
1. Prefix everything—no exceptions.
Early on I named a function add_social_icons()
, only to discover another plugin using the exact same name. White screen. Now every callback starts with my namespace, e.g. mysite_add_social_icons()
. Collisions solved before they happen.
2. Treat priority like a traffic light.
Think of the default 10
as “yellow.” Go lower (5
, 1
) if you must run first, higher (20
, 50
) if you need to override someone else. I jot a comment beside any non-default number so future-me remembers why.
add_filter( 'the_content', 'mysite_toc', 5 ); // run before shortcodes
3. Pass only what you need.
If your callback uses one argument, set $accepted_args
to 1
. Extra data costs memory and can slow high-traffic sites.
add_action( 'save_post', 'mysite_clear_cache', 10, 1 ); // $post_id only
4. Return something—always.
Filters that forget the return
break output. My personal trick: type return $content;
first, then wrap tweaks around it.
5. Keep logic lightweight.
Database queries, API calls, or heavy loops inside the_content
will drag every page view. Offload heft to background cron jobs or cache wherever possible.
6. Document like you’re explaining to a stranger.
A quick PHPDoc block saves headaches later—especially when multiple hooks touch the same data.
/**
* Append affiliate disclosure to single posts.
*
* @param string $content Post body.
* @return string Modified content.
*/
7. Unhook when you must.
Third-party plugin doing something odd? remove_action()
or remove_filter()
lets you surgically disable it without editing vendor code.
remove_filter( 'the_content', 'annoying_plugin_autolinks', 15 );
8. Stage > Live.
I’ve torched too many production sites at 2 AM. Spin up a staging clone, test, commit, deploy. Future-you will sleep better.
Advanced Tips & Troubleshooting
When things get weird, here’s my playbook.
Catch every hook in real time.
Install the free Query Monitor plugin, open the “Hooks & Actions” panel, and watch which hooks fire on each page. It’s like turning on the lights in a messy room.
Log without spamming yourself.
Sprinkle error_log( __FUNCTION__ );
inside a suspect callback, then tail wp-content/debug.log
. Faster than print-r’ing to the screen.
Measure, don’t guess.
Wrap expensive code with micro-timers:
$start = microtime( true );
/* ...heavy lifting... */
error_log( 'My filter ran in ' . round( microtime( true ) - $start, 3 ) . 's' );
Anything over ~0.05 s per request deserves caching or a cron rethink.
Override a third-party filter safely.
Sometimes a plugin filters data after you do. Bump your priority number higher:
add_filter( 'the_title', 'mysite_force_title_case', 99 );
Still losing? remove_filter()
their callback, then add yours.
Register hooks only when needed.
Why load a WooCommerce tweak on the blog? Guard with conditionals:
if ( class_exists( 'WooCommerce' ) ) {
add_action( 'woocommerce_thankyou', 'mysite_send_sms_receipt' );
}
Dump all hooks to a file (last-ditch).
If the rabbit hole goes deep, run:
file_put_contents( __DIR__ . '/hooks.txt', print_r( $wp_filter, true ) );
Open hooks.txt
, search for the rogue callback, and track it back to its source.
Profile in production—carefully.
New Relic, Blackfire, or even the built-in WordPress Performance Profiler can reveal slow hooks under real traffic. Just remember to disable verbose logging afterward; your disk will thank you.
With these habits and tools, you’ll spend less time chasing gremlins and more time shipping features that wow your readers. Up next, we’ll wrap everything with a quick recap and a few calls to action so you can keep the momentum rolling.
Conclusion & Next Steps
If you’re still with me, congrats—you’ve gone from hacking core files to wielding hooks and filters like a pro. Here’s the big takeaway:
- Actions let you do things at the perfect moment.
- Filters let you change any data before WordPress shows it to the world.
- Together they keep your custom code upgrade-safe, tidy, and lightning fast.
I’ve packed years of late-night debugging and client fire-drills into this guide, but the real magic happens when you start experimenting on your own site.
Your 3-Step Action Plan
- Grab the Cheatsheet – I condensed every common hook name, priority tip, and gotcha into a one-page PDF.
Download the Hooks & Filters Cheatsheet - Test on Staging Tonight – Copy one snippet (the emoji remover is an easy win), drop it into your staging site, and watch how smoothly it works. Momentum starts with a single line of code.
- Join the Conversation – Scroll down and share your favorite hook in the comments. I read every reply and often feature the best tips in future posts.
Thanks for reading, and happy hooking!
Leave a Reply