
When I build the Radiant Body Yoga site for Kia Miller, I created the directory of RBY-certified teachers using the Our Team plugin from WooThemes and FacetWP. It was fairly easy to add additional fields and not difficult to code the FacetWP query; tinkering with the CSS for the grid display probably took longer than setting up the query.
A couple of WordPress upgrades (and no Our Team upgrades) later, it wasn’t working properly. Kia’s assistant complained to me of not being able to save featured images, and when I went to check, I found she was right–something had ganged seriously agley.
I’d used other team plugins on occasion, but none was all that well-suited to what Kia wanted: to display the photo, name and location of each teacher, with a link to the person’s site, sorted by last name, and then be able to filter the list (well, grid) by city, state, and country. For some reason, most team and staff plugins don’t seem to use first name and last name as separate fields, making it a bit difficult to sort the display by last name. You’d think a feature like that might be obvious for a directory, but I guess not.
Also, the pagination on the FacetWP query wasn’t working. That by itself I’m pretty sure I could have solved, but I ran into further issues with FacetWP on this page later on, and decided to look for an alternative, which I found. (FacetWP is an amazing plugin and I’m pretty sure the issues were mainly my lack of development experience.)
So here’s what I did to get a working, filterable directory.
- Created a functionality plugin to register a “yoga teacher” post type.
- Set up a field group and applied it to the post type.
- Created a custom page_teacher_directory template to display the teacher entries alphabetically, with custom fields.
- Added CSS for a responsive grid display of teacher entries.
- Installed the Ultimate WP Query Search Filter plugin and set up a custom field search.
- Customized the display of the search box on the teacher directory page.
- Customized the Ajax search results to display the same fields and classes as the original query.
Now that you have the overview of what was involved, let me break it down.
Register yoga_teacher post type
I like to keep my custom post types in plugins. Right now I’m putting them in the regular plugin directory rather than in mu-plugins because I want to be able to turn them on and off easily when testing for problems. I suppose that once everything was working I could move the functionality plugin to mu-plugins, but so far I haven’t done that.
I learned on a recent project that sometimes you run out of room in the admin menu for custom post types, so in addition to registering the post types (and taxonomies, if I need them), I also added some code to rearrange the menu. And just to be fancy, I added the custom post types to the dashboard “At a Glance” metabox, too.
Here’s the code I used to register the yoga_teacher post type:
<?php /* Plugin Name: RBY Custom Post Type Plugin Plugin URI: https://wpfangirl.com/ Description: Yoga Teacher Post Type for RBY Author: Sallie Goetsch Version: 1.0 Author URI: https://wpfangirl.com License: GPL2 */ /* CREATE CUSTOM POST TYPE: Yoga Teachers ---------------------------------------------- */ add_action( 'init', 'create_yoga_teacher_post_type' ); register_activation_hook( __FILE__, 'activate_yoga_teacher_type' ); function activate_yoga_teacher_type() { create_yoga_teacher_post_type(); flush_rewrite_rules(); } function create_yoga_teacher_post_type() { $labels = array( 'name' => __( 'Yoga Teachers' ), 'singular_name' => __( 'Yoga Teacher' ), 'all_items' => __('All Yoga Teachers'), 'add_new' => _x('Add new Yoga Teacher', 'Teachers'), 'add_new_item' => __('Add new Yoga Teacher'), 'edit_item' => __('Edit Yoga Teacher'), 'new_item' => __('New Yoga Teacher'), 'view_item' => __('View Yoga Teacher'), 'search_items' => __('Search in Yoga Teachers'), 'not_found' => __('No Yoga Teachers found'), 'not_found_in_trash' => __('No Yoga Teachers found in trash'), 'parent_item_colon' => '' ); $args = array( 'capability_type' => 'post', 'hierarchical' => false, 'labels' => $labels, 'menu_icon' => 'dashicons-groups', 'public' => true, 'show_ui' => true, 'has_archive' => true, 'rewrite' => array('slug' => 'yoga-teachers'), 'taxonomies' => array( 'years-episode' ), 'supports' => array( 'title', 'editor', 'author', 'thumbnail' , 'excerpt', 'custom-fields', 'revisions', 'genesis-cpt-archives-settings' ) ); register_post_type( 'yoga_teacher', $args); } // REARRANGE ADMIN MENU ORDER add_filter('custom_menu_order', 'my_custom_menu_order'); add_filter('menu_order', 'my_custom_menu_order'); function my_custom_menu_order($menu_ord) { if (!$menu_ord) return true; return array( 'index.php', // the dashboard link 'edit.php?post_type=tribe_events', 'edit.php?post_type=product', 'edit.php?post_type=testimonial', 'edit.php?post_type=yoga_teacher', 'edit.php?post_type=page', 'admin.php?page=nestedpages', 'edit.php', // posts 'edit-comments.php', 'admin.php?page=gf_edit_forms' ); } // Add Custom Post Type to WP-ADMIN Right Now Widget // Ref Link: http://wpsnipp.com/index.php/functions-php/include-custom-post-types-in-right-now-admin-dashboard-widget/ // http://wordpress.org/support/topic/dashboard-at-a-glance-custom-post-types // http://halfelf.org/2012/my-custom-posttypes-live-in-mu/ function vm_right_now_content_table_end() { $args = array( 'public' => true , '_builtin' => false ); $output = 'object'; $operator = 'and'; $post_types = get_post_types( $args , $output , $operator ); foreach( $post_types as $post_type ) { $num_posts = wp_count_posts( $post_type->name ); $num = number_format_i18n( $num_posts->publish ); $text = _n( $post_type->labels->name, $post_type->labels->name , intval( $num_posts->publish ) ); if ( current_user_can( 'edit_posts' ) ) { $cpt_name = $post_type->name; } echo '<li class="post-count"><tr><a href="edit.php?post_type='.$cpt_name.'"><td class="first b b-' . $post_type->name . '"></td>' . $num . ' <td class="t ' . $post_type->name . '">' . $text . '</td></a></tr></li>'; } // $taxonomies = get_taxonomies( $args , $output , $operator ); // foreach( $taxonomies as $taxonomy ) { // $num_terms = wp_count_terms( $taxonomy->name ); // $num = number_format_i18n( $num_terms ); // $text = _n( $taxonomy->labels->name, $taxonomy->labels->name , intval( $num_terms )); // if ( current_user_can( 'manage_categories' ) ) { // $cpt_tax = $taxonomy->name; // } // echo '<li class="post-count"><tr><a href="edit-tags.php?taxonomy='.$cpt_tax.'"><td class="first b b-' . $taxonomy->name . '"></td>' . $num . ' <td class="t ' . $taxonomy->name . '">' . $text . '</td></a></tr></li>'; // } } add_action( 'dashboard_glance_items' , 'vm_right_now_content_table_end' ); ?>
Note that the last bit of code displays ALL custom post types in the “At a Glance” section, not just the yoga_teacher type. For more control over the display, try the Glance That plugin.
Set up yoga teacher field group
I’m going to assume for the sake of this tutorial that you know how to use Advanced Custom Fields. If not, there are plenty of other tutorials on how to set up field groups.
I created fields for Teacher First Name, Teacher Last Name, Nickname or Alternate Name, Teacher URL, City, State, and Country, and set them to display only on the yoga_teacher post type and to hide the normal post edit screen.
That resulted in a yoga teacher edit screen like this:
Create a custom page_teacher_directory template
In this case, I didn’t even want to show the single entries, because all the content was there on the directory page. And I also wanted the directory to show up in the normal page hierarchy, because of the way the sidebar menus were being generated and because site structure and page hierarchy are becoming more important to SEO.
This one took me a little fiddling, mainly because I’m not all that good at nested conditionals but didn’t want to display the optional fields if they were empty, but here’s what I came up with:
<?php /** * Template Name: Teacher Directory Page * @author WP Fangirl * @package Enviable * @subpackage Customizations */ // Add teacher directory loop add_action( 'genesis_loop', 'rby_teacher_directory_loop' ); function rby_teacher_directory_loop() { echo '<div class="teacher-directory-wrap">'; $query = new WP_Query( array( 'post_type' => 'yoga_teacher', 'post_status' => 'publish', 'meta_key' => 'teacher_last_name', 'orderby'=> 'meta_value', 'order' => 'ASC', 'paged' => get_query_var( 'paged' ), 'posts_per_page' => 18, ) ); if ( $query->have_posts() ) : while ( $query->have_posts() ) : $query->the_post(); global $post; echo '<div class="team-members">'; // Show Featured Image if ( has_post_thumbnail() ) { echo '<figure itemprop="image">'; the_post_thumbnail('thumbnail', array('class' => 'teacher-avatar') ); echo '</figure>'; } echo '<div class="teacher-name">'; // open teacher-name div // Opening H3 tag around teacher name echo '<h3 itemprop="name" class="member">'; // Link to Teacher URL if ( get_field('teacher_url') ) { echo '<a href="'. get_field('teacher_url') .'">'; } // Display Teacher Name echo the_field('teacher_first_name'); echo ' '; echo the_field('teacher_last_name') ; // Add closing tag to link if (get_field('teacher_url')) { echo '</a>'; } echo '</h3>'; //Closing H3 tag around teacher name // Display Teacher Nickname/Alternate Name if (get_field('teacher_nickname')) : echo '<h6 class="member">('. get_field('teacher_nickname') .')</h6>'; endif; echo '</div>'; // close teacher-name div echo '<div class="teacher-location">'; // Begin location list // Display teacher city if field has a value echo '<li class="teacher-city">'. get_field('teacher_city') . '</li>'; // Display teacher state or region if field has a value if (get_field('teacher_state')) : echo '<li class="teacher-state">'. get_field('teacher_state') . '</li>'; endif; // Display teacher country if field has a value echo '<li class="teacher-country">'. get_field('teacher_country') . '</li>'; echo '</div>'; // End location list echo '</div>'; // Close team-member div endwhile; genesis_posts_nav(); endif; wp_reset_query(); echo '</div>'; //close teacher directory wrap } genesis();
I applied the template to the existing teacher directory page and added a couple of lines of introductory text.
Add CSS for a responsive grid display of teacher entries
Actually, I had set all this CSS up when I created the original directory, but you’ll need it if you want to duplicate what I did.
/*---------------Teacher Directory Styles ---------------- */ li.teacher-city::after, li.teacher-state::after { font-family: Arial, Helvetica, sans-serif; content: '\2c\00a0'; } div.team-members { width:250px; height:300px; float: left; margin-right: 5px; justify-content: center; } div.teacher-location { display:flex; justify-content:center; } .teacher-city, .teacher-state, .teacher-country { color: #333; } .teacher-country { clear: right; } h3.member, h3.member a { font-family: 'Open Sans Condensed', Lato, Helvetica, sans-serif; font-weight:bold; font-size: 24px; text-align:center; margin-bottom:4px; } h3.member a, h6.member a { color: #e96d5d; } h6.member { font-family: 'Open Sans Condensed', Lato, Helvetica, sans-serif; text-align:center; margin-top:4px; margin-bottom:4px; } .team-members figure { max-width:250px; justify-content: center; } img.teacher-avatar { clear:both; margin: 0 auto; }
The flex display on the div.team-member saved me writing a few media queries and helped center everything. (Please, anyone who sees anything I can improve, let me know.)
That gives me a directory page with three teachers across in desktop view, since the page has a sidebar and the main content area is 850 px wide.
The teachers stack nicely as the page shrinks, showing two across on medium tablet screens and one across on phone screens.

Install the Ultimate WP Query Search Filter plugin and set up a custom field search
For some reason, when I added the facetwp-template class to the page, however, I got an error message saying “FacetWP is unable to auto-detect the main query.” I had followed the directions for using FacetWP with existing template files, but it wasn’t working. If I took out the facetwp-template class, I could see the directory, but the “Narrow Search” widget in the sidebar wouldn’t display anything in the dropdowns. (Yes, I re-indexed.) This might have been due to something besides FacetWP, but after wrestling with it unsuccessfully, I decided to look for another option.
A little Google Fu uncovered the Ultimate WP Query Search Filter plugin (UWPQSF for short). Setting up the search was the easy part, because the search builder already works with ACF fields (and other custom fields). I set the Ajax search template to display the results in the same div as the original directory (.teacher-directory-wrap), then set up the meta fields for city, state, and country. I decided to display them as dropdowns to keep the search form compact.
To show the search box on the page, I just pasted the shortcode (in this case [[ULWPQSF id=6364]]
) into the content metabox of the page edit screen.
Customize the search box display
After I’d added the search box to the page, I needed a little custom CSS to match the box to the theme and display the field drop-downs in a row rather than a column.
/* Ultimate WP Search Query Styles -------------------------------------------------- */ #uwpqsf_id{ border:1px solid #efefef; padding:8px; } .uform_title { color: #e96d5d; font-family: 'Open Sans Condensed', 'Lato', Arial, Helvetica, sans-serif; font-weight: 600; font-size:22px; } .uwpqsf_class { float:left; width: 31.623931623931625%; margin-bottom:5px; margin-left:8px; clear:none; } .uwpqsf_class::last-child { float:right; margin-left:0; } .uwpqsf_submit { width:100%; clear:both; text-align:center; margin:15px 0; } @media only screen and (max-width: 768px) { .uwpqsf_class { float:none; width: 100%; margin-left: 0; clear:both; } }
That gives us a search box that looks like this on larger screens…
…and like this on smaller screens:
So far, so good. But the search results showed up in a column with all the wrong formatting and none of the custom fields, which leads me to the final step of this process.
Customize the Ajax search results to display the same fields and classes as the original query
The author of Ultimate WP Query Search Filter created one and only one hook in his plugin, but fortunately, it was the one I needed and he had some sample code on his website. After pasting it into my functions.php file (I wondered about putting it into the page template but wasn’t sure it would work there), I modified the query to be exactly the same as the one for the page template itself. This meant I didn’t have to write any new CSS for the results display. The code looked like this:
// Customize Ajax Search Results for Ultimate WP Query Search add_filter('uwpqsf_result_tempt', 'customize_output', '', 4); function customize_output($results , $arg, $id, $getdata ){ // The Query $apiclass = new uwpqsfprocess(); $query = new WP_Query( $arg ); ob_start(); $result = ''; // The Loop if ( $query->have_posts() ) { while ( $query->have_posts() ) { $query->the_post();global $post; echo '<div class="team-members">'; // Show Featured Image if ( has_post_thumbnail() ) { echo '<figure itemprop="image">'; the_post_thumbnail('thumbnail', array('class' => 'teacher-avatar') ); echo '</figure>'; } echo '<div class="teacher-name">'; // open teacher-name div // Opening H3 tag around teacher name echo '<h3 itemprop="name" class="member">'; // Link to Teacher URL if ( get_field('teacher_url') ) { echo '<a href="'. get_field('teacher_url') .'">'; } // Display Teacher Name echo the_field('teacher_first_name'); echo ' '; echo the_field('teacher_last_name') ; // Add closing tag to link if (get_field('teacher_url')) { echo '</a>'; } echo '</h3>'; //Closing H3 tag around teacher name // Display Teacher Nickname/Alternate Name if (get_field('teacher_nickname')) : echo '<h6 class="member">('. get_field('teacher_nickname') .')</h6>'; endif; echo '</div>'; // close teacher-name div echo '<div class="teacher-location">'; // Begin location list // Display teacher city if field has a value echo '<li class="teacher-city">'. get_field('teacher_city') . '</li>'; // Display teacher state or region if field has a value if (get_field('teacher_state')) : echo '<li class="teacher-state">'. get_field('teacher_state') . '</li>'; endif; // Display teacher country if field has a value echo '<li class="teacher-country">'. get_field('teacher_country') . '</li>'; echo '</div>'; // End location list echo '</div>'; // Close team-member div } echo $apiclass->ajax_pagination($arg['paged'],$query->max_num_pages, 4, $id, $getdata); } else { echo 'no post found'; } /* Restore original Post Data */ wp_reset_postdata(); $results = ob_get_clean(); return $results; }
And the search results (in this case filtered for all yoga teachers in California) look like this:
The whole thing took me more hours than I’d like to put together, but the working version is not really that complicated, and I will likely re-use this code as the basis of other directories in the future. ACF makes it easy to set up different fields for different types of directories, which can then serve the client’s needs better than a generic plugin. (Though for a typical “meet the team” page the plugins are just fine.)
If you can show me how to do this better, by all means do so!
Thanks for your tip! It helped me a lot in a recent project.
this was an awesome help. Thank you
I just had to rebuild it with Search and Filter Pro, because UWPQSF hasn’t been updated and had stopped working. I hope to have time to write about that.