Grouping custom WordPress posts by taxonomy
I have a custom post type called "products" that has two custom taxonomies - "product range" and "product categories". A range serves as a top-level grouping, while a category is a sub-group within that.
I have installed the taxonomy-product-range.php template which has the following code:
<?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 displaying products and grouping them by product category. However, it will list all products, no matter which archive you are viewing. I only need the output of messages for the archive you are viewing.
It looks like this, but I'm not sure how to fix it.
== Edit ==
Each of the products will belong to the same "range" and "category". When people visit the product level archive page, I try 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>
source to share
Just remove the code you have and replace it with the default loop. You shouldn't replace the main query with a custom one. Use pre_get_posts
to modify the main query to suit your needs.
This is what your taxonomy page looks like
if ( have_posts() ) {
while ( have_posts() ) {
the_post();
// Your template tags and markup
}
}
As we sort your problem, we'll go over this with a filter usort
and thee the_posts
to do the sort before the loop starts, but right after the main query starts. We will not use multiple loops as they are quite expensive and resource intensive, and this breaks the functionality of the page.
I have commented out the code so it is easy to understand 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
This is how your loop should look like to display your page in the order you 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 above code is now tested and working. As requested, this is a test run on my local installation. For this test, I used the code in the OP and my code.
results
(These results were obtained using the Query Monitor Plugin . Additionally, all results include the same additional queries that do widgets, navigation menus, custom functions, etc.)
-
Code in OP -> 318 db requests at 0.7940s with page generation time of 1.1670s. Memory usage was 12.8Mb
-
My code is in response -> 46 db requests at 0.1045s with page generation time of 0.1305s. Memory usage was 12.6Mb
As I said earlier, the proof is in the pudding
source to share