Shortcode For Alpahbetical List of Posts By Term

UPDATE:  This code is now available as a plugin.  Just download the plugin to your local machine, install it through Admin->Plugins->Add New->Upload, and activate it.  Then place a shortcode as described below in any Post or Page.

Download here:

Click to Download

This code was written in response to a request to list all States alphabetically as links to an alphabetic list of all Cities in each State. The City links were to point to a Post for each link.
A shortcode approach was chosen so that the lists could be used in practically any theme.

Add all of the code below to your functions.php file and add the CSS shown following the code to your style.css.

 * Shortcode to list posts that are assigned a given term in a taxonomy.
 * By default it lists posts that are in the 'uncategorized' category.
 * Posts are sorted on title, or a custom field if present, and grouped by
 * the first letter of the sort field.
 * Example: List all posts in the category 'state', 3 per row, using the
 *    custom field named 'state-name' for the sort key:
 *    [mam_list_term_posts term_slug="state" title_field='state-name']
 * Version 4.1
function mam_list_term_posts_func($atts) {
	extract( shortcode_atts( array(
		'tax_slug' => 'category',
		'term_slug' => 'uncategorized',
		'post_type' => 'post',
		'posts_per_row' => 3,            // Should be 1, 2, 3, or 4
		'posts_per_page' => -1,
		'title_field' => 'custom_title',
	), $atts ) );

	global $wp_query,$post;

	$taxobject = get_taxonomy($tax_slug);
	if( !$taxobject )
	   return "<h2>Sorry, no taxonomy found for taxonomy='{$tax_slug}'!</h2>";
	$tax_name = $taxobject->labels->name;

	// Get the slug term id
	$args = array(
      'slug' => $term_slug,
      'hide_empty' => true,
      'hierarchical' => true,
	$termarray = get_terms($tax_slug,$args);
	if ( !$termarray )
	   return "<h2>Sorry, no term found for taxonomy='{$tax_name}' term='{$term_slug}'!</h2>";
	$term = $termarray[0];

	// Use filters to modify the query to sort on a Custom Field if present.

   $paged = (get_query_var('paged')) ? get_query_var('paged') : 1;
   $args = array (
      'posts_per_page' => $posts_per_page,
      'post_type' => $post_type,
      'orderby' => 'title',
      'order' => 'ASC',
      'paged' => $paged,
      'category__in' => array($term->term_id),
   $temp_query = clone $wp_query;
   $wp_query = new WP_QUERY($args);

   // If the query is paged, check for letters split across pages.
   $continued_from_msg = $continued_on_msg = '';
   if ($wp_query->max_num_pages > 1) mam_check_for_split_letters($continued_from_msg, $continued_on_msg, $args);

   // Clear query filters set by mam_alter_query_sort
   $mam_global_fields = $mam_global_join = $mam_global_orderby = '';

   if ( have_posts() ) {
      $in_this_row = 0;
      while ( have_posts() ) {
         $title = ( $title_field ) ? $post->sort_key : apply_filters('the_title', get_the_title());
         $first_letter = strtoupper(substr($title,0,1));
         if ($first_letter != $curr_letter) {
            if (++$post_count > 1) {
            mam_start_new_letter($first_letter, $posts_per_row, $continued_from_msg);
            $continued_from_msg = '';
            $curr_letter = $first_letter;
            $in_this_row = 0;
         if (++$in_this_row > $posts_per_row) {
            $in_this_row = 1;  // Account for this first post
         } ?>
         <div class="title-cell title-cell-<?php echo $posts_per_row; ?>"><a href="<?php the_permalink() ?>" rel="bookmark" title="Permanent Link to <?php the_title_attribute(); ?>"><?php echo $title; ?></a></div>
      <?php }
      if( $continued_on_msg ) echo $continued_on_msg;
      <div class="navigation">
         <div class="alignleft"><?php next_posts_link('&laquo; Higher Letters') ?></div>
         <div class="alignright"><?php previous_posts_link('Lower Letters &raquo;') ?></div>
   <?php $wp_query = clone $temp_query;
   } else {
      echo "<h2>Sorry, no $post_type entries were found for taxonomy='{$tax_name}' term='{$term->name}'!</h2>";
   $results = ob_get_clean();
   return $results;

function mam_end_prev_letter() {
   echo "</div><!-- End of letter-group -->\n";
   echo "<div class='clear clear-letter-group'></div>\n";
function mam_start_new_letter($letter, $posts_per_row, $continued_from_msg) {
   echo "<div class='letter-group'>\n";
   echo "\t<div class='letter-cell-row'><div class='letter-cell'>$letter</div>$continued_from_msg</div>\n";
   echo "\t\t<div class='clear'></div>\n";
function mam_end_prev_row() {
   echo "\t</div><!-- End row-cells -->\n";
function mam_start_new_row($posts_per_row) {
   global $in_this_row;
   $in_this_row = 0;
   echo "\t<div class='row-cells row-cells-{$posts_per_row}'>\n";
function mam_alter_query_sort($cf_name) {
   global $wpdb;
   global $mam_global_fields, $mam_global_join, $mam_global_orderby;
   $mam_global_fields = ", IFNULL(pm.meta_value, $wpdb->posts.post_title) as sort_key";
   $mam_global_join = " LEFT OUTER JOIN $wpdb->postmeta pm ON ($wpdb->posts.ID = pm.post_id AND pm.meta_key = '$cf_name')";
   $mam_global_orderby = " UPPER(sort_key) ASC, UPPER($wpdb->posts.post_title) ASC";

if ( !function_exists('mam_posts_fields') ) {
   function mam_posts_fields ($fields) {
      global $mam_global_fields;
      // Make sure there is a leading comma
      if ($mam_global_fields) $fields .= (preg_match('/^(\s+)?,/',$mam_global_fields)) ? $mam_global_fields : ", $mam_global_fields";
      return $fields;
if ( !function_exists('mam_posts_join') ) {
   function mam_posts_join ($join) {
      global $mam_global_join;
      if ($mam_global_join) $join .= " $mam_global_join";
      return $join;
if ( !function_exists('mam_posts_where') ) {
   function mam_posts_where ($where) {
      global $mam_global_where;
      if ($mam_global_where) $where .= " $mam_global_where";
      return $where;
if ( !function_exists('mam_posts_orderby') ) {
   function mam_posts_orderby ($orderby) {
      global $mam_global_orderby;
      if ($mam_global_orderby) $orderby = $mam_global_orderby;
      return $orderby;

function mam_check_for_split_letters(&$continued_from_msg, &$continued_on_msg, $args) {
   global $wp_query, $wpdb;

   $this_page = $wp_query->query['paged'];
   $max_pages = $wp_query->max_num_pages;
   $posts_this_page = sizeof($wp_query->posts);
   $first_letter = strtoupper(substr($wp_query->posts[0]->sort_key,0,1));
   $last_letter = strtoupper(substr($wp_query->posts[$posts_this_page - 1]->sort_key,0,1));

   // If we are on any page other than 1, check for letter continued from.
   if ($this_page > 1) {
      // Create new query to get just one post that precedes this page.
      $query = $wp_query->request;
      if ( preg_match('/limit (\d+), *(\d+)/i', $query, $matches)) {
         $new_limit = $matches[1] - 1;
         $new_query = str_replace($matches[0],"LIMIT $new_limit, 1", $query);
         $prior_post = $wpdb->get_row($new_query);
         $prior_letter = strtoupper(substr($prior_post->sort_key,0,1));
         if ($prior_letter == $first_letter) {
            $continued_from_msg = "<span class='continued-from'> (letter $prior_letter continued from prior page) </span>";

   // If we are on any page other than the last one, check for letter continued on.
   if ($this_page < $max_pages) {
      // Create new query to get just one post that follows this page.
      $query = $wp_query->request;
      if ( preg_match('/limit (\d+), *(\d+)/i', $query, $matches)) {
         $new_limit = $matches[1] + $posts_this_page;
         $new_query = str_replace($matches[0],"LIMIT $new_limit, 1", $query);
         $next_post = $wpdb->get_row($new_query);
         $next_letter = strtoupper(substr($next_post->sort_key,0,1));
         if ($next_letter == $last_letter) {
            $continued_on_msg = "<div class='continued-on'> (letter $next_letter continued on next page) </div>";

Here is the CSS:

.letter-group { width: 100%; }
.letter-cell-row { float: left; }
.letter-cell {
   background: #e0e0e0;
   float: left;
   font-weight: bold;
   height: 2em;
   margin-bottom: 8px;
   min-width: 3em;
   padding-top: 8px;
   text-align: center;
.row-cells { width: 90%; float: right; }
.title-cell { float: left; overflow: hidden; margin-bottom: 8px; }
.title-cell-1 { width: 98%; }
.title-cell-2 { width: 48%; }
.title-cell-3 { width: 32%; }
.title-cell-4 { width: 23%; }
.continued-on {
    padding: 0 2em;
.clear { clear: both; }

9 Responses to Shortcode For Alpahbetical List of Posts By Term

  • Adi says:


    I am trying to get the state/city code to work on the WPtouch mobile plugin. The plugin turns my desktop theme to a mobile theme.

    The shortcode doesn’t display properly on the mobile theme and I would like to know what is required to output those results.

    Can you please let me know if you can assist? Thanks!

    • Mac McDonald says:

      Sorry for the delay in replying – I am not getting emails when comments are posted.

      I have not used that plugin, so I am afraid I cannot help.

  • Fabio says:

    Hello, Mac!

    First, let me tell you, it’s a great plugin you got there, I love it!

    The thing is, I wanted to limit the exibition of all the posts to a single first letter, allowing navigation to the next and previous letters. Maybe with a list of first caracters righ bellow the list, available for choosing, between NEXT and PREVIOUS, like:

    Can you help me, please?

    Thank you and keep up the good work!

  • This looks to be the plugin i was after for our posts index . I only have one issue i am trying to get it to list only posts of post_type=”incsub_wiki” undder a category but it doesnt seem to want to do that.

    I was trying :

    [mam_list_term_posts post_type=”post” term_slug=”news” title_field=”]

    which works for my news category but if i try :

    [mam_list_term_posts post_type=”incsub_wiki” term_slug=”wiki” title_field=”]

    Its says there is no category when in my Wiki there is a category wiki .

    Any ideas please and if this plugin gets our problems solved i’ll even add a link to your site on our sponsors page.


    • Mac McDonald says:

      Please post the error message you get.

      EDIT: Just a thought – are there any posts assigned the ‘wiki’ category? Also, is ‘wiki’ a category or something else?

  • Ryan says:

    For some reason the plugin is not returning any results even though it there are posts associated with that taxonomy and that term. Any idea why this might be happening?

    • Mac McDonald says:

      Are you sure you are using the ‘slugs’ and not the ‘names’? What happens if you use only the default parameters?

Leave a Reply

Your email address will not be published. Required fields are marked *