Stay in Category

The text of this article is also in a pastebin – http://pastebin.com/4fLC9FLy

In many themes, the previous/next_post_link in single.php does not stay in category. If you view a category archive, then select a single post, the prev/next link may not show a post in the same category.

If you set the ‘in_same_cat’ parameter to the previous/next_post_link calls in single.php to ‘true’, single.php will stay in category provided the posts belong only to one category. If posts belong to more than one category, the prev/next links may point to posts in one of the categories other than the one from the category archive. Changing ‘in_same_cat’ will also affect how a blog works, because you normally don’t want to stay in category when coming from a blog post list.

Solving this problem requires several steps:

  • Modify archive.php (or category.php, if it exists) to pass the ‘stayincat’ parameter in the URL of single posts.
  • Modify single.php to detect the ‘stayincat’ parameter, set a global category id for the use by following filters, and set the ‘in_same_cat’ to ‘true’ in previous/next_post_link.
  • Add filters for previous/next_post_join so that only the global category id is selected, instead of all categories for the current post.
  • Add filters for previous/next_post_link to pass the ‘stayincat’ parameter in the links to other single pages.
  • Add a replacement function for add_query_arg because it does not work on the links in the previous/next_post_link filters.

Here is the code for archive.php:

<?php while (have_posts()) : the_post(); ?>
   <div <?php post_class() ?>>
   <?php $permalink = get_permalink();   // MAM modification to stay in category - see single.php
   if (is_category()) $permalink = add_query_arg('stayincat',get_query_var('cat'),$permalink);  ?>
   <h3 id="post-<?php the_ID(); ?>"><a href="<?php echo $permalink; ?>" rel="bookmark" title="Permanent Link to <?php the_title_attribute(); ?>"><?php the_title(); ?></a></h3>
   // Rest of Loop code here

Here is the code for single.php:

<div class="navigation">
   <?php $mam_global_stay_in_cat = ($_GET['stayincat']) ? $_GET['stayincat'] : false;
   if ( $mam_global_stay_in_cat ) { ?>
      <div><?php previous_post_link('&laquo; %link','%title',true) ?></div>
      <div><?php next_post_link('%link &raquo;','%title',true) ?></div>
   <?php } else { ?>
      <div><?php previous_post_link('&laquo; %link') ?></div>
      <div><?php next_post_link('%link &raquo;') ?></div>
   <?php } ?>
</div>

And here are the filters and functions. They can go in functions.php, or at the top of single.php:

// MAM - modification to stay in the category set in archive.php
function mam_get_previous_post_join_filter ( $join, $in_same_cat = false, $excluded_categories = '' ) {
   global $mam_global_stay_in_cat;
   if ($mam_global_stay_in_cat) {
     $join = preg_replace('/tt.term_id IN \([^(]+\)/',"tt.term_id IN ($mam_global_stay_in_cat)",$join);
   }
   return $join;
}
function mam_get_next_post_join_filter ( $join, $in_same_cat = false, $excluded_categories = '' ) {
   global $mam_global_stay_in_cat;
   if ($mam_global_stay_in_cat) {
     $join = preg_replace('/tt.term_id IN \([^(]+\)/',"tt.term_id IN ($mam_global_stay_in_cat)",$join);
   }
   return $join;
}
function mam_previous_post_link_filter ($link='') {
  global $mam_global_stay_in_cat;
  //echo 'PREV LINK BEFORE:' . htmlspecialchars($link) . '';

  if ($mam_global_stay_in_cat && $link) {
    $link = mam_add_query_arg('stayincat',$mam_global_stay_in_cat,$link);
    //echo 'PREV LINK AFTER:' . htmlspecialchars($link) . '';
  }
  return $link;
}
function mam_next_post_link_filter ($link='') {
  global $mam_global_stay_in_cat;
  //echo 'NEXT LINK BEFORE:' . htmlspecialchars($link) . '';
  if ($mam_global_stay_in_cat && $link) {
    $link = mam_add_query_arg('stayincat',$mam_global_stay_in_cat,$link);
    //echo 'NEXT LINK AFTER:' . htmlspecialchars($link) . '';
  }
  return $link;
}
 function mam_add_query_arg ($key,$value,$link) {
   // Adds the parameter $key=$value to $link, or replaces it if already there.
   // Necessary because add_query_arg fails on previous/next_post_link.
   if (strpos($link,'href')) {
     $hrefpat = '/(href *= *([\"\']?)([^\"\' ]+)\2)/';
   } else {
     $hrefpat = '/(([\"\']?)(http([^\"\' ]+))\2)/';
   }
   if (preg_match($hrefpat,$link,$matches)) {
      $url = $matches[3];
      $newurl = add_query_arg($key,$value,$url);
      // echo 'OLDURL:' . htmlspecialchars($url) . '';
      // echo 'NEWURL:' . htmlspecialchars($newurl) . '';
      $link = str_replace($url,$newurl,$link);
   }

   return $link;
}

