4

I would like to sort posts by tags in category and archive pages. (Unfortunately there isn’t an orderby parameter for tags. That would make things so easy!)

So a category page would be like this:

Category name

  • Post 1 (Tag 1)
  • Post 2 (Tag 1)
  • Post 3 (Tag 1)
  • Post 4 (Tag 2)
  • Post 5 (Tag 2)
  • Post 6 (Tag 2)
  • Post 7 (Tag 3)
  • Post 8 (Tag 3)
  • Post 9 (Tag 3) ...

Or, even better:

Category name

Tag 1

  • Post 1
  • Post 2
  • Post 3

Tag 2

  • Post 4
  • Post 5
  • Post 6

Tag 3

  • Post 7
  • Post 8
  • Post 9 ...

The tags would appear in alphabetical order. The posts below the tags would also appear in alphabetical order.

When a post has more than one tag, it needs to appear multiple times (one for each tag).

Is there any way to do that?

I thought about doing something like this, but I couldn’t come up with a solution that would show only the posts and tags in the category. (The linked code shows all posts and tags.)

Or maybe there is a way to treat tags as a meta_value? Then I could simply use pre_get_posts like this:

add_action( 'pre_get_posts', 'archive_post_order'); 

function archive_post_order($query){
    if(is_archive()):
        $query->set( 'orderby', 'meta_value' );
        $query->set( 'metakey', 'tag' );
        $query->set( 'order', 'ASC' );
    endif;    
}

Any ideas are welcome. Thanks in advance.

8
  • Do you mean group posts by tags ?
    – bravokeyl
    Jun 4, 2016 at 7:13
  • seems like a strange thing to do... but maybe you could turn it around by setting to default tag page query that gets all posts with tags (so maybe that can be ordered by tags) and then adding a tax_query to filter back to posts in the category..? just a thought...
    – majick
    Jun 4, 2016 at 7:42
  • @bravokeyl Yes. I updated my question to make it clearer.
    – Emerson
    Jun 5, 2016 at 23:37
  • @majick I didn't understand the first part of your suggestion (setting to default tag page query that gets all posts with tags). Could you clarify it? Thanks.
    – Emerson
    Jun 5, 2016 at 23:39
  • Are you using pagination? Would you want to categorise the posts into tags that happen to appear on a page with the default order, or do you actually want all posts in Tag 1 to be returned first, Tag 2 next, etc.
    – Tim Malone
    Jun 6, 2016 at 1:05

2 Answers 2

2

There really is no sane way to accomplish this, specially if you have posts that are assigned to more than one tag (which is almost always the case). What is very sure, you have you work cut out for you.

Here are some thought and ideas you can persue:

POSTS WITH MULTIPLE TERMS

If posts have more than one tag (or any term for that matter) assigned to them, it means that there are more than one relationship to another post or set of posts, and grouping those together will either: (a). Be impossible without duplication, or (b). Be possible, with duplication of posts.

The terms (tags in this case) assigned to a post from a specific taxonomy (post_tag in this case) is ordered in a specific way when more than one term exists. Simply grabbing the first term might not always be the term you would want to use.

Sorting tags from get_the_tags() (or the more generic get_the_terms()) would require additional PHP sorting to sort the returned array of tag object to suite your needs, or you would want to use a function like wp_get_post_tags() to do the sorting via SQL, but it would require an extra db call.

If you want to sort by the first tag only, then it is quite easy if you sorted the tag array to suite or needs. I'll post example code later

WORKAROUND

If you have posts with multiple tags, you will need to find a specific unique relationship between posts, and according to that, either:

  • assign a custom field with a sortable value to a set of posts and then use pre_get_posts to sort your category pages according to custom field value

  • create a specific taxonomy for this, and then assign a special term to a set of posts and then use the the_posts filter, which I will post, to sort the returned array of posts

If you do not mind post duplication, you have a look at this post, this was however done for custom fields, so you would need to break it down and modify it to work with tags, but in essence, the principle by building a multidimentional array, flatten it later and then return a sorted array with duplicate posts remains the same

POSTS WITH SINGLE TAGS

If you have posts assigned to one tag only (or you are happy to sort by first tag only), it is quite easy to sort the posts accordingly by using the the_posts filter. You can try the following then (UNTESTED and very basic)

add_filter( 'the_posts', function ( $posts, \WP_Query $q )
{
    // Make sure we only target the main query on category pages
    if (    !is_admin()
         && $q->is_main_query()
         && $q->is_category()
    ) {
        // Use usort to sort the posts
        usort( $posts, function( $a, $b )
        {
            return strcasecmp( 
                get_the_tags( $a->ID )[0]->name, 
                get_the_tags( $b->ID )[0]->name 
            );
        });
    }
}, 10, 2 );
1
  • Thanks for the answer, I appreciate all the explanation! I actually want the posts with more than one tag to appear multiple times (one for each tag) — I should have mentioned it and I just updated my question to include that. I will have a look at the post you linked.
    – Emerson
    Jun 6, 2016 at 8:37
