Multiple loops on page only show taxonomy name of first loop

As I have stated, you can do all of the above in your question with only one shortcode. The idea here would be to

  • Use usort() to sort posts according to term name

  • Pass a string of term names in the order you need to display them to the shortcode

FEW IMPORTANT NOTES

  • Never ever use extract(). It is unreliable and extremely hard to debug when it fails. Because of this, the use of extract() was removed from core and the codex. You can read more about this in the following trac ticket: trac ticket #22400

  • There are a quite a few bugs in your code, most of them are now fixed. There are however two left in the following line of code

    echo '<a href="' . get_permalink( $thumbnail->ID ) . '" title="' . the_title_attribute( 'echo=0' ) . '" class="book-holder" ><span class="book-thumb">';
    

    $thumbnail is undefined, I’m not sure what this line actually should look like, so you will need to sort this out yourself. For debugging purposes, I personally use a plugin called Debug Objects (to which I don”t have any affiliation to) on my local test install for debugging purposes. This plugin catches all bugs and display it to screen.

  • I have commented the code so it will be easy to understand what I did. On a couple of places I have left notes where I’m not very sure of what the actual values should be. be sure to check those out

  • I have added an attribute called term_order. If this attribute is left empty, all terms are displayed in alphabetical order. You can also specify a particular order by term name (I have use term names here as it seems like your client will pass term names and not slugs or ids. If you are going to pass ids or slugs, just modify the code accordingly). This attribute takes a comma deliminated string of term names in the order you wish to display them. For example:

    term_order="cityguides, travelguides, languageguides"
    

    That will display posts from these three terms only, in the order of cityguides, travelguides, languageguides. The term names will be displayed only before the first post in that particular term

  • There is bug in usort() for many many years now, and still it is not fixed. This is the bug

    usort(): Array was modified by the user comparison function

    Up till now, like I said, this bug is still not fixed. The best solution is to turn off error reporting for this issue by using the @ sign before usort(). Please note, you should never use this method to suppress errors and bug, all errors and bugs must be fixed. Because this is a php core error that seems like it will never be fixed, it is recommended to use this method till the bug is fixed. See bug details here

  • This shortcode is only reliable if a post belongs to one term. If there are more than one term attached to a post, the term name that is displayed on top of the posts can have unexpected effects. For multi term posts, it might be better then to use separate shortcodes or extent my code to handle that

  • I have build in a system to remove whitespaces before and after the comma when passing term names to the term_order attribute. So you do not need to worry about whitespaces and failing the shortcode because of that

  • Important note, a post should have at least one term attached to it. Posts without terms will lead to bugs and shortcode failure. I have not build in any precaution for this due to time restraints due to personal issues that arised this week. You can extent the shortcode to adapt for such situations when a post does not have any terms

  • Modify an use the code as needed

  • This code need PHP 5.4+ due the use of the short array syntax ([]) in favor of the old syntax (array()). I have also used array dereferencing (get_the_terms( $post->ID, $taxonomy )[0]->name) which is also only available in PHP 5.4+

HERE IS THE CODE

// create shortcode with parameters so that the user can define what's queried - default is to list all blog posts
add_shortcode( 'books', 'books_shortcode' );

