Modify the global WordPress query on default pages
pre_get_posts fires after the query variable object is created but before the database query runs. It gives you direct access to the $query object for every query WordPress makes — including the main loop — so you can change arguments without duplicating the template or creating a secondary query.
Problem: On default WordPress archive pages like archive.php, how do you change the query arguments that aren't explicitly set?
Solution: Use the pre_get_posts hook to modify the main query before it runs:
add_action( 'pre_get_posts', 'custom_query_on_archive_page' );
function custom_query_on_archive_page( $query ) {
if ( $query->is_post_type_archive( 'news' ) && $query->is_main_query() && ! is_admin() ) {
$query->set( 'posts_per_page', 10 );
} elseif ( $query->is_post_type_archive( 'events' ) && $query->is_main_query() && ! is_admin() ) {
// Exclude past events
$args = [
'post_type' => 'events',
'posts_per_page' => -1,
'fields' => 'ids',
];
$events = new WP_Query( $args );
$exclude = [];
foreach ( $events->posts as $event_id ) {
if ( strtotime( current_time( 'F j, Y' ) ) > strtotime( get_field( 'start_date', $event_id ) ) ) {
$exclude[] = $event_id;
}
}
$query->set( 'posts_per_page', 12 );
$query->set( 'orderby', 'meta_value' );
$query->set( 'order', 'ASC' );
$query->set( 'meta_key', 'start_date' );
$query->set( 'post__not_in', $exclude );
}
}
NOTE: Always include both $query->is_main_query() and ! is_admin() in the condition — without them you risk modifying admin list table queries and secondary queries on the same page. Never run a new WP_Query inside a pre_get_posts callback; it creates the same hook context and causes infinite recursion.