Display WordPress Custom Post Types by Meta Value

This is going to be a developer-centric post. Anyone who isn’t familiar with WordPress development, custom post types and taxonomies and/or WP_Query can probably skip this one.

I recently developed a site that uses a plugin that I developed to allow them to list all the products that they manufacture and distribute. The plugin uses custom post types for their products, with three independent custom taxonomies for each brand that they manufacture. The site recently launched officially, so I encourage you to check them out. Last week they asked me if they could order their products by Product ID, one of the meta values that they enter when they are adding products into the database. This is something that I had researched before, and from what I could find at the time, you needed to create a custom database query to pull the data and arrange it in the right way, completely bypassing the standard WordPress loop. However, what I ultimately found was a means to not only order my products (custom post types) by a meta value, but also filter that list by taxonomy and taxonomy term all in a regular WP_Query.

This is how I did it. By no means do I consider myself to be a coding genius, so if there is a better way of doing this, feel free to share in the comments. However, I couldn’t find anything that really covered what I was trying to do (not exactly) when I searched on the web, so this post is for you folks that are trying to do the same thing.

Prerequisites

  • Custom Post Type (My custom post type is called ‘products’ — I’m not using more unique, descriptive CPT names because I know they are not using any other plugins that use a custom post type named ‘products’, otherwise, I’d call it ‘ap_products’ or something.)
  • Custom Taxonomies (I have three taxonomies for three different brands of products. Each of these taxonomies contain several terms that break up that brand’s products into different product categories.)
  • taxonomy.php (All of this code went straight into a custom taxonomy.php file that I’m using for their site. A similar code will be added to custom page templates that display all the products for each brand (taxonomy) shortly after the writing of this post.)
  • WP_Query (We’ll be using WP_Query for this experiment, which bypasses the standard WordPress loop. However, as you’ll see, we’re actually using a regular loop first, and then rewinding and using our custom WP_Query.)

Ready? Let’s go:

The Code

This is the first block of code in my taxonomy.php.

<?php
/*
 * this is the taxonomy page.  this displays all the products by category and brand
 */
 
get_header(); ?>
<div id="content">
	<div class="content-wide" role="main">
<?php
	if ( have_posts() )	the_post();
		$term = get_term_by( 'slug', get_query_var( 'term' ), get_query_var( 'taxonomy' ) ); 
		$tax = get_query_var( 'taxonomy' ); 			
			if (($plenus_taxonomies=wp_get_object_terms($post->ID, 'pgi')) != null) { /* if the taxonomy "pgi" is not null, display this logo */ ?>			
			<div class="taxonomy brand-logo"><a href="<?php echo home_url(); ?>/plenus-group-products/"><img src="<?php bloginfo('template_url'); ?>/images/plenuslogo.png" alt="Plenus Group, Inc." /></a></div><?php }
			  if (($plenus_taxonomies=wp_get_object_terms($post->ID, 'bcc')) != null) { /* if the taxonomy "bcc" is not null, display this other logo */ ?>
			<div class="bcc-banner"><a href="http://www.bostonchowda.com" target="_blank"><img src="<?php bloginfo('template_url'); ?>/images/bcc-banner.png" alt="Boston Chowda Co." /></a></div><?php }
			  if (($plenus_taxonomies=wp_get_object_terms($post->ID, 'ecg')) != null) { /* if the taxonomy "ecg" is not null, display THIS logo */ ?>
			<div class="ecg-banner"><a href="http://www.eastcoastgourmet.com" target="_blank"><img src="<?php bloginfo('template_url'); ?>/images/ecg-banner.png" alt="East Coast Gourmet" /></a></div><?php } ?>			
		<span class="breadcrumb"><h3><?php echo $term->name; ?></h3></span>

This first loop checks the taxonomy that we’re looking at, and displays a custom banner for that taxonomy. This is entirely cosmetic, but looks pretty cool.

