pre_get_posts lets you edit a query’s arguments just before it runs — post counts on an archive, restricting to certain posts, changing the ordering. The important part is that you are modifying the main query rather than building a second one in the template, which keeps pagination and the conditional tags intact.
This hook fires for every query
The first thing to know is that it runs for every WP_Query instance, not just the main one. It fires while menus are built, while a widget fetches recent posts, and while the admin post list table is drawn.
Which is why the first two lines of the callback are effectively fixed.
add_action( 'pre_get_posts', 'wper_book_archive' );
function wper_book_archive( $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( ! $query->is_post_type_archive( 'book' ) ) {
return;
}
$query->set( 'posts_per_page', 12 );
$query->set( 'orderby', 'menu_order' );
}
There is a reason to test with $query-> methods rather than the global conditional tags. The globals always describe the main query, so they can answer about something other than the query currently in your hands.
Read, modify, write — never overwrite
Calling set() on meta_query or tax_query outright discards whatever was already there. Another plugin may have set it; WordPress itself may have set it for that screen. Read it, add to it, put it back.
$meta = (array) $query->get( 'meta_query' );
$meta[] = [
'key' => '_in_stock',
'value' => '1',
'compare' => '=',
];
$query->set( 'meta_query', $meta );
Somebody else’s condition can empty your archives
The failure runs in the other direction too. In this repository, ten custom taxonomy archives once stood at zero posts. The rewrites and the queried object were both correct, yet the executed SQL carried a _thumbnail_id join we had never asked for.
It came from the parent theme, which hides portfolio items without a featured image via pre_get_posts, deciding with $query->is_tax( $portfolio_taxonomies ). This site has no portfolio post type, so that array is empty — and is_tax() reads an empty first argument as “not restricted to any taxonomy” and returns true unconditionally. Every custom taxonomy archive became a portfolio query, and since our thumbnails are drawn in code rather than uploaded, nothing had a featured image and every list came back empty.
The pages returned 200, the headings and filters rendered correctly, and even our empty-state message printed properly — it reads as “no posts yet” rather than as a fault. Two lessons: never treat an empty array as equivalent to an empty string, and when a list looks wrong, inspect the query that actually ran rather than the arguments you meant to set.
Query debugging tools and the caveats around running them live in the development workflow archive, and a full sweep including issues of this kind is part of our optimization program.
Next part
Next comes the_content. The same filter hands you a completely different string at priority 1 than at priority 12, and most cases of “my regex never matches anything” start there.