function books_shortcode( $atts ) {
    ob_start();
    // Do not use extract(), use this syntax for your attributes
    $attributes = shortcode_atts( 
        [
            'term_order' => '', // New attribute to sort your terms in a custom order, pass a comma deliminated spaced string here. Default is term name
            'taxonomy'   => 'booktypes',
            'type'       => 'books',
            'order'      => 'ASC',
            'orderby'    => 'post_title',
            'posts'      => -1,
        ], 
        $atts 
    );

    $taxonomy = filter_var( $attributes['taxonomy'], FILTER_SANITIZE_STRING );
    // Check if our taxonomy is valid to avoid errors and bugs. If invalid, return false
    if ( !taxonomy_exists( $taxonomy ) )
        return false;

    //Set the variables for sorting purposes    
    $orderby = filter_var( $attributes['orderby'], FILTER_SANITIZE_STRING );
    $order   = filter_var( $attributes['order'], FILTER_SANITIZE_STRING );

    // Convert the string of term names to an array
    $tax_query = [];
    if ( $attributes['term_order'] ) {
        // First we need to remove the whitespaces before and after the commas
        $no_whitespaces = preg_replace( '/\s*,\s*/', ',', filter_var( $attributes['term_order'], FILTER_SANITIZE_STRING ) );
        $terms_array = explode( ',', $no_whitespaces );

        /*
         * As we are using term names, and due to a bug in tax_query, we cannot pass term names to a tax_query
         * We are going to use get_term_by to get the term ids
         */
        foreach ( $terms_array as $term ) {
            $term_ids_array[] = get_term_by( 'name', $term, $taxonomy )->term_id;
        }

        // Build a tax_query to get posts from the passed term names in $attributes['term_order']
        $tax_query = [
            [
                'taxonomy'         => $taxonomy,
                'terms'            => $term_ids_array,
                'include_children' => false
            ],
        ];

    }

    // Pass your attributes as query arguments, remember to filter and sanitize user input
    $options = [
        'post_type'         => filter_var( $attributes['type'], FILTER_SANITIZE_STRING ),
        'posts_per_page'    => filter_var( $attributes['posts'], FILTER_VALIDATE_INT ),
        'tax_query'         => $tax_query
    ];
    $query = new WP_Query( $options );

    /**
     * We need to sort the loop now before we run it. If the custom attribute, term_order is an empty array or not a valid array,
     * we will sort by term name, ascending. If a valid array of term names is passed, we will sort by the order given
     *
     * There is a bug in usort causing the following error:
     * usort(): Array was modified by the user comparison function
     * @see https://bugs.php.net/bug.php?id=50688
     * This bug has yet to be fixed, when, no one knows. The only workaround is to suppress the error reporting
     * by using the @ sign before usort
     */
    @usort( $query->posts, function ( $a, $b ) use ( $taxonomy, $terms_array, $order, $orderby )
    {
        // We will use names, so the array should be names as well. Change according to needs
        $array_a = get_the_terms( $a->ID, $taxonomy )[0]->name;
        $array_b = get_the_terms( $b->ID, $taxonomy )[0]->name;

        // If the post terms are the same, orderby the value of $attributes['orderby']
        if ( $array_a != $array_b ) {   
            // If $terms_array is empty, sort by name default
            if ( !$terms_array )
                return strcasecmp( $array_a, $array_b );

            $array_flip = array_flip( $terms_array );
            return $array_flip[$array_a] - $array_flip[$array_b];
        } else {
            $orderby_param = ['ID', 'post_date', 'post_date_gmt', 'post_parent', 'post_modified', 'post_modified_gmt', 'comment_count', 'menu_order'];
            if ( in_array( $orderby, $orderby_param ) ) {
                if ( $order == 'ASC' ) {
                    return $a->$orderby - $b->$orderby; 
                } else {
                    return $b->$orderby - $a->$orderby; 
                }
            } else { 
                if ( $order == 'ASC' ) {
                    return strcasecmp( $a->$orderby, $b->$orderby );    
                } else {
                    return strcasecmp( $b->$orderby, $a->$orderby );    
                }
            }
        }
    });

    if ( $query->have_posts() ) {
        //Will hold the term name of the previous post for comparison
        $term_name_string = '';
        // Start a counter in order to set our 'section_inner clearfix' and 'section_inner_margin clearfix' divs correctly
        $counter = 0;

            while ( $query->have_posts() ) { 
                $query->the_post(); 

                // Display the term name
                global $post;
                $terms_array = get_the_terms( $post->ID, $taxonomy );
                $term_name = $terms_array[0]->name;

                /**
                 * If this $term_name_string is not $term_name, we have a lot to do like display term name,
                 * and opening and closing divs
                 */
                if ( $term_name_string != $term_name ) { 
                    // Reset our counter back to 0 to set our 'section_inner clearfix' and 'section_inner_margin clearfix' divs correctly
                    $counter = 0;
                    /**
                     * We need to close our 'section_inner clearfix' and 'section_inner_margin clearfix' divs
                     * We also need to close our previous term div if term name changes 
                     * Open our div to wrap each term in a block. We will do this in same way as our term names
                     * Also open our 'section_inner clearfix' and 'section_inner_margin clearfix' divs
                     */
                    if ( $query->current_post != 0 ) {  
                        echo '</div></div> <!--/.wrap-->'."\n"; 
                    echo '</div><!-- Close div on last post in term -->';
                    }

                    // Use the term slug here
                    echo '<div class="books-container ' . $terms_array[0]->slug . '">';

                        ?>

                            <h2 id="<?php echo $taxonomy /*I don't know what this should be, so just correct my change */ ?>" style="margin:0px 0px 20px 0px; ">
                                <?php
                                    echo $term_name;
                                ?>
                            </h2>
                        <?php 

                        echo "\n".'<div class="section_inner clearfix"><div class="section_inner_margin clearfix">'."\n"; 

                } // end $term_name_string != $term_name if condition

                /**
                 * Close our 'section_inner clearfix' and 'section_inner_margin clearfix' divs
                 * Open our 'section_inner clearfix' and 'section_inner_margin clearfix' divs on every third post
                 */
                if( $counter != 0 && $counter%3 == 0 ) {
                        echo '</div></div> <!--/.wrap-->'."\n";
                    echo "\n".'<div class="section_inner clearfix"><div class="section_inner_margin clearfix">'."\n"; 
                }

                // Set the current term name to $term_name_string for comparison
                $term_name_string = $term_name; 

                ?>

                    <div <?php post_class('vc_span4 wpb_column column_container'); ?> style="padding-bottom: 60px;">
                        <div class="wpb_wrapper">

                            <?php // Get the post thumbnail ?>
                            <?php if ( has_post_thumbnail() ) { ?>
                                <div class="wpb_single_image wpb_content_element element_from_fade element_from_fade_on">
                                    <div class="wpb_wrapper">
                                        <?php 
                                            //$large_image_url = wp_get_attachment_image_src( get_post_thumbnail_id( $post->ID ), 'Book-thumbnail' );
                                            echo '<a href="' . get_permalink() . '" title="' . the_title_attribute( 'echo=0' ) . '" class="book-holder" ><span class="book-thumb">';
                                            echo get_the_post_thumbnail( $post->ID, 'book-thumbnail' ); 
                                            echo '</span></a>';
                                        ?>
                                    </div> 
                                </div>
                            <?php } ?>

                        </div>

                        <div class="wpb_text_column wpb_content_element text-align-center" style="margin: 20px 0px 0px 0px; ">

                            <div class="wpb_wrapper">
                                <h5><?php the_title(); ?></h5>
                            </div>
                            <a href="<?php the_permalink(); ?>" target="_blank" class="qbutton  small" style="margin: 20px 0px 0px 0px; "><?php _e('More Info', 'qode'); ?></a>

                        </div>

                    </div>

                <?php 
                if ( ( $query->current_post + 1 ) == $query->post_count ) { 
                    echo '</div></div> <!--/.wrap-->'."\n"; 
                echo '</div><!-- Close div on last post -->';
                }

                $counter++;
            }
            wp_reset_postdata(); 

        return ob_get_clean();
    }
}