-1

If you have a lot of tags this is going to be pretty inefficient, but still, it should get the job done...

This first part is a little pointless but it will pare down to posts in the category that actually have tags.

add_action('pre_get_posts', 'get_category_tag_posts');
function get_category_tag_posts($q) {
    if ( !is_admin() && $q->is_main_query() && $q->is_category() ) {
        global $categorytags;
        $terms = get_terms('post_tag');
        foreach ($terms as $term) {$categorytags[] = $term->term_id;}
        $q->set('tag__in',$categorytags);
        // inefficient but we cannot know how many posts are needed
        $q->set('posts_per_page',-1); // also makes offset ignored
    }
}

Really just using this part to index some tags etc., in the hope of creating slightly better efficiency by not doubling up later...

add_filter('the_posts', 'get_category_post_tags', 10, 2);
function get_category_post_tags($posts, $q) {
    if ( !is_admin() && $q->is_main_query() && $q->is_category() ) {
        global $categoryposttags, $categoryposts, $posttagnames;
        $categoryposts = $posts;
        if (count($posts) > 0) {
            foreach ($posts as $post) {
                 $tagarray = array();
                 $tags = get_the_tags($post->ID);
                 if ($tags) {
                     foreach ($tags as $tag) {
                        if (!in_array($tag->term_id,$tagarray)) {$tagarray[] = $tag->term_id;}
                        $posttagnames[$tag->term_id] = $tag->name;
                     }
                     $categoryposttags[$post->ID] = $tagarray;
                 }
            }
        }
    }

And for the category template:

if (is_category()) {

global $categoryposts, $categorytags, $categoryposttags, $posttagnames;
$postsperpage = get_option('posts_per_page'); $postoffset = 0;
if (isset($_GET['posts_per_page'])) {$postsperpage = absint($_GET['posts_per_page']);}
if (isset($_GET['page'])) {$postoffset = (absint($_GET['page'])-1) * $postsperpage;}

$postcount = 0; $displaycount = 0;
foreach ($categorytags as $categorytag) {
    $theseposts = '';
    foreach ($categoryposts as $post) {
         if (in_array($categorytag,$categoryposttags[$post->ID])) {
             $postcount++;
             if ($postcount > $postoffset) {
                 $theseposts .= '<li><h4><a href="'.get_permalink($post->ID).'">'.$post->post_title.'</a></h4><br>';
                 if (empty($post->post_excerpt)) {$theseposts .= wp_kses_post(wp_trim_words($post->post_content,50));}
                 else {$theseposts .= $post->post_excerpt;}
                 $theseposts .= '<br></li>';
                 $displaycount++;
             }
         }
         if ($displaycount == $postsperpage) {break;}
    }
    if ($theseposts != '') {
         echo '<h3>'.$posttagnames[$categorytag].'</h3><br>';
         echo '<ul>'.$theseposts.'</ul><br>';
    }
    if ($displaycount == $postsperpage) {break;}
}
if ($displaycount == 0) {echo 'No posts were found.';}

}

Note I have wrapped in is_category if this is for use not on the category template (eg. archive.php)

May need to create the pagination links manually for this but it does work. :-)

5
  • Thanks for the answer! When I use the second code (function get_category_post_tags) all pages (not only category pages, but all of them, including the front page) and posts of the website return a 404 error. So there might be an error with the code (or maybe it’s my website). I’ll try to figure out what’s happening here.
    – Emerson
    Jun 6, 2016 at 23:39
  • whoops it was actually the first function I had $q->is_category there instead of $q->is_category() ... I added some checks in the second just in case but I don't seem how it would do anything it is just indexing stuff there...
    – majick
    Jun 7, 2016 at 1:41
  • Your use of so many globals are ridiculous and IMHO, is just poor design. Also, using non sanitized $_GET values are dangerous. Jun 7, 2016 at 7:38
  • yeah i was waiting for that from you, but really it's just the result a faster thought process for me in creating a working prototype of code - and there is nothing "poor" about that at all. sure it's overdone here and could be optimized, but so what? for the record i remember your opinion of globals, I just don't share it - they can create for some very efficient code if used correctly (and no I'm not claiming that is the case here)... besides, compared to the completely ridiculous amount of multiple huge global objects now in WordPress these are super-light footprint arrays.
    – majick
    Jun 7, 2016 at 9:21
  • added absint to the other $_GET variable... though I fail to see how query values used only for a simple numeric comparison for pagination display could be dangerous.
    – majick
    Jun 7, 2016 at 9:25

Your Answer

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

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