Action Hook since 2.0.0
pre_get_posts
Modify any WP_Query before it runs
Description
pre_get_posts fires for every query — main and secondary. Always check is_main_query() (and is_admin()) or you'll silently break widgets, related-post queries, and REST requests.
When it runs
Runs for the main front-end query, admin list tables, and any custom WP_Query — every single instantiation.
Signature
do_action( 'pre_get_posts', WP_Query $query );Parameters
$queryWP_Query — The query object, passed by reference — modify it directly.
Examples
Basic
add_action( 'pre_get_posts', function( $query ) {
if ( ! is_admin() && $query->is_main_query() && is_post_type_archive( 'book' ) ) {
$query->set( 'posts_per_page', 24 );
}
} );Change how many books show on the archive without touching the theme's template.
Real case
add_action( 'pre_get_posts', function( $query ) {
if ( ! is_admin() && $query->is_main_query() && is_search() ) {
$query->set( 'post_type', [ 'post', 'book' ] );
}
} );Include a custom post type in front-end search results.
Common Use Cases
- Change posts_per_page for an archive
- Include/exclude post types from search
- Reorder posts on a custom query
- Restrict a query by taxonomy or meta
Common mistakes
- Forgetting the is_main_query() and !is_admin() checks — the callback then also fires on admin list tables, widgets, and unrelated secondary queries
Related hooks
FAQ
Why did my widget's post list get shortened after I used pre_get_posts?
You likely modified every WP_Query, not just the main one. Add $query->is_main_query() to the condition.
Source: wp-includes/class-wp-query.php