How to change arguments of the global query on default pages

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.