See edit at bottom.

I have a wp_query loop in a page template for a custom post type ("success_stories"). The loop itself works fine but I cannot seem to get pagination working. I've tried every solution I could find on these forums and everytime I try to navigate to /page/child-page/page/2/ WordPress sends me back to /page/child-page/ with a 301 redirect.

I have of course flushed the permalinks about 100 times. Here's the code I'm working with:


if ( get_query_var('paged') ) {
    $paged = get_query_var('paged');
} elseif ( get_query_var('page') ) { // 'page' is used instead of 'paged' on Static Front Page
    $paged = get_query_var('page');
} else {
    $paged = 1;
$paged = (get_query_var('paged')) ? get_query_var('paged') : 1;

// Query arguments
$args = array(
    'post_type'             => array( 'success_stories' ),
    'posts_per_page'        => '3',
    'paged'                 => $paged,
    'ignore_sticky_posts'   => true,
    'tax_query'             => array(
                'taxonomy' => 'practice_areas_taxonomy',
                'field'    => 'ID',
                'terms'    => array( 17 ),

// Instantiate custom query
$practice_area_query = new WP_Query( $args );

// Pagination fix
$temp_query = $wp_query;
$wp_query   = NULL;
$wp_query   = $practice_area_query;

// Output custom query loop
if ( $practice_area_query->have_posts() ) :

    echo '<div>';

    while ( $practice_area_query->have_posts() ) :
        $practice_area_query->the_post(); ?>

        <div class="<?php echo $pa_class_inner; ?>">

            <blockquote>&ldquo;<?php echo get_the_excerpt();?>&rdquo;</blockquote>


    <?php endwhile;

    echo '</div>';


// Pagination
$total_pages = $wp_query->max_num_pages;
if ($total_pages > 1) {

    $big = 999999999;
    $current_page = max(1, get_query_var('paged'));

    echo '<nav class="pagination clearfix">';

    echo paginate_links(
            'base' => str_replace($big, '%#%', esc_url(get_pagenum_link($big))),
            'current' => $current_page,
            'total' => $total_pages,
            'prev_text' => 'Prev',
            'next_text' => 'Next',
            'mid_size' => 1,
            'end_size' => 3

    echo '</nav>';   


// Reset main query object
$wp_query = NULL;
$wp_query = $temp_query; ?> 

Where am I going wrong? Can this not be done from a (child?) page template?

Edit: adding the following rewrite rules almost resolves the issue:

// Primary Rewrite Rule
add_rewrite_rule( 'practice-areas/family-law/?', 'index.php?post_type=success_stories', 'top' );
// Pagination Rewrite Rule
add_rewrite_rule( 'practice-areas/family-law/page/([0-9]{1,})/?', 'index.php?post_type=success_stories&paged=$matches[1]', 'top' );

The only problem then is that (a) on paginated pages I'm suddenly in a different template (archive.php) and (b) WordPress seems to default to regular wp_query parameters. That is, it's "forgotten" all my args from the above query, as well as my offset, and I think even the taxonomy query.

  • Is there a reason this is a custom page template, and not just the post type archive? Commented Jul 6, 2021 at 12:59
  • 1
    This may not answer the question, but you should use $total_pages = $practice_area_query->max_num_pages; instead of having to do those $temp_query things.
    – Sally CJ
    Commented Jul 6, 2021 at 13:09
  • 1
    if you don't have a second page on your page then it won't find anything and give a 404 or redirect to the first. Remember, WordPress operates on the main query and is unaware of your custom query, and the main query runs before the page template is loaded ( how else would it know which page and which template to load ). If your template does not reference the page then is there a reason you didn't use a main archive template then the pre_get_posts filter to modify the parameters of the query instead of creating a second query?
    – Tom J Nowell
    Commented Jul 6, 2021 at 13:16
  • 1
    Also there's an extremely high chance the answer will involve custom rewrite rules, as well as overriding the multiple page pagination rules ( a pages content can contain sub-pages separated by a marker, all within the same page
    – Tom J Nowell
    Commented Jul 6, 2021 at 13:18
  • 1
    @SallyCJ - it looks like example.com/practice-areas/family-law/success-stories/page/2/ "practice-areas" is the CPT slug. "Family-law" is the parent page slug. "Success-stories" is the child page slug. PS I think I have a solution, will post later this morning.
    – Josh M
    Commented Jul 7, 2021 at 14:50

2 Answers 2


Glad that you managed to figure out a solution, which yes, does work. But you could make it more selectively, i.e. target just the specific pages, by using is_single( '<parent slug>/<child slug>' ), e.g. is_single( 'family-law/success-stories' ) in your case.

Secondly, it's not exactly the redirect_canonical hook which caused the (301) redirect on singular paginated post pages (with URL having the /page/<number>). Instead, it's the $addl_path part in the redirect_canonical() function which fires the redirect_canonical hook. More specifically, that part runs only if ! is_single() is true (and that the current page number is 2 or more), and thus:

  • If the request (i.e. current page) URL was for a CPT at example.com/practice-areas/family-law/success-stories/page/2/ where practice-areas is the CPT slug and success-stories is child of family-law, the redirect/canonical URL would not contain page/2/ because ! is_single() is false, hence $addl_path would be empty.

    The same scenario would also happen on example.com/practice-areas/family-law/page/2/ (i.e. page 2 of the parent post) and other singular CPT pages, including regular posts (in the default post type), e.g. at example.com/hello-world/page/2/, but not on singular Pages (post type page) because ! is_single() is false — because is_single() works with any post type, except attachment and page.

  • So if the request URL does not match the canonical URL, then WordPress runs the (301) redirect, which explains why this happened: "everytime I try to navigate to /page/child-page/page/2/ WordPress sends me back to /page/child-page/ with a 301 redirect".

Also, redirect_canonical() is hooked on template_redirect, so instead of having the function runs the various logic of determining the redirect/canonical URL and yet you eventually return an empty string, you might better off just disable the function like so:

add_action( 'template_redirect', 'my_template_redirect', 1 );
function my_template_redirect() {
    if ( is_paged() && is_single( 'family-law/success-stories' ) ) {
        remove_action( 'template_redirect', 'redirect_canonical' );

And yes, no extra rewrite rules are needed because WordPress already generated them (which handle the /page/<number>) for your CPT. But I don't know for sure why WordPress does the above ! is_single() check. 🤔

Last but not least, in my original answer and comment as well, I suggested you to remove the $temp_query parts, and your reply was: "I'm gonna keep the $temp_query bit though, since it allows our pagination component to work without the need to rewrite or complicate things" and "I think you were just saying it's not needed".

So yes, you're right with that second sentence. But more precisely, I actually tested your template code where I put it in single-practice-areas.php and then removed the $temp_query parts (and used $total_pages = $practice_area_query->max_num_pages;), and the /page/<number> pagination worked just fine for me with the help of the above my_template_redirect() function.

So I wondered what exactly does the $temp_query fix, or why must you assign the $wp_query to $practice_area_query?

But if you really must keep it, then you need to move the wp_reset_postdata(); to after the $wp_query = $temp_query;, i.e. after you restore the global $wp_query variable back to the actual main query. Because otherwise, if for example you run the_title(), you would see the title of the last post in your custom query and not the current (and the only) one in the main query.

  • I like your conditional a lot better. I'll try this later today to confirm it works. Thanks.
    – Josh M
    Commented Jul 8, 2021 at 13:12
  • This works well. I'll select is the correct answer. I'm gonna keep the $temp_query bit though, since it allows our pagination component to work without the need to rewrite or complicate things. Unless I'm missing something and you're saying there's an actual disadvantage to that? But I think you were just saying it's not needed.
    – Josh M
    Commented Jul 8, 2021 at 14:00
  • Please check the last four paragraphs in my answer (I also reworded some other paragraphs).
    – Sally CJ
    Commented Jul 9, 2021 at 11:16

The problem turned out to be the redirect_canonical filter. That was causing the 301 back to the child page itself.

The solution is to selectively disable that filter. Below is what I've done, though it's not quite as selective as I'd like.

But it does allow pagination to work, without the need of any rewrite rules.

// redirect_canonical is preventing pagination on PA child pages. This removes the filter in that context
add_filter( 'redirect_canonical', 'namespace_prevent_redirect_on_pagination', 10 );
function namespace_prevent_redirect_on_pagination( $redirect_url ) {
    if ( is_singular('practice-areas') && !is_admin() && get_post()->post_parent ) {
        return '';
    return $redirect_url;

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.