Custom pagination for custom post types (by names)

I have two custom post types that deal with people’s names. Right now, in browsing views, it just lists them all alphabetically and the pagination breaks them down by numbers, which isn’t terribly helpful when you’re trying to find a specific person.

Specifically, I’ve been asked to create pagination links for people that look like this:

Read More
  • A-G
  • H-M
  • N-Q
  • R-Q

My problem – I can’t figure out how I can query the custom post types by the first letter of one field. Then, I’m not sure how I can go about creating the pagination in this way. Does anybody have any suggestions? Thank you!

Related posts

Leave a Reply

5 comments

  1. Interesting question! I solved it by expanding the WHERE query with a bunch of post_title LIKE 'A%' OR post_title LIKE 'B%' ... clauses. You can also use a regular expression to do a range search, but I believe the database won’t be able to use an index then.

    This is the core of the solution: a filter on the WHERE clause:

    add_filter( 'posts_where', 'wpse18703_posts_where', 10, 2 );
    function wpse18703_posts_where( $where, &$wp_query )
    {
        if ( $letter_range = $wp_query->get( 'wpse18703_range' ) ) {
            global $wpdb;
            $letter_clauses = array();
            foreach ( $letter_range as $letter ) {
                $letter_clauses[] = $wpdb->posts. '.post_title LIKE '' . $letter . '%'';
            }
            $where .= ' AND (' . implode( ' OR ', $letter_clauses ) . ') ';
        }
        return $where;
    }
    

    Of course you don’t want to allow random external input in your query. That is why I have an input sanitization step on pre_get_posts, which converts two query variables into a valid range. (If you find a way to break this please leave a comment so I can correct it)

    add_action( 'pre_get_posts', 'wpse18703_pre_get_posts' );
    function wpse18703_pre_get_posts( &$wp_query )
    {
        // Sanitize input
        $first_letter = $wp_query->get( 'wpse18725_first_letter' );
        $last_letter = $wp_query->get( 'wpse18725_last_letter' );
        if ( $first_letter || $last_letter ) {
            $first_letter = substr( strtoupper( $first_letter ), 0, 1 );
            $last_letter = substr( strtoupper( $last_letter ), 0, 1 );
            // Make sure the letters are valid
            // If only one letter is valid use only that letter, not a range
            if ( ! ( 'A' <= $first_letter && $first_letter <= 'Z' ) ) {
                $first_letter = $last_letter;
            }
            if ( ! ( 'A' <= $last_letter && $last_letter <= 'Z' ) ) {
                if ( $first_letter == $last_letter ) {
                    // None of the letters are valid, don't do a range query
                    return;
                }
                $last_letter = $first_letter;
            }
            $wp_query->set( 'posts_per_page', -1 );
            $wp_query->set( 'wpse18703_range', range( $first_letter, $last_letter ) );
        }
    }
    

    The final step is to create a pretty rewrite rule so you can go to example.com/posts/a-g/ or example.com/posts/a to see all posts beginning with this (range of) letter(s).

    add_action( 'init', 'wpse18725_init' );
    function wpse18725_init()
    {
        add_rewrite_rule( 'posts/(w)(-(w))?/?', 'index.php?wpse18725_first_letter=$matches[1]&wpse18725_last_letter=$matches[3]', 'top' );
    }
    
    add_filter( 'query_vars', 'wpse18725_query_vars' );
    function wpse18725_query_vars( $query_vars )
    {
        $query_vars[] = 'wpse18725_first_letter';
        $query_vars[] = 'wpse18725_last_letter';
        return $query_vars;
    }
    

    You can change the rewrite rule pattern to start with something else. If this is for a custom post type, be sure to add &post_type=your_custom_post_type to the substitution (the second string, which starts with index.php).

    Adding pagination links is left as an exercise for the reader 🙂

  2. This will help you getting started. I don’t know how you would break the query at specific letter and then tell WP that there’s another page with more letters, but the following takes 99% of the rest.

    Don’t forget to post your solution!

    query_posts( array( 'orderby' => 'title' ) );
    
    // Build an alphabet array
    foreach( range( 'A', 'G' ) as $letter )
        $alphabet[] = $letter;
    
    foreach( range( 'H', 'M' ) as $letter )
        $alphabet[] = $letter;
    
    foreach( range( 'N', 'Q' ) as $letter )
        $alphabet[] = $letter;
    
    foreach( range( 'R', 'Z' ) as $letter )
        $alphabet[] = $letter;
    
    if ( have_posts() ) 
    {
        while ( have_posts() )
        {
            global $wp_query, $post;
            $max_paged = $wp_query->query_vars['max_num_pages'];
            $paged = $wp_query->query_vars['paged'];
            if ( ! $paged )
                $paged = (int) 1;
    
            the_post();
    
            $first_title_letter = (string) substr( $post->post_title, 1 );
    
            if ( in_array( $first_title_letter, $alphabet ) )
            {
                // DO STUFF
            }
    
            // Pagination
            if ( $paged !== (int) 1 )
            {
                echo 'First: '._wp_link_page( 1 );
                echo 'Prev: '._wp_link_page( $paged - 1 );
            }
            while ( $i = 1; count($alphabet) < $max_paged; i++; )
            {
                echo $i._wp_link_page( $i );
            }
            if ( $paged !== $max_paged )
            {
                echo 'Next: '._wp_link_page( $paged + 1 );
                echo 'Last: '._wp_link_page( $max_paged );
            }
        } // endwhile;
    } // endif;
    
  3. An answer using @kaiser’s example, with a custom post type as a function accepting alpha start and end params. This example is obviously for a short list of items, as it does not include secondary paging. I’m posting it so you can incorporate the concept into your functions.php if you like.

    // Dr Alpha Paging
    // Tyrus Christiana, Senior Developer, BFGInteractive.com
    // Call like alphaPageDr( "A","d" );
    function alphaPageDr( $start, $end ) {
        echo "Alpha Start";
        $loop = new WP_Query( 'post_type=physician&orderby=title&order=asc' );      
        // Build an alphabet array of capitalized letters from params
        foreach ( range( $start, $end ) as $letter )
            $alphabet[] = strtoupper( $letter );    
        if ( $loop->have_posts() ) {
            echo "Has Posts";
            while ( $loop->have_posts() ) : $loop->the_post();              
                // Filter by the first letter of the last name
                $first_last_name_letter = ( string ) substr( get_field( "last_name" ), 0, 1 );
                if ( in_array( $first_last_name_letter, $alphabet ) ) {         
                    //Show things
                    echo  "<img class='sidebar_main_thumb' src= '" . 
                        get_field( "thumbnail" ) . "' />";
                    echo  "<div class='sidesbar_dr_name'>" . 
                        get_field( "salutation" ) . " " . 
                        get_field( 'first_name' ) . " " . 
                        get_field( 'last_name' ) . "</div>";
                    echo  "<div class='sidesbar_primary_specialty ' > Primary Specialty : " . 
                        get_field( "primary_specialty" ) . "</div>";                
                }
            endwhile;
        }
    }
    
  4. Here is a way to do this by using the query_vars and posts_where filters:

    public  function range_add($aVars) {
        $aVars[] = "range";
        return $aVars;
    }
    public  function range_where( $where, $args ) {
        if( !is_admin() ) {
            $range = ( isset($args->query_vars['range']) ? $args->query_vars['range'] : false );
            if( $range ) {
                $range = split(',',$range);
                $where .= "AND LEFT(wp_posts.post_title,1) BETWEEN '$range[0]' AND '$range[1]'";
            }
        }
        return $where;
    }
    add_filter( 'query_vars', array('atk','range_add') );
    add_filter( 'posts_where' , array('atk','range_where') );
    

    Souce: https://gist.github.com/3904986

  5. This is not so much an answer, but more a pointer to a direction to take. This will probably have to be 100% custom – and will be very involved. You’ll need to create a custom sql query (using the wpdb classs) and then for pagination you’ll pass these parameters to your custom query. You’ll probably need to also create new rewrite rules for this as well. Some functions to look into:

    add_rewrite_tag( '%byletter%', '([^/]+)');
    add_permastruct( 'byletter', 'byletter' . '/%byletter%' );
    $wp_rewrite->flush_rules();
    paginate_links()