add_filter('get_previous_post_join','mam_get_previous_post_join_filter');
add_filter('get_next_post_join','mam_get_next_post_join_filter');
add_filter('previous_post_link','mam_previous_post_link_filter');
add_filter('next_post_link','mam_next_post_link_filter');

30 Responses to Stay in Category

  • Tom says:

    I need something like that for custom taxonomy. How can I get this to work for CT? Some posts belong to 2 terms. I’m searching like crazy for a solution.

    • Mac McDonald says:

      I am not sure what it would take to have this work with a Custom Taxonomy.

      One easy question to answer: Will single.php stay in a single taxonomy term if the ‘stay in category’ parameter to next/previous_post_link() is set to true?

      Give that a try. If it fails, then the approach shown here will never work. If it works, then this code may work without any changes.

  • fjpoblam says:

    This looks like exactly what I need (or most of it), and I truly thank you. Here’s the hitch. I have a widget that offers a category selection list. I’m trying to figure out how to let the visitor select a category and then loop through posts in *the selected* category. I suspect this may be the answer, once I put my pea brain to it.

    My blog is so simple-minded, I’ve written a template consisting of *just* an index.php and an accompanying functions.php – needed to accommodate the category-select widget and your stuff. The category-select widget yields a call to blog.com/?cat=[category] so, maybe that’s all I need maybe not. We’ll see.

    • Mac McDonald says:

      I think you will need at least a category.php script to make it work. The code needs to pass a URL parameter (stayincat) along to the next request. If you have only one php script, it will not know when to stop passing the parameter.

      You may also need a single.php script for much the same reason.

  • Mac McDonald says:

    I have had a couple of comments asking if this code is compatible with WP 3.4.2. I am using it in many sites running 3.4.2 with no problems. I believe that it is completely compatible with 3.4.2.

  • Vuchko says:

    Great work man! Thanks a lot!

  • Peter says:

    Thanks for this solution! I have just a question. This solutions works fine when you go the post from an archive. But when you enter the post directly or you came from home() no category_id is set. is there a solution to set a fixed category to use with the nex / previous links when someone enters the direct post?

    • Mac McDonald says:

      If your Posts belong to only one category, you can modify the ‘in_same_cat’ parameter as described at the start of this article:

      If you set the ‘in_same_cat’ parameter to the previous/next_post_link calls in single.php to ‘true’, single.php will stay in category provided the posts belong only to one category.

      If the Posts belong to more than one category, you could modify index.php (or loop.php, depending on your theme) to pass the ‘stayincat’ parameter in the links, but I’m not sure where you would store the category id to pass as the value of the parameter.

  • Jason says:

    Sorry, I was trying to paste the bit of PHP I used to no avail. If you want to let me know how you pasted code in your comments, I’ll try again. Either way, I figured it out, and this little piece of code you wrote up is really slick. Kudos, and thanks a bunch.

  • Jason says:

    I am passing the stayincat parameter in the URL of a single post, via a page of posts template (not from the archive template). The parameter shows up in the URL, but the mam_global_stay_in_cat function still returns false on the single post page. Can you help with this?

    • Mac says:

      I need a link to your site and to see your code. Please put the code in a pastebin and post a link to it here along with a link to your site..

      • Jason says:

        I just figured it out. In the Page of Posts template, I was using: <a href="">

        ..in order to add the stayincat parameter to the link. All I had to do was change the_permalink() to get_permalink().

      • Jason says:

        I just figured it out. In the Page of Posts template, I was using: ">

        ..in order to add the stayincat parameter to the link. All I had to do was change the_permalink() to get_permalink().

      • Jason says:

        Okay I’ll try again:

        <a href="<?php $permalink = add_query_arg('stayincat',get_query_var('cat'),the_permalink()); echo $permalink; ?>"><?php the_title(); ?></a>

        I changed the_permalink() to get_permalink().

  • strony www says:

    Excellent! It’s working now :) But how may it work for all child categories?

    • Mac says:

      I don’t understand the question. Please explain more.

      And, please use the Reply link inside this comment so all these related comments are connected.

  • strony www says:

    If I click in the menu child category, I get list of all posts from this category, when I click one of them, the function works fine. If I click in the menu, parent category, I get posts from all child categories. Than “stay in category” uses parent category and I can’t even see previous next links…
    I’d like to but I don’t know how you want me to use in_category() …

    • Mac says:

      I don’t have a way to test this, so it may not work as I think it will. Replace the $cats array with a list of your own child categories:

      The important part is the if (is_category()) test.

      <?php while (have_posts()) : the_post(); ?>
         < div <?php post_class() ?>>
         <?php $permalink = get_permalink();   // MAM modification to stay in category - see single.php
         if (is_category()) :
            $this_cat = get_query_var('cat');
            $cats = array(22,65,14,5);
            foreach ($cats as $cat) :
               if (in_category($cat)) :
                  $this_cat = $cat;
                  break;
               endif;
            endforeach;
            $permalink = add_query_arg('stayincat',$this_cat,$permalink);
         endif; ?>
         <h3 id="post-<?php the_ID(); ?>"><a href="<?php echo $permalink; ? rel="nofollow">" rel="bookmark" title="Permanent Link to <?php the_title_attribute(); ?>"><?php the_title(); ?></a></h3>
         // Rest of Loop code here
      <?php endwhile; ?>
      
  • strony www says:

    It’s really working on this site. But the problem is that on my site I can click “Good Eats” for example and then I can see all posts from all child categories that are in “Good Eats”: “All Americans”, “Barbeque”, etc. And when I than click one of the posts, let’s say from “Barbeque”, “stay in category” stops working… because it uses “Good Eats” category…

    • Mac says:

      I think I understand what you are saying: It always stays in the category from the archive page but you want it to switch to the child category of a post on the page.

      I can’t think of a good generalized way to do that at the moment. Maybe something will come to mind later.

      For a specific case, could you use an in_category() test in the archive.php loop to see if the current post belongs to one of the child categories and then use that value for the stayincat parameter?

  • strony www says:

    Working great. Thanks.
    But there’s a problem when I have parent and child category. Is it possible to work only for child category?

    • Mac says:

      Not sure what the problem is. In this site, under Good Stuff, the category Good Eats has several child categories and the plugin works to stay in a child category.

      For example, go to Good Stuff->Good Eats->Barbeque and click on the title of any article. The single.php display previous/next links only to other Barbeque posts.

  • lukasz says:

    your code both here and pastebin is getting messed up somewhere, your “&&” are being replaced in pastebin and if you copy from website it causes other errors.

    • Mac says:

      Thanks for pointing that out. Somehow the copy/paste operation messed up the code. I believe that it has been corrected now.

  • Matt Edwards says:

    This could be just was I’m looking for.
    Will this work with links anywhere??

    I have a list of products but some are in two categories. Ie necklaces & featured. A product page shows other products from the same category. But if u select one that’s in both. It may change categories. Hopefully this will sort it.

    • Mac says:

      Not quite sure what you mean by ‘links anywhere’, but the approach should work if you are able to edit the URL of the link to add the parameter.

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>