Here is modified version of my code if you are going to use term slugs in stead of term names

// create shortcode with parameters so that the user can define what's queried - default is to list all blog posts
add_shortcode( 'books', 'books_shortcode' );

function books_shortcode( $atts ) {
    ob_start();
    // Do not use extract(), use this syntax for your attributes
    $attributes = shortcode_atts( 
        [
            'term_order' => '', // New attribute to sort your terms in a custom order, pass a comma deliminated spaced string here. Default is term slug
            'taxonomy'   => 'booktypes',
            'type'       => 'books',
            'order'      => 'ASC',
            'orderby'    => 'title',
            'posts'      => -1,
        ], 
        $atts 
    );

    $taxonomy = filter_var( $attributes['taxonomy'], FILTER_SANITIZE_STRING );
    // Check if our taxonomy is valid to avoid errors and bugs. If invalid, return false
    if ( !taxonomy_exists( $taxonomy ) )
        return false;

    //Set the variables for sorting purposes    
    $orderby = filter_var( $attributes['orderby'], FILTER_SANITIZE_STRING );
    $order   = filter_var( $attributes['order'], FILTER_SANITIZE_STRING );

    // Convert the string of term slugs to an array
    $tax_query = [];
    if ( $attributes['term_order'] ) {
        // First we need to remove the whitespaces before and after the commas
        $no_whitespaces = preg_replace( '/\s*,\s*/', ',', filter_var( $attributes['term_order'], FILTER_SANITIZE_STRING ) );
        $terms_array = explode( ',', $no_whitespaces );

        // Build a tax_query to get posts from the passed term slugs in $attributes['term_order']
        $tax_query = [
            [
                'taxonomy'         => $taxonomy,
                'field'            => 'slug',
                'terms'            => $terms_array,
                'include_children' => false
            ],
        ];

    }

    // Pass your attributes as query arguments, remember to filter and sanitize user input
    $options = [
        'post_type'         => filter_var( $attributes['type'], FILTER_SANITIZE_STRING ),
        'posts_per_page'    => filter_var( $attributes['posts'], FILTER_VALIDATE_INT ),
        'tax_query'         => $tax_query
    ];
    $query = new WP_Query( $options );

    /**
     * We need to sort the loop now before we run it. If the custom attribute, term_order is an empty array or not a valid array,
     * we will sort by term name, ascending. If a valid array of term names is passed, we will sort by the order given
     *
     * There is a bug in usort causing the following error:
     * usort(): Array was modified by the user comparison function
     * @see https://bugs.php.net/bug.php?id=50688
     * This bug has yet to be fixed, when, no one knows. The only workaround is to suppress the error reporting
     * by using the @ sign before usort
     */
    @usort( $query->posts, function ( $a, $b ) use ( $taxonomy, $terms_array, $order, $orderby )
    {
        // We will use names, so the array should be names as well. Change according to needs
        $array_a = get_the_terms( $a->ID, $taxonomy )[0]->name;
        $array_b = get_the_terms( $b->ID, $taxonomy )[0]->name;

        // If the post terms are the same, orderby the value of $attributes['orderby']
        if ( $array_a != $array_b ) {   
            // If $terms_array is empty, sort by name default
            if ( !$terms_array )
                return strcasecmp( $array_a, $array_b );

            $array_flip = array_flip( $terms_array );
            return $array_flip[$array_a] - $array_flip[$array_b];
        } else {
            $orderby_param = ['ID', 'post_date', 'post_date_gmt', 'post_parent', 'post_modified', 'post_modified_gmt', 'comment_count', 'menu_order'];
            if ( in_array( $orderby, $orderby_param ) ) {
                if ( $order == 'ASC' ) {
                    return $a->$orderby - $b->$orderby; 
                } else {
                    return $b->$orderby - $a->$orderby; 
                }
            } else { 
                if ( $order == 'ASC' ) {
                    return strcasecmp( $a->$orderby, $b->$orderby );    
                } else {
                    return strcasecmp( $b->$orderby, $a->$orderby );    
                }
            }
        }
    });

    if ( $query->have_posts() ) {
        //Will hold the term name of the previous post for comparison
        $term_name_string = '';
        // Start a counter in order to set our 'section_inner clearfix' and 'section_inner_margin clearfix' divs correctly
        $counter = 0;

            while ( $query->have_posts() ) { 
                $query->the_post(); 

                // Display the term name
                global $post;
                $terms_array = get_the_terms( $post->ID, $taxonomy );
                $term_name = $terms_array[0]->name;

                /**
                 * If this $term_name_string is not $term_name, we have a lot to do like display term name,
                 * and opening and closing divs
                 */
                if ( $term_name_string != $term_name ) { 
                    // Reset our counter back to 0 to set our 'section_inner clearfix' and 'section_inner_margin clearfix' divs correctly
                    $counter = 0;
                    /**
                     * We need to close our 'section_inner clearfix' and 'section_inner_margin clearfix' divs
                     * We also need to close our previous term div if term name changes 
                     * Open our div to wrap each term in a block. We will do this in same way as our term names
                     * Also open our 'section_inner clearfix' and 'section_inner_margin clearfix' divs
                     */
                    if ( $query->current_post != 0 ) {  
                        echo '</div></div> <!--/.wrap-->'."\n"; 
                    echo '</div><!-- Close div on last post in term -->';
                    }

                    // Use the term slug here
                    echo '<div class="books-container ' . $terms_array[0]->slug . '">';

                        ?>

                            <h2 id="<?php echo $taxonomy /*I don't know what this should be, so just correct my change */ ?>" style="margin:0px 0px 20px 0px; ">
                                <?php
                                    echo $term_name;
                                ?>
                            </h2>
                        <?php 

                        echo "\n".'<div class="section_inner clearfix"><div class="section_inner_margin clearfix">'."\n"; 

                } // end $term_name_string != $term_name if condition

                /**
                 * Close our 'section_inner clearfix' and 'section_inner_margin clearfix' divs
                 * Open our 'section_inner clearfix' and 'section_inner_margin clearfix' divs on every third post
                 */
                if( $counter != 0 && $counter%3 == 0 ) {
                        echo '</div></div> <!--/.wrap-->'."\n";
                    echo "\n".'<div class="section_inner clearfix"><div class="section_inner_margin clearfix">'."\n"; 
                }

                // Set the current term name to $term_name_string for comparison
                $term_name_string = $term_name; 

                ?>

                    <div <?php post_class('vc_span4 wpb_column column_container'); ?> style="padding-bottom: 60px;">
                        <div class="wpb_wrapper">

                            <?php // Get the post thumbnail ?>
                            <?php if ( has_post_thumbnail() ) { ?>
                                <div class="wpb_single_image wpb_content_element element_from_fade element_from_fade_on">
                                    <div class="wpb_wrapper">
                                        <?php 
                                            //$large_image_url = wp_get_attachment_image_src( get_post_thumbnail_id( $post->ID ), 'Book-thumbnail' );
                                            echo '<a href="' . get_permalink() . '" title="' . the_title_attribute( 'echo=0' ) . '" class="book-holder" ><span class="book-thumb">';
                                            echo get_the_post_thumbnail( $post->ID, 'book-thumbnail' ); 
                                            echo '</span></a>';
                                        ?>
                                    </div> 
                                </div>
                            <?php } ?>

                        </div>

                        <div class="wpb_text_column wpb_content_element text-align-center" style="margin: 20px 0px 0px 0px; ">

                            <div class="wpb_wrapper">
                                <h5><?php the_title(); ?></h5>
                            </div>
                            <a href="<?php the_permalink(); ?>" target="_blank" class="qbutton  small" style="margin: 20px 0px 0px 0px; "><?php _e('More Info', 'qode'); ?></a>

                        </div>

                    </div>

                <?php 
                if ( ( $query->current_post + 1 ) == $query->post_count ) { 
                    echo '</div></div> <!--/.wrap-->'."\n"; 
                echo '</div><!-- Close div on last post -->';
                }

                $counter++;
            }
            wp_reset_postdata(); 

        return ob_get_clean();
    }
}

USAGE

You can now use your shortcode as follow: (I have used the default post type post and taxonomy category for testing purposes, so this explain my use of the attributes)

[books type="post" taxonomy='category' term_order="cityguides, travelguides, languageguides"]

That will display posts from the terms cityguides, travelguides, languageguides in that specific order

EDIT

From comments, there is an issue with the posts sort order within the same term as we only sort by terms. To get the sort order right, the best way to do this is

  • Remove the sort order from the query as it really does not matter

  • Adapt the code in the usort function. We need to sort by term and by the orderby attribute. to accomplish this, we need to compare the terms from two posts, and if they are the same, sort by the orderby

I have updated the code above in both code samples to reflect these changes. One big change here is the values that you pass to orderby because we will now order by post properties. You need to check the valid WP_Post properties and use them instead of the default values used with WP_Query

So, for example, if you need to sort posts by title, the value will be post_title and not title. If you want to sort by post date, the value will be post_date and not date as normal with WP_query

I hope that makes sense.

Leave a Comment