Why does pagination always break when used on any form of a home page?

Why does WP pagination break when used on the homepage?

If you take the code below and use it in a page template it works perfectly (make sure you have 3 or more posts). However, as soon as you use the same code on home.php, front-page.php, index.php or even as a page template but set as a static homepage, it breaks.

The URL shows /page/2/ but you get a 404 page. If you change the URL to /?page=2 it works.

I have seen so many questions related to this all over the place, yet none have a solution that works.

For the sake of simplicity I have simplified the loop below and I’m using the default WP next and previous posts links. I don’t wish to use a plugin such as WP-PageNavi or similar.

<?php get_header(); ?>

    <?php

        // taken from https://codex.wordpress.org/Pagination
        if ( get_query_var('paged') ) { 
            $paged = get_query_var('paged'); 
        } 
        else if ( get_query_var('page') ) {
            $paged = get_query_var('page'); 
        } 
        else {
            $paged = 1;
        }

        $wp_query = new WP_Query( array( 
            'posts_per_page' => 2,
            'paged' => $paged
        ));

    ?>

    <?php if ( $wp_query->have_posts() ) : while ( $wp_query->have_posts() ) : $wp_query->the_post(); ?>

         <?php the_title(); ?>

    <?php endwhile; endif; ?>

    <?php previous_posts_link(); ?>
    <?php next_posts_link(); ?>

    <?php wp_reset_query(); ?>

<?php get_footer(); ?>

Solutions Collecting From Web of "Why does pagination always break when used on any form of a home page?"

The solution is to not alter the main query in the template. The default query happens before the template is loaded, so querying in the template overwrites that original query, which is a waste of resources. See the example in codex under pre_get_posts for the correct way to alter the default query without pagination issues.

Also from codex.wordpress.com/Pagination (Under “Advanced Troubleshooting Steps” > “Removing query_posts from the main loop“) regarding modifying the main query via the pre_get_posts action mentioned by Milo:

[…] add the query for your home and category pages back in your theme’s functions.php file:

function my_post_queries( $query ) {
     // do not alter the query on wp-admin pages and only alter it if it's the main query
    if (!is_admin() && $query->is_main_query()){
        // alter the query for the home and category pages 

        if(is_home()){
           $query->set('posts_per_page', 3);
        }

        if(is_category()){
            $query->set('posts_per_page', 3);
        }
    }
}
add_action( 'pre_get_posts', 'my_post_queries' );

So your implementation might look something along the lines of

function modify_query( $query ) {
    if( !$query->is_main_query() )
        return;  //Only wish to modify the main query

    //Modify the query vars for the home and front pages
    if( is_home() || is_front_page() ) {
        $paged = get_query_var('page');

        //If the 'page' query var isn't set, or didn't return an integer, default to 1
        $paged = !isset( $paged ) || !is_int( $paged ) ? $paged : 1;

        $query->set( 'paged', $paged );
        $query->set( 'posts_per_page', 2 );
    }
}
add_action( 'pre_get_posts', 'modify_query' );