Grouping WordPress custom posts by taxonomy

I’ve got a custom post type called “products”, which has two custom taxonomies – “product-range” and “product-categories”. The range serves as a top-level grouping, whereas the category is a sub-grouping within that.

I’ve set-up a taxonomy-product-range.php template which features the following code:

Read More
<?php
$terms = get_terms('product-categories');
foreach( $terms as $term ):
?>     

<h2><?php echo $term->name;?></h2>
<ul>

    <?php                         
    $posts = get_posts(array(
        'post_type' => 'products',
        'taxonomy' => $term->taxonomy,
        'term' => $term->slug,
        'nopaging' => true
    ));
    foreach($posts as $post): setup_postdata($post);
    ?>

    <li><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></li>

    <?php endforeach; ?>

</ul>           

<?php endforeach; ?>

This works as expected by outputting the products and grouping them by product category. However, it outputs all of the products, regardless of which archive you’re viewing. I need it to only output posts for the archive you’re viewing.

This feels like it’s nearly there, but I’m not sure how to fix it.

== Edit ==

Each of the products will belong to one “Range” and one “Category”. When people visit the product-range archive page, I’m trying to display the following:

<h1>Range Title</h1>
<h2>Category 1 Title</h2>
<ul>
<li>Product 1 Title</li>
<li>Product 2 Title</li>
<li>Product 3 Title</li>
</ul>
<h2>Category 2 Title</h2>
<ul>
<li>Product 4 Title</li>
<li>Product 5 Title</li>
<li>Product 6 Title</li>
</ul>

Related posts

1 comment

  1. Simple remove the code that you have and replace it with the default loop. You should not replace the main query with a custom one. Use pre_get_posts to alter the main query according to needs.

    This is what your taxonomy page should look like

    if ( have_posts() ) {
        while ( have_posts() ) {
        the_post();
    
            // Your template tags and markup
    
        }
    }
    

    As your problem is sorting, we will tackle this using usort and thee the_posts filter to do the sorting before the loop runs but just after the main query has run. We will not use multiple loops as they are quite expensive and resource intensive, and it breaks page functionalities

    I have commented the code so it can be easy to follow and understand. (NOTE: The following code is untested and requires PHP 5.4+ due to array dereferencing)

    add_filter( 'the_posts', function ( $posts, $q ) 
    {
        $taxonomy_page = 'product-range';
        $taxonomy_sort_by = 'product-categories'; 
    
        if (    $q->is_main_query() // Target only the main query
             && $q->is_tax( $taxonomy_page ) // Only target the product-range taxonomy term pages
        ) {
            /**
             * There is a bug in usort that will most probably never get fixed. In some instances
             * the following PHP warning is displayed 
    
             * usort(): Array was modified by the user comparison function
             * @see https://bugs.php.net/bug.php?id=50688
    
             * The only workaround is to suppress the error reporting
             * by using the @ sign before usort
             */      
            @usort( $posts, function ( $a, $b ) use ( $taxonomy_sort_by )
            {
                // Use term name for sorting
                $array_a = get_the_terms( $a->ID, $taxonomy_sort_by );
                $array_b = get_the_terms( $b->ID, $taxonomy_sort_by );
    
                // Add protection if posts don't have any terms, add them last in queue
                if ( empty( $array_a ) || is_wp_error( $array_a ) ) {
                    $array_a = 'zzz'; // Make sure to add posts without terms last
                } else {
                    $array_a = $array_a[0]->name;
                }
    
                // Add protection if posts don't have any terms, add them last in queue
                if ( empty( $array_b ) || is_wp_error( $array_b ) ) {
                    $array_b = 'zzz'; // Make sure to add posts without terms last
                } else {
                    $array_b = $array_b[0]->name;
                }
    
                /**
                 * Sort by term name, if term name is the same sort by post date
                 * You can adjust this to sort by post title or any other WP_Post property_exists
                 */
                if ( $array_a != $array_b ) { 
                    // Choose the one sorting order that fits your needs
                    return strcasecmp( $array_a, $array_b ); // Sort term alphabetical ASC 
                    //return strcasecmp( $array_b, $array_a ); // Sort term alphabetical DESC
                } else {
                    return $a->post_date < $b->post_date; // Not sure about the comparitor, also try >
                }
            });
        }
        return $posts;
    }, 10, 2 ); 
    

    EDIT

    Here is how your loop should look like to display your page in the order in your edit

    if ( have_posts() ) {
        // Display the range term title
        echo '<h1>' . get_queried_object()->name . '</h1>';
    
        // Define the variable which will hold the term name
        $term_name_test = '';
    
        while ( have_posts() ) {
        the_post();
    
            global $post;
            // Get the terms attached to a post
            $terms = get_the_terms( $post->ID, 'product-categories' );
            //If we don't have terms, give it a custom name, else, use the first term name
            if ( empty( $terms ) || is_wp_error( $terms ) ) {
                $term_name = 'SOME CUSTOM NAME AS FALL BACK';
            } else { 
                $term_name = $terms[0]->name;
            }
    
            // Display term name only before the first post in the term. Test $term_name_test against $term_name
            if ( $term_name_test != $term_name ) {
                // Close our ul tags if $term_name_test != $term_name and if not the first post
                if ( $wp_query->current_post != 0 )
                    echo '</ul>';
    
                echo '<h2>' . $term_name . '</h2>';
    
                // Open a new ul tag to enclose our list
                echo '<ul>';
            } // endif $term_name_test != $term_name
    
            $term_name_test = $term_name;
    
            echo '<li>' . get_the_title() . '</li>';    
    
            // Close the ul tag on the last post        
            if ( ( $wp_query->current_post + 1 ) == $wp_query->post_count ) 
                echo '</ul>';
    
        }
    }
    

    EDIT 2

    The code above is now tested and working. On request, here are the test run on my local install. For this test I have used the code in OP and my code.

    RESULTS

    (This results was obtained with the Query Monitor Plugin. Also, all results include the same extra queries made by widgets, nav menus, custom functions etc)

    • Code in OP -> 318 db queries in 0.7940 s with page generation time of 1.1670s. Memory usage was 12.8Mb

    • My code in answer -> 46 db queries in 0.1045 s with page generation time of 0.1305s. Memory usage was 12.6Mb

    As I have stated previously, the proof is in the pudding

Comments are closed.