The following is a guest post by Kevin Leary. I was pretty stoked to get this guest post submission from Kevin because it’s my favorite kind of tutorial: practical and detailed.

Almost every custom theme I develop with WordPress requires a Management Team or Meet the Team page. If I had to guess, I’d say that I’ve built just around 50 different setups. It occurred to me that their must be many other WordPress developers out there creating similar systems as well. For this reason, I’ll share the approach I typically use to build and manage a “Meet The Team” page in WordPress.

For those of you that just want the final product, check out the PHP Class on Github or see an example.

See Example Repo on Github

Creating and managing a team page like this in WordPress involves the following combination of tools:

Custom post type (e.g. team )

) Custom taxonomy for filtering (e.g. department )

) A meta box UI for managing custom fields (e.g. position, email, phone, and social media links)

Using these tools, let’s walk through the process of creating a Meet Our Team template for a custom WordPress theme.

First & Foremost

Before we begin, I should clarify a few things. In the Github example for this tutorial I’m using an object oriented approach, and have stored my code in a separate .php file that I typically include in a theme’s functions.php file.

For the sake of clarity, I’ll be describing the process step by step procedurally and will refer to setting this up inside of your functions.php instead.

Create Post Type & Taxonomy

The first step is to register a new post type (e.g. team ). You can also register a taxonomy (e.g. department ) if filtering or categorization is required.

This post type will add a new Team Profiles menu to the WordPress admin, separating all team posts from Posts and Pages for easier content management.

The taxonomy will add a custom category to the team posts, allowing you to filter or categorize your team. I find that this is appropriate when you have a team that is more than ten people. It’s often handy for filtering staff members by office location or department.

Post Type

