Skip to content
WordPress Tips

WordPress Hooks Explained: A Practical Guide to Actions and Filters (With Real Examples)

Master WordPress hooks with practical, real-world examples. Learn how actions and filters work, understand priority, create custom hooks, and avoid the mistakes that break sites — the complete developer's reference for 2026.

Thakur Aarti
10 min read

Every WordPress plugin you’ve ever installed, every theme customization you’ve ever made, and every piece of custom functionality you’ve ever added to a site — they all work because of hooks. Hooks are the backbone of WordPress extensibility, and understanding them is the single most important skill that separates someone who uses WordPress from someone who truly controls it.

Yet most tutorials on hooks are either too abstract (explaining the concept without showing real use) or too shallow (showing one basic example and calling it a day). This guide takes a different approach. We’ll build your understanding from the ground up with practical, copy-paste-ready examples that solve real problems you’ll actually encounter.

What Are WordPress Hooks, Really?

A hook is a specific point in the WordPress execution process where you can insert your own code. Think of WordPress as a factory assembly line — your page moves through dozens of stages before it’s delivered to the browser. Hooks are designated spots on that assembly line where WordPress pauses and asks: “Does anyone have something to add or change here?”

There are exactly two types of hooks in WordPress, and the distinction between them is critical.

Actions let you do something at a specific point. They execute your code at a particular moment in the WordPress lifecycle — like adding a tracking script to the header, sending an email when a post is published, or registering a custom sidebar when widgets are initialized. Actions don’t return anything; they just run.

Filters let you modify something that WordPress is processing. They intercept a piece of data, give you a chance to change it, and then pass it along. Filters always receive data, and they must always return data — either modified or untouched. For example, you might filter the post title to add a prefix, filter the excerpt length, or filter the content to append a call-to-action.

If actions are verbs (do this), filters are adjectives (change this). That mental model will carry you through every hook scenario you encounter.

How Actions Work (With Real Examples)

To attach your code to an action hook, you use the add_action() function. It takes four parameters, though you’ll typically only need the first two:

add_action( 'hook_name', 'your_callback_function', priority, accepted_args );

The hook name is the specific point in WordPress where your code will run. The callback function is the function containing your custom code. The priority (default: 10) controls the order of execution when multiple functions are hooked to the same action — lower numbers run first. And accepted_args specifies how many arguments your function receives.

Example 1: Add a Google Analytics Script to the Header

The wp_head action fires inside the <head> section of every front-end page. This is the standard way to inject tracking scripts, custom meta tags, or preload directives.

function swiftly_add_analytics_script() {
    ?>
    <!-- Google Analytics -->
    <script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
    <script>
        window.dataLayer = window.dataLayer || [];
        function gtag(){dataLayer.push(arguments);}
        gtag('js', new Date());
        gtag('config', 'G-XXXXXXXXXX');
    </script>
    <?php
}
add_action( 'wp_head', 'swiftly_add_analytics_script' );

This is a clean, plugin-free approach. Your analytics code loads on every page without editing any template files directly.

Example 2: Send a Slack Notification When a Post Is Published

The publish_post action fires the moment a post transitions to “published” status. You can use this to trigger notifications, clear caches, or sync content to external platforms.

function swiftly_notify_slack_on_publish( $post_id, $post ) {
    $webhook_url = 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL';
    
    $message = sprintf(
        'New post published: *%s* by %s — %s',
        $post->post_title,
        get_the_author_meta( 'display_name', $post->post_author ),
        get_permalink( $post_id )
    );

    wp_remote_post( $webhook_url, array(
        'body' => wp_json_encode( array( 'text' => $message ) ),
        'headers' => array( 'Content-Type' => 'application/json' ),
        'blocking' => false,
    ) );
}
add_action( 'publish_post', 'swiftly_notify_slack_on_publish', 10, 2 );

Notice the 10, 2 at the end — priority 10 (default), and 2 accepted arguments ($post_id and $post). Without specifying 2 accepted args, WordPress would only pass the first parameter.

Example 3: Register a Custom Widget Area

The widgets_init action is where WordPress expects you to register sidebars and widget areas. This fires early in the initialization process, ensuring your widget areas are available throughout the theme.

function swiftly_register_cta_sidebar() {
    register_sidebar( array(
        'name'          => 'After Post CTA',
        'id'            => 'after-post-cta',
        'description'   => 'Appears below every blog post for conversion widgets.',
        'before_widget' => '<div class="post-cta-widget %2$s">',
        'after_widget'  => '</div>',
        'before_title'  => '<h3 class="cta-heading">',
        'after_title'   => '</h3>',
    ) );
}
add_action( 'widgets_init', 'swiftly_register_cta_sidebar' );

