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:
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!
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 🙂