<?php
	/* Since we called the_post() above, we need to
	 * rewind the loop back to the beginning that way
	 * we can run the loop properly, in full.
	 */
	rewind_posts();
	global $post, $wp_query;
	$term = get_term_by( 'slug', get_query_var( 'term' ), get_query_var( 'taxonomy' ) ); 
	$args = array(
		'post_type' => 'products',
		'meta_key' => 'product_ID',
		$term->taxonomy => $term->slug,
		'meta_query' => array(
			array(
                 	'key' => 'product_ID',
			'type' => 'NUMERIC',
			),
     		),
		'posts_per_page' => '-1',
     		'orderby' => 'meta_value',
     		'order' => 'ASC',
	);
	$temp = $wp_query;
	$wp_query = null;
	$wp_query = new WP_Query();
	$wp_query->query($args);
        while ($wp_query->have_posts()) : $wp_query->the_post(); ?>
		<div <?php post_class(); ?>>
			<?php
			//the_meta();
			$new_title = get_the_title();
			$new_title = str_replace(' ', '', $new_title);
			echo "<a name=\"".$new_title."\"></a>";
			//echo $term->taxonomy;
			?>

This is the meat of the code, where most of the work is being done. If you’re familiar with WP_Query, this will all look familiar. The real a ha! moment came when I copied the $term variable that I was using on another page template to get all the taxonomy and term data, and then fed that into the arguments with this line:

$term->taxonomy => $term->slug,

Everywhere you look in the Codex and elsewhere, you will see how to create a list like what we’re trying to do to filter posts by taxonomy and term with one (or both) of those values hard-coded. So instead of using the line above, your $args would look like this:

	$args = array(
		'post_type' => 'products',
		'bcc' => 'retail-soups',
	);

This is fine if you wanted to create a million taxonomy-my-term.php templates for all of your taxonomy terms, but that’s not really very feasible if you’re developing a site to deliver to a client (e.g. not just for yourself). I needed to be able to grab that information dynamically. The aggravating thing was that I knew that it was being pulled dynamically from somewhere, because the title of the page (taxonomy term) and the banner (taxonomy) were being generated dynamically. That’s when I started echoing various things to see what kind of values I had where, and realized that when I did echo $term->slug I got the slug of the taxonomy term that I wanted (e.g. ‘retail-soups’) and when I did echo $term->taxonomy I got the slug of the parent taxonomy for that term. So that $term variable ended up being the crux of being able to display only the posts for the specific taxonomy term that I wanted.

The next key ingredient was ordering by a meta value. This sent me all over the web and I tried various different incarnations before I came across this:

		'meta_key' => 'product_ID',
		'meta_query' => array(
			array(
                 	'key' => 'product_ID',
			'type' => 'NUMERIC',
			),
     		),
     		'orderby' => 'meta_value',
     		'order' => 'ASC',

In this example, product_ID is the name of the meta field that I want to sort by. The meta_query pulls that meta field and we’re defining the type of data in that field as numeric (as opposed to a string). Now that we’ve got the meta_query defined, we can order by the meta_value of that meta_key and list them in the order we want (in this case ascending order).

For another project a couple months ago, I was asked to do something similar for custom post types (events) that included a meta field that was a date. The goal was to order the posts not by the post date, but by the date stored in the meta value. Additionally, we needed to “expire” old events — i.e. events that already happened. Using the above method, we could easily adapt the code above to work for events, and then compare the event date with today’s date and only display those posts whose value was >= today’s date. I plan on returning to this concept and playing around with this using this WP_Query.

The rest of my taxonomy.php is pretty standard. Here’s the entire page:

<?php
/*
 * this is the taxonomy page.  this displays all the products by category and brand
 */
 
get_header(); ?>
<div id="content">
	<div class="content-wide" role="main">
