How to paginate a list of custom taxonomy terms?

Your shortcode is extremely expensive to run. get_term_link() is not user friendly when you feed it with term ID’s as it results in a db query to query the term object. If you feed the term object to get_term_link(), it is happy and do not need to do any work to get the term object.

Just quickly look at that section

function get_term_link( $term, $taxonomy = '' ) {
    global $wp_rewrite;

    if ( !is_object($term) ) {
        if ( is_int( $term ) ) {
            $term = get_term( $term, $taxonomy );
        } else {
            $term = get_term_by( 'slug', $term, $taxonomy );
        }
    }

As you can see, if $term is not a term object, but the term ID or slug, extra calls are done to db to get the term object. All of this means is, if you have 100 terms and you need to get the link to the term page, and you pass either the ID or slug to get_term_link(), you are making 100 extra db calls. Because you already have the term object, simply feed that to get_term_link() and you save 100 db calls.

As for pagination and how to make it work, you only need to know how many terms you have, divide that by how many terms per page you need, and have the maximum amount of pages there will be. You also need to calculate term offset which will be the amount of terms per page times the current page number

Just one note, you would not want to hardcode URL’s, you would want to use dynamic value. Look at wp_upload_dir() to return the path to the upload directory, so be sure to change that accordingly in your shortcode

add_shortcode( 'taxography', function ( $atts )
{
    $attributes = shortcode_atts(
        [ 
            'terms_per_page' => 12,
            'taxonomy'      => 'books',
            // Add any attribute which you seem fit
        ],
        $atts,
        'taxography'
    );

    // Sanitize and validate our inputs and set variables
    $tpp = filter_var( $attributes['terms_per_page'], FILTER_VALIDATE_INT );
    $taxonomy = filter_var( $attributes['taxonomy'], FILTER_SANITIZE_STRING );

    // Make sure our taxonomy exists to avoid unnecessary work
    if ( !taxonomy_exists( $taxonomy ) )
        return false;

    // Our taxonomy exists, lets continue
    // Get the term count to calculate pagination.
    $term_count = get_terms( $taxonomy, ['fields' => 'count'] );

    // Check if we have terms to avoid bugs
    if ( !$term_count )
         return false;

    // We have terms, now calculate pagination
    $max_num_pages = ceil( $term_count / $tpp );
    // Get current page number. Take static front pages into account as well
    if ( get_query_var( 'paged' ) {
        $paged = get_query_var( 'paged' );
    } elseif ( get_query_var( 'page' ) {
        $paged = get_query_var( 'page' );
    } else {
        $paged = 1;
    }
    // Calculate term offset
    $offset = ( ( $paged - 1 ) * $tpp );

    // We can now get our terms and paginate it
    $args = [
        'number' => $tpp, // Amount of terms to return
        'offset' => $offset // The amount to offset the list by for pagination
    ];
    // Set our variable to hold our string
    $output="";
    $wpbtags = get_terms( $taxonomy, $args );
    $output.= '<div class="grid"><div class="taxography-grid"><ul>';
    foreach($wpbtags as $tag) {
        $output.= '<li class="item"><a href="'. get_term_link($tag, $taxonomy ) .'" style="background-image: url(\'http://localhost/wordpress/wp-content/uploads/books/' . $tag->slug . '.png\')"><span class="count">'. $tag->count .'</span><span class="taxography-name">'. $tag->name . '</span></a></li>';
    }
    $output.= '</ul></div></div>';

    // Add our pagination links, I have used the default 'get_*_posts_link()'. Adjust accordingly
    $output .= get_next_posts_link( 'Next Terms', $max_num_pages ) . '</br>';
    $output .= get_previous_posts_link( 'Previous Terms' ) . '</br>';

    return $output;
});

It is a good idea to also check out the Shortcode API on how to correctly create shortcodes and how it should be used

EDIT

For numbered pagination links, you can use a modified version my pagination function which I have written here in an answer. Make sure to check out my answer about how the code should be used and how it works.

Here is a modified version to work with code like your as well as for all type of queries. You should be able to use the function as is as described, with the only modification is that you can pass an integer value to query which will be the maximum amount of pages

/**
 * @author Pieter Goosen
 * @license GPLv2 
 * @link http://www.gnu.org/licenses/gpl-2.0.html
 *
 * This function returns numbered pagination links or text pagination links
 * depending what is been set
 *
 * Paginated numbered links uses get_pagenum_link() to return links to the
 * required pages
 * @uses http://wpseek.com/function/get_pagenum_link/
 *
 * The pagination links uses next_posts_link() and previous_posts_link()
 * @uses http://codex.wordpress.org/Function_Reference/next_posts_link
 * @uses http://codex.wordpress.org/Template_Tags/previous_posts_link
 *
 * @param array $args An array of key => value arguments. Defaults below 
 * - string query variable                  'query'                 => $GLOBALS['wp_query'],
 * - string Previous page text              'previous_page_text'    => __( '&laquo;' ),
 * - string Next page text                  'next_page_text'        => __( '&raquo;' ),
 * - string First page link text            'first_page_text'       => __( 'First' ),
 * - string Last page link text             'last_page_text'        => __( 'Last' ),
 * - string Older posts text                'next_link_text'        => __( 'Older Entries' ),
 * - string Newer posts text                'previous_link_text'    => __( 'Newer Entries' ),
 * - bool Whether to use links              'show_posts_links'      => false,
 * - int Amount of numbered links to show   'range'                 => 5,
 *
 * @return string $paginated_text
*/ 
function get_paginated_numbers( $args = [] ) {

    //Set defaults to use
    $defaults = [
        'query'                 => $GLOBALS['wp_query'],
        'previous_page_text'    => __( '&laquo;' ),
        'next_page_text'        => __( '&raquo;' ),
        'first_page_text'       => __( 'First' ),
        'last_page_text'        => __( 'Last' ),
        'next_link_text'        => __( 'Older Entries' ),
        'previous_link_text'    => __( 'Newer Entries' ),
        'show_posts_links'      => false,
        'range'                 => 5,
    ];

    // Merge default arguments with user set arguments
    $args = wp_parse_args( $args, $defaults );

    /**
     * Get current page if query is paginated and more than one page exists
     * The first page is set to 1
     * 
     * Static front pages is included
     *
     * @see WP_Query pagination parameter 'paged'
     * @link http://codex.wordpress.org/Class_Reference/WP_Query#Pagination_Parameters
     *
    */ 
    if ( get_query_var('paged') ) { 

        $current_page = get_query_var('paged'); 

    }elseif ( get_query_var('page') ) { 

        $current_page = get_query_var('page'); 

    }else{ 

        $current_page = 1; 

    }

    // Get the amount of pages from the query
    $max_pages = ( is_object( $args['query'] ) ) ? (int) $args['query']->max_num_pages : (int) $args['query'];

    /**
     * If $args['show_posts_links'] is set to false, numbered paginated links are returned
     * If $args['show_posts_links'] is set to true, pagination links are returned
    */
    if( false === $args['show_posts_links'] ) {

        // Don't display links if only one page exists
        if( 1 === $max_pages ) {

            $paginated_text="";

        }else{

            /**
             * For multi-paged queries, we need to set the variable ranges which will be used to check
             * the current page against and according to that set the correct output for the paginated numbers
            */
            $mid_range      = (int) floor( $args['range'] / 2 );
            $start_range    = range( 1 , $mid_range );
            $end_range      = range( ( $max_pages - $mid_range +1 ) , $max_pages );
            $exclude        = array_merge( $start_range, $end_range );  

            /**
             * The amount of pages must now be checked against $args['range']. If the total amount of pages
             * is less than $args['range'], the numbered links must be returned as is
             *
             * If the total amount of pages is more than $args['range'], then we need to calculate the offset
             * to just return the amount of page numbers specified in $args['range']. This defaults to 5, so at any
             * given instance, there will be 5 page numbers displayed
            */
            $check_range    = ( $args['range'] > $max_pages )   ? true : false;

            if( true === $check_range ) {

                $range_numbers = range( 1, $max_pages );

            }elseif( false === $check_range ) {

                if( !in_array( $current_page, $exclude ) ) {

                    $range_numbers = range( ( $current_page - $mid_range ), ( $current_page + $mid_range ) );

                }elseif( in_array( $current_page, $start_range ) && ( $current_page - $mid_range ) <= 0 ) {

                    $range_numbers = range( 1, $args['range'] );

                }elseif(  in_array( $current_page, $end_range ) && ( $current_page + $mid_range ) >= $max_pages ) {

                    $range_numbers = range( ( $max_pages - $args['range'] +1 ), $max_pages );

                }

            }

            /**
             * The page numbers are set into an array through this foreach loop. The current page, or active page
             * gets the class 'current' assigned to it. All the other pages get the class 'inactive' assigned to it
            */
            foreach ( $range_numbers as $v ) {

                if ( $v == $current_page ) { 

                    $page_numbers[] = '<span class="current">' . $v . '</span>';

                }else{

                    $page_numbers[] = '<a href="' . get_pagenum_link( $v ) . '" class="inactive">' . $v . '</a>';

                }

            }

            /** 
            * All the texts are set here and when they should be displayed which will link back to:
             * - $previous_page The previous page from the current active page
             * - $next_page The next page from the current active page
             * - $first_page Links back to page number 1
             * - $last_page Links to the last page
            */
            $previous_page  = ( $current_page !== 1 )                       ? '<a href="' . get_pagenum_link( $current_page - 1 ) . '">' . $args['previous_page_text'] . '</a>' : '';
            $next_page      = ( $current_page !== $max_pages )              ? '<a href="' . get_pagenum_link( $current_page + 1 ) . '">' . $args['next_page_text'] . '</a>'     : '';
            $first_page     = ( !in_array( 1, $range_numbers ) )            ? '<a href="' . get_pagenum_link( 1 ) . '">' . $args['first_page_text'] . '</a>'                    : '';
            $last_page      = ( !in_array( $max_pages, $range_numbers ) )   ? '<a href="' . get_pagenum_link( $max_pages ) . '">' . $args['last_page_text'] . '</a>'            : '';

            /**
             * Text to display before the page numbers
             * This is set to the following structure:
             * - Page X of Y
            */
            $page_text="<span>" . sprintf( __( 'Page %s of %s' ), $current_page, $max_pages ) . '</span>';
            // Turn the array of page numbers into a string
            $numbers_string = implode( ' ', $page_numbers );

            // The final output of the function
            $paginated_text="<div class="pagination">";
            $paginated_text .= $page_text . $first_page . $previous_page . $numbers_string . $next_page . $last_page;
            $paginated_text .= '</div>';

        }

    }elseif( true === $args['show_posts_links'] ) {

        /**
        * If $args['show_posts_links'] is set to true, only links to the previous and next pages are displayed
        * The $max_pages parameter is already set by the function to accommodate custom queries
        */
        $paginated_text = next_posts_link( '<div class="next-posts-link">' . $args['next_link_text'] . '</div>', $max_pages );
        $paginated_text .= previous_posts_link( '<div class="previous-posts-link">' . $args['previous_link_text'] . '</div>' );

    }

    // Finally return the output text from the function
    return $paginated_text;

}

You can simply add it anywhere in your functions.php or in a plugin, and then call it as follow in your shortcode

Replace

// Add our pagination links, I have used the default 'get_*_posts_link()'. Adjust accordingly
$output .= get_next_posts_link( 'Next Terms', $max_num_pages ) . '</br>';
$output .= get_previous_posts_link( 'Previous Terms' ) . '</br>';

with

if ( function_exists( 'get_paginated_numbers' ) )
    $output .= get_paginated_numbers( ['query' => $max_num_pages] );

Leave a Comment