Hooks: actions and filters
All of WordPress is built on hooks. In hundreds of places the core announces: "scripts are about to load", "a post was saved", "here is the article text before output". Your code attaches to those points and changes the site's behaviour without touching a single core file.
Actions
An action is an event a function is tied to:
// functions.php
add_action('wp_footer', function () {
echo '<p class="copyright">This site runs on WordPress</p>';
});The function runs at the moment the theme calls wp_footer().
Filters
A filter receives a value, changes it and must return it:
// functions.php
add_filter('the_title', function (string $title): string {
return strtoupper($title);
});
add_filter('excerpt_length', fn () => 20);The first filter uppercases every title, the second sets the excerpt length to 20 words.
How they differ
| Action | Filter | |
|---|---|---|
| Purpose | Do something | Change something |
| Returns a value | No | Always |
| Attached with | add_action() | add_filter() |
| Called in the core with | do_action() | apply_filters() |
Priority and arguments
The third argument is the priority: the lower the number, the earlier the function runs (10 by default). The fourth is how many arguments it accepts:
add_action('save_post', function (int $postId, WP_Post $post, bool $update) {
if ($update) {
error_log("Post $postId changed: {$post->post_title}");
}
}, 20, 3);Without the fourth argument the function receives only the first parameter.
Common hooks
| Hook | When it fires |
|---|---|
after_setup_theme | The theme is loaded — time to declare its features |
init | WordPress is loaded — time to register post types |
wp_enqueue_scripts | Adding the site's styles and scripts |
admin_menu | Building the dashboard menu |
save_post | A post was saved |
wp_head, wp_footer | Output in head and before </body> |
the_content (filter) | The post text before output |
pre_get_posts | Before the main posts query |
Removing a handler
Someone else's function is taken off a hook by naming the same function and priority:
// functions.php
remove_action('wp_head', 'wp_generator');
remove_action('wp_head', 'print_emoji_detection_script', 7);The first line removes the tag with the WordPress version from the page.
Your own hooks
A theme or a plugin can create its own extension points:
<?php
// single.php
do_action('mytheme_before_content');
the_content();
echo esc_html(apply_filters('mytheme_read_more_text', 'Read more'));Now other code — a child theme, for example — can add a block before the text or replace the label:
add_filter('mytheme_read_more_text', fn () => 'Details →');Common mistake
A filter function that forgets return turns the value into null: titles, texts or links simply vanish from the page. A filter always returns the value — even when it does not need changing in this case:
add_filter('the_content', function (string $content): string {
if (!is_single()) {
return $content;
}
return $content . '<p>Thank you for reading!</p>';
});