/** * Register `team` post type */ function team_post_type() { // Labels $labels = array( 'name' => _x("Team", "post type general name"), 'singular_name' => _x("Team", "post type singular name"), 'menu_name' => 'Team Profiles', 'add_new' => _x("Add New", "team item"), 'add_new_item' => __("Add New Profile"), 'edit_item' => __("Edit Profile"), 'new_item' => __("New Profile"), 'view_item' => __("View Profile"), 'search_items' => __("Search Profiles"), 'not_found' => __("No Profiles Found"), 'not_found_in_trash' => __("No Profiles Found in Trash"), 'parent_item_colon' => '' ); // Register post type register_post_type('team' , array( 'labels' => $labels, 'public' => true, 'has_archive' => false, 'menu_icon' => get_stylesheet_directory_uri() . '/lib/TeamProfiles/team-icon.png', 'rewrite' => false, 'supports' => array('title', 'editor', 'thumbnail') ) ); } add_action( 'init', 'team_post_type', 0 );

Optional Taxonomy

/** * Register `department` taxonomy */ function team_taxonomy() { // Labels $singular = 'Department'; $plural = 'Departments'; $labels = array( 'name' => _x( $plural, "taxonomy general name"), 'singular_name' => _x( $singular, "taxonomy singular name"), 'search_items' => __("Search $singular"), 'all_items' => __("All $singular"), 'parent_item' => __("Parent $singular"), 'parent_item_colon' => __("Parent $singular:"), 'edit_item' => __("Edit $singular"), 'update_item' => __("Update $singular"), 'add_new_item' => __("Add New $singular"), 'new_item_name' => __("New $singular Name"), ); // Register and attach to 'team' post type register_taxonomy( strtolower($singular), 'team', array( 'public' => true, 'show_ui' => true, 'show_in_nav_menus' => true, 'hierarchical' => true, 'query_var' => true, 'rewrite' => false, 'labels' => $labels ) ); } add_action( 'init', 'team_taxonomy', 0 );

In this example, we’re not actually using the department taxonomy for anything. I’ve included it in the tutorial because it’s useful to understand that it can be used to filter team members.

Meta Box for Custom Fields

Now that we have a new *Team Profiles* menu in WordPress, we need to customize the data that we store with each team post. In my experience most team profiles have the following fields:

Position

Email

Phone

Twitter

LinkedIn

To manage this content, I like to customize the Add New and Edit UI for the team post type, allowing site admins and authors to intuitively update this information without training.

My tool of choice for creating custom field meta box UIs is currently the Advanced Custom Fields (ACF) plugin.

To create this meta box you’ll need to install the ACF plugin, creating your fields under the Custom Fields admin menu. Below is a look at the fields and settings I’ve used for this tutorial.

If you’re lazy like me, you can import my XML export file to automate the field creation process. Here’s how:

Download my ‘Team Details’ field group export: acf-export-team-details.xml.zip Navigate to Tools » Import and select WordPress Install WP import plugin if prompted Upload and import the .xml file Select your user and ignore Import Attachments That’s it!

The ACF plugin stores its data inside of a custom post type, so the standard WordPress XML import tool can be used. Quite a smart move by the plugin author, Elliot Condon.

Extras

In my PHP Class I’ve added an admin notice that will prompt you to install the ACF plugin if you haven’t already. This provides a nice reminder that you need it to get the team post type working properly.

Custom Template

Now that we have our Team management system setup, we’ll need to output our team profiles somewhere on the site. To do this, I usually create a custom theme template (e.g. template-team.php ) that alters the view for a specific WordPress page. View the docs on WordPress.org for details about custom templates.

Loop to Display Team Posts

To output our team posts inside of our custom template, we’ll use the following code.

<?php /** * Template Name: Team */ the_post(); // Get 'team' posts $team_posts = get_posts( array( 'post_type' => 'team', 'posts_per_page' => -1, // Unlimited posts 'orderby' => 'title', // Order alphabetically by name ) ); if ( $team_posts ): ?> <section class="row profiles"> <div class="intro"> <h2>Meet The Team</h2> <p class="lead">“Individuals can and do make a difference, but it takes a team<br>to really mess things up.”</p> </div> <?php foreach ( $team_posts as $post ): setup_postdata($post); // Resize and CDNize thumbnails using Automattic Photon service $thumb_src = null; if ( has_post_thumbnail($post->ID) ) { $src = wp_get_attachment_image_src( get_post_thumbnail_id( $post->ID ), 'team-thumb' ); $thumb_src = $src[0]; } ?> <article class="col-sm-6 profile"> <div class="profile-header"> <?php if ( $thumb_src ): ?> <img src="<?php echo $thumb_src; ?>" alt="<?php the_title(); ?>, <?php the_field('team_position'); ?>" class="img-circle"> <?php endif; ?> </div> <div class="profile-content"> <h3><?php the_title(); ?></h3> <p class="lead position"><?php the_field('team_position'); ?></p> <?php the_content(); ?> </div> <div class="profile-footer"> <a href="tel:<?php the_field('team_phone'); ?>"><i class="icon-mobile-phone"></i></a> <a href="mailto:<?php echo antispambot( get_field('team_email') ); ?>"><i class="icon-envelope"></i></a> <?php if ( $twitter = get_field('team_twitter') ): ?> <a href="<?php echo $twitter; ?>"><i class="icon-twitter"></i></a> <?php endif; ?> <?php if ( $linkedin = get_field('team_linkedin') ): ?> <a href="<?php echo $linkedin; ?>"><i class="icon-linkedin"></i></a> <?php endif; ?> </div> </article><!-- /.profile --> <?php endforeach; ?> </section><!-- /.row --> <?php endif; ?>

To get a loop of our team posts, I’ve used the get_posts function. Its simple, easy to use and efficient. I’ve used the following arguments to target and organize the query results:

'post_type' => 'team' will only query team posts

will only query team posts 'posts_per_page' => 50 will return all of our team profiles, assuming you have less than 50. If you plan to have more adjust this accordingly.

will return all of our team profiles, assuming you have less than 50. If you plan to have more adjust this accordingly. 'orderby' => 'title' will order the results by name

will order the results by name 'order' =>; 'ASC' will start the order alphabetically

Once we have our query object, we loop through each team post, outputting data and content into our HTML structure.

The get_field and the_field functions are built-in to the ACF plugin. These are probably the two most commons functions that you will find yourself using when working with it. They output the value of a given custom field.

Once we have the loop complete, we can create a new Page in WordPress, selecting Team from the custom templates dropdown. When you view this page, you should see a list of your team’s profiles.

Notes on Performance

Without any caching, this bit of code adds a whopping 26 queries to the page. If you have a high volume site, it’s a requirement that you leverage the Transients API to cache the output of an intensive custom query like this. I’ve included a static display() method in my PHP Class that handles the transient caching.

if ( $team_profiles = TeamProfiles::display() ) echo $team_profiles;

The display() method uses output buffering and transient caching to store and cache the HTML generated by our team loop.

Using this approach instead of our previous loop reduces the number of queries to 1, saving us 25 hits to the database. This also reduces the initial page load by 400-500ms. Not bad!

Styling The Template with CSS

Now that we have our team page management system in place and our HTML structure output, we need to add some style to our new template.

Conditionally Load in CSS

To include a stylesheet for our custom template only ( template-team.php ), we can use the following conditional check.

/** * Load CSS for template-team.php */ function team_styles() { if ( is_page_template('template-team.php') ) wp_enqueue_style( 'team-template', get_stylesheet_directory_uri() . '/assets/css/team.css' ); } add_action( 'wp_enqueue_scripts', 'team_styles', 101 );

This will enqueue a CSS file ( /assets/css/team.css ) when template-team.php is in use. Using this method helps to keep your core stylesheet lean.

Example Styles

Here are the styles I’ve used for the example that accompanies this tutorial:

/* ============================================== Team profiles ============================================== */ .profiles { margin-bottom: -20px; } .intro { padding-left: 140px; } .intro h2 { margin: 0 0 7px; } .intro .lead { line-height: 120%; font-size: 1.1em; font-style: italic; margin: 0 0 35px; } .profile { position: relative; margin: 0 0 20px; } .profile:nth-child(even) { clear: left; } .profile-header { position: absolute; top: 0; } .profile-header img { float: left; } .profile-content { font-size: 14px; padding: 27px 20px 0 0; line-height: 1.4em; margin: 0 0 0 125px; } .profile-content h3 { margin: 0; } .profile-content .lead { font-size: 1.3em; line-height: 100%; font-style: italic; margin: 3px 0 20px; } .profile-content:before { content: ''; width: 36px; height: 3px; background: #dededc; position: absolute; top: 0; } .profile-content p { margin: 0 0 10px; } .profile-footer { position: absolute; top: 121px; width: 100px; text-align: center; } .profile-footer a { line-height: 18px; margin: 0 3px; display: inline-block; } .profile-footer a:hover i { color: #595959; } .profile-footer a:active i { color: #000; } .profile-footer i { font-size: 18px; position: relative; } .profile-footer i.icon-envelope { font-size: 16px; top: -1px; } .profile-footer i.icon-linkedin { font-size: 16px; top: -1px; }

In my project workflow I use LESS to pre-process my CSS. If you use would like to use LESS in your project, below is the CSS in LESS format.

/* ================================================== Team profiles ================================================== */ // Mixins .border-radius(@radius) { -webkit-border-radius: @radius; -moz-border-radius: @radius; border-radius: @radius; } // Global .profiles { margin-bottom: -20px; // Offset adjustment } .intro { padding-left: 140px; h2 { margin: 0 0 7px; } .lead { line-height: 120%; font-size: 1.1em; font-style: italic; margin: 0 0 35px; } } .profile { position: relative; margin: 0 0 20px; &:nth-child(even) { clear: left; } } // Header .profile-header { position: absolute; top: 0; img { float: left; } } // Content .profile-content { font-size: 14px; padding: 27px 20px 0 0; line-height: 1.4em; margin: 0 0 0 125px; h3 { margin: 0; } .lead { font-size: 1.3em; line-height: 100%; font-style: italic; margin: 3px 0 20px; } // Top separator &:before { content: ''; width: 36px; height: 3px; background: #dededc; position: absolute; top: 0; } p { margin: 0 0 10px; } } // Footer .profile-footer { position: absolute; top: 121px; width: 100px; text-align: center; a { line-height: 18px; margin: 0 3px; display: inline-block; } a:hover i { color: #595959; } a:active i { color: #000; } i { font-size: 18px; position: relative; } i.icon-envelope { font-size: 16px; top: -1px; } i.icon-linkedin { font-size: 16px; top: -1px; } }

Awesome, You Made It!

Thanks for reading, I hope this tutorial gives you a solid understanding of how to create a team bio/meet the team/team profile management system in WordPress. If you understand the underlying concepts explained in this article, you should now have a valuable new approach to managing custom content in WordPress.

Custom post types, taxonomies, and meta box managed custom fields provide a powerful approach to managing many complex WordPress CMS scenarios.