<?php
	if ( have_posts() )	the_post();
		$term = get_term_by( 'slug', get_query_var( 'term' ), get_query_var( 'taxonomy' ) ); 
		$tax = get_query_var( 'taxonomy' ); 			
			if (($plenus_taxonomies=wp_get_object_terms($post->ID, 'pgi')) != null) { /* if the taxonomy "pgi" is not null, display this logo */ ?>			
			<div class="taxonomy brand-logo"><a href="<?php echo home_url(); ?>/plenus-group-products/"><img src="<?php bloginfo('template_url'); ?>/images/plenuslogo.png" alt="Plenus Group, Inc." /></a></div><?php }
			  if (($plenus_taxonomies=wp_get_object_terms($post->ID, 'bcc')) != null) { /* if the taxonomy "bcc" is not null, display this other logo */ ?>
			<div class="bcc-banner"><a href="http://www.bostonchowda.com" target="_blank"><img src="<?php bloginfo('template_url'); ?>/images/bcc-banner.png" alt="Boston Chowda Co." /></a></div><?php }
			  if (($plenus_taxonomies=wp_get_object_terms($post->ID, 'ecg')) != null) { /* if the taxonomy "ecg" is not null, display THIS logo */ ?>
			<div class="ecg-banner"><a href="http://www.eastcoastgourmet.com" target="_blank"><img src="<?php bloginfo('template_url'); ?>/images/ecg-banner.png" alt="East Coast Gourmet" /></a></div><?php } ?>			
		<span class="breadcrumb"><h3><?php echo $term->name; ?></h3></span>
<?php
	/* Since we called the_post() above, we need to
	 * rewind the loop back to the beginning that way
	 * we can run the loop properly, in full.
	 */
	rewind_posts();
	global $post, $wp_query;
	$term = get_term_by( 'slug', get_query_var( 'term' ), get_query_var( 'taxonomy' ) ); 
	$args = array(
		'post_type' => 'products',
		'meta_key' => 'product_ID',
		$term->taxonomy => $term->slug,
		'meta_query' => array(
			array(
                 	'key' => 'product_ID',
			'type' => 'NUMERIC',
			),
     		),
		'posts_per_page' => '-1',
     		'orderby' => 'meta_value',
     		'order' => 'ASC'
	);
	$temp = $wp_query;
	$wp_query = null;
	$wp_query = new WP_Query();
	$wp_query->query($args);
        while ($wp_query->have_posts()) : $wp_query->the_post(); ?>
		<div <?php post_class(); ?>>
			<?php
			//the_meta();
			$new_title = get_the_title();
			$new_title = str_replace(' ', '', $new_title);
			echo "<a name=\"".$new_title."\"></a>";
			//echo $term->taxonomy;
			?>
			<div id="tax_product">
				<div id="tax_product_image_section"><?php if (get_post_meta($post->ID,'product_img')) { ?><img src="<?php echo get_post_meta($post->ID, 'product_img', true); ?>" /><br /><?php } ?></div> <!-- if there's a product image, display it : otherwise, don't -->
				<div id="product_info"><h3><a href="<?php echo get_permalink(); ?>"><?php echo get_the_title(); ?></a></h3>
					<?php if (get_post_meta($post->ID, 'product_ID')) { ?>Item ID: <?php echo get_post_meta($post->ID, 'product_ID', true); ?><br /><?php } ?>
					<?php if (get_post_meta($post->ID, 'product_barcode')) { ?>Barcode: <?php echo get_post_meta($post->ID, 'product_barcode', true); ?><br /><?php } ?>
						<!-- this is the same basic code as the single template, but will just display text rather than an image -->
						<div class="taxonomy" id="product_description"><p><?php echo get_post_meta($post->ID, 'product_description', true); ?></p></div>
						<div class="clear"></div>											
				</div>
			</div>
		</div>
			<?php endwhile; ?>
	<div class="clear"></div>
	<div class="navigation">
		<div class="alignright"><?php next_posts_link('Next page &raquo;') ?></div>
		<div class="alignleft"><?php previous_posts_link('&laquo; Previous page') ?></div>
	</div>			
	<?php $wp_query = null; $wp_query = $temp;?>
	</div><!-- .content -->
	<div class="clear"></div>
<?php get_footer(); ?>

(Note: due to a bug that is currently active in Trac, it is not possible to paginate pages where the only type of post is a custom post type, so the next_posts_link and previous_posts_link in the navigation div at the bottom isn’t really doing anything for us at the moment.)

I hope this helps someone because I have been looking for a solution to this problem for months. Part of the reason I actually accepted this update to my client’s page was because I wanted the opportunity to solve this riddle and I’m happy with the results. You can take a look at this code being used live on the Plenus Group site.