How Filters Work (With Real Examples)

Filters follow the same structure as actions, but with one critical difference: your callback function receives data, and it must return data. If you forget to return the value, you’ll silently break whatever WordPress was processing.

add_filter( 'hook_name', 'your_callback_function', priority, accepted_args );

Example 4: Change the Default Excerpt Length

WordPress excerpts default to 55 words, which is rarely ideal. The excerpt_length filter lets you change this globally without touching any template file.

function swiftly_custom_excerpt_length( $length ) {
    return 30; // Show 30 words instead of 55
}
add_filter( 'excerpt_length', 'swiftly_custom_excerpt_length' );

The function receives the current length ($length), and returns the new length. Simple, clean, and it works site-wide.

Example 5: Automatically Add a CTA Below Every Post

The the_content filter is one of the most powerful hooks in WordPress. It processes the post content right before it’s displayed, giving you the opportunity to append, prepend, or transform anything.

function swiftly_append_post_cta( $content ) {
    if ( ! is_singular( 'post' ) || ! is_main_query() ) {
        return $content;
    }

    $cta = '<div class="post-cta" style="background:#f0f4f8;padding:24px;border-radius:8px;margin-top:32px;">';
    $cta .= '<h3>Build Faster WordPress Sites</h3>';
    $cta .= '<p>Check out our lightweight, performance-first WordPress plugins built for developers who care about speed.</p>';
    $cta .= '<a href="/plugins/" style="display:inline-block;background:#2563eb;color:#fff;padding:10px 20px;border-radius:6px;text-decoration:none;">Browse Plugins</a>';
    $cta .= '</div>';

    return $content . $cta;
}
add_filter( 'the_content', 'swiftly_append_post_cta' );

The is_singular('post') check ensures this only runs on single blog posts — not pages, not archives, not custom post types. The is_main_query() check prevents the CTA from appearing in sidebar widgets or related post loops that also process content.

Example 6: Remove the WordPress Version Number From Your HTML

WordPress outputs its version number in a meta tag by default, which is a minor security concern. The the_generator filter controls this output.

function swiftly_remove_wp_version() {
    return '';
}
add_filter( 'the_generator', 'swiftly_remove_wp_version' );

By returning an empty string, you effectively remove the version number from the page source. This is a one-line security improvement that every WordPress site should have.

Understanding Hook Priority

When multiple functions are hooked to the same action or filter, WordPress executes them in order of priority. The default priority is 10, and lower numbers run first.

This matters more than most people realize. If a plugin hooks into the_content at priority 10 to add sharing buttons, and you hook in at priority 10 to add your CTA, the execution order is unpredictable. But if you set your priority to 20, your CTA will reliably appear after the sharing buttons.

// Runs first (priority 5)
add_filter( 'the_content', 'do_something_early', 5 );

// Runs at default time (priority 10)
add_filter( 'the_content', 'do_something_normal' );

// Runs later (priority 20)
add_filter( 'the_content', 'do_something_late', 20 );

// Runs last (priority 999)
add_filter( 'the_content', 'do_something_absolutely_last', 999 );

A common pattern is using priority 999 when you need to guarantee your function runs after everything else — useful for final cleanup operations or ensuring your output isn’t overridden by other plugins.

Removing Hooks: Unhooking Actions and Filters

Sometimes you need to remove functionality that WordPress core, your theme, or a plugin has added. The remove_action() and remove_filter() functions handle this, but they require you to know the exact callback function name and priority used when the hook was added.

// Remove the default WordPress emoji scripts (performance win)
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'wp_print_styles', 'print_emoji_styles' );

// Remove the Windows Live Writer manifest link
remove_action( 'wp_head', 'wlwmanifest_link' );

// Remove the RSD (Really Simple Discovery) link
remove_action( 'wp_head', 'rsd_link' );

The emoji removal alone eliminates two extra HTTP requests from every page load. These are the kinds of micro-optimizations that compound into measurable performance improvements across an entire site.

Timing matters when removing hooks. If a plugin adds a hook during plugins_loaded, you can’t remove it before that action has fired. The safest place to remove hooks is usually inside your own function hooked to after_setup_theme or init.

The Most Useful WordPress Hooks You Should Know

WordPress has hundreds of hooks, but you’ll use a handful of them constantly. Here are the ones that solve real problems most frequently.

init — Fires after WordPress is fully loaded but before any output is sent. This is where you register custom post types, custom taxonomies, and handle form submissions.

wp_enqueue_scripts — The correct place to load your CSS and JavaScript files. Never hardcode script tags in your header; always enqueue them properly through this hook.

admin_menu — Where you add custom pages to the WordPress admin dashboard. If you’re building a plugin with settings pages, this is your starting point.

save_post — Fires whenever a post is created or updated. Essential for custom meta box processing, syncing data to external APIs, or triggering cache invalidation.

pre_get_posts — Lets you modify the main WordPress query before it runs. Want to exclude a category from the blog page, change posts per page for a custom archive, or sort results differently? This is the hook.

wp_footer — Fires right before the closing </body> tag. Ideal for non-critical JavaScript, chat widgets, or conversion tracking pixels that don’t need to block rendering.

Creating Your Own Custom Hooks

This is where hooks go from useful to powerful. When you’re building plugins or complex themes, creating your own hooks lets other developers (or your future self) extend your code without modifying it directly.

// In your plugin: create a custom action hook
function swiftly_process_order( $order_id ) {
    // Your core order processing logic here
    $order_data = get_order_data( $order_id );
    
    // Let other plugins do something after order processing
    do_action( 'swiftly_after_order_processed', $order_id, $order_data );
}

// In another plugin or theme: hook into it
function my_custom_order_handler( $order_id, $order_data ) {
    // Send a custom notification, update inventory, etc.
}
add_action( 'swiftly_after_order_processed', 'my_custom_order_handler', 10, 2 );

For custom filters, use apply_filters() to allow modification of your data:

// In your plugin: create a filterable value
function swiftly_get_pricing_markup() {
    $default_markup = 1.15; // 15% markup
    
    // Let other code modify this value
    $markup = apply_filters( 'swiftly_pricing_markup', $default_markup );
    
    return $markup;
}

// Someone else can now change your markup rate
add_filter( 'swiftly_pricing_markup', function( $markup ) {
    return 1.20; // Override to 20%
} );

This is exactly how professional WordPress plugins are built. WooCommerce, for instance, has over 1,000 hooks — which is why it’s so extensible without anyone needing to modify its core files.

Common Hook Mistakes That Break Sites

Forgetting to return the value in a filter. This is the most common hook mistake. If your filter callback doesn’t return anything, the filtered value becomes null, which can cause white screens, missing content, or broken functionality. Always return the value — even if you didn’t change it.

Hooking too early. If you try to use a WordPress function before it’s available, you’ll get a fatal error. For example, calling get_current_user() before the init hook fires will fail. Always hook your code to an appropriate action rather than running it directly in functions.php.

Infinite loops with filters. If your filter callback triggers the same filter it’s hooked to, you’ll crash the site with a memory exhaustion error. A common example: hooking into the_content and calling apply_filters('the_content', $something) inside your callback.

Not checking context. A hook like the_content fires everywhere content is displayed — single posts, archives, widgets, REST API responses. Always check is_singular(), is_main_query(), or is_admin() to ensure your code only runs where you intend it to.

Debugging Hooks: Finding What’s Hooked Where

When you need to understand what’s happening on a particular hook — which functions are attached, in what order — you can inspect the global $wp_filter variable.

// Dump all callbacks attached to a specific hook
function swiftly_debug_hook( $hook_name ) {
    global $wp_filter;
    
    if ( isset( $wp_filter[ $hook_name ] ) ) {
        echo '<pre>';
        print_r( $wp_filter[ $hook_name ] );
        echo '</pre>';
    }
}

// Usage: add this temporarily to see what's hooked to 'the_content'
add_action( 'wp_footer', function() {
    swiftly_debug_hook( 'the_content' );
} );

For a more visual approach, the Query Monitor plugin shows all hooks and their attached callbacks with execution times — invaluable when diagnosing performance issues or unexpected behavior caused by hook interactions.

Final Thoughts

Hooks are the reason WordPress powers over 40% of the web. They allow an ecosystem of tens of thousands of plugins and themes to coexist, extend each other, and modify WordPress behavior without ever touching core files.

The mental model is straightforward: actions let you do something at a specific point, filters let you change something that’s being processed. Master those two concepts, learn the dozen or so hooks you’ll use most frequently, and you’ll be able to customize WordPress in ways that no page builder or settings panel can match.

Start small — hook into wp_footer to add a simple script, or filter excerpt_length to change your blog layout. Once you see how cleanly hooks work, you’ll start spotting opportunities to use them everywhere. That’s the moment WordPress stops being a tool you configure and becomes a platform you control.

Leave a Reply

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