Creating Client Testimonials With Custom Post Types

Creating Client Testimonials With Custom Post Types

If you’re running any type of business online, it’s a good idea to get your clients’ opinions on what they thought about the services you provided.

First of all, this can benefit you by giving you feedback on how you can improve aspects of your business, but most of all, it can give you great testimonials, which can help to persuade potential clients to use your services.

The easiest way to add this functionality to your site would be to include it as a plugin. I’ve put together all the necessary files and included a download link for the Client Testimonials plugin above.


Testimonial Custom Post Type

Custom Post Types are great for separating your content according to different needs. Especially if your custom content doesn’t need all the bells and whistles of a straight-up post.

For this tutorial, I’m going to show you how you can quickly create a Custom Post Type for your testimonials that only requires the text editor and three custom meta boxes.

add_action( 'init', 'testimonials_post_type' );
function testimonials_post_type() {
	$labels = array(
		'name' => 'Testimonials',
		'singular_name' => 'Testimonial',
		'add_new' => 'Add New',
		'add_new_item' => 'Add New Testimonial',
		'edit_item' => 'Edit Testimonial',
		'new_item' => 'New Testimonial',
		'view_item' => 'View Testimonial',
		'search_items' => 'Search Testimonials',
		'not_found' =>  'No Testimonials found',
		'not_found_in_trash' => 'No Testimonials in the trash',
		'parent_item_colon' => '',
	);

	register_post_type( 'testimonials', array(
		'labels' => $labels,
		'public' => true,
		'publicly_queryable' => true,
		'show_ui' => true,
		'exclude_from_search' => true,
		'query_var' => true,
		'rewrite' => true,
		'capability_type' => 'post',
		'has_archive' => true,
		'hierarchical' => false,
		'menu_position' => 10,
		'supports' => array( 'editor' ),
		'register_meta_box_cb' => 'testimonials_meta_boxes', // Callback function for custom metaboxes
	) );
}

Adding a Metabox

Now that a Custom Post Type for your testimonials has been created and you’ve established a callback for the custom metaboxes, you need to set up how those metaboxes will be displayed. So next up you need to use the add_meta_box() function to do just that.

function testimonials_meta_boxes() {
	add_meta_box( 'testimonials_form', 'Testimonial Details', 'testimonials_form', 'testimonials', 'normal', 'high' );
}

function testimonials_form() {
	$post_id = get_the_ID();
	$testimonial_data = get_post_meta( $post_id, '_testimonial', true );
	$client_name = ( empty( $testimonial_data['client_name'] ) ) ? '' : $testimonial_data['client_name'];
	$source = ( empty( $testimonial_data['source'] ) ) ? '' : $testimonial_data['source'];
	$link = ( empty( $testimonial_data['link'] ) ) ? '' : $testimonial_data['link'];

	wp_nonce_field( 'testimonials', 'testimonials' );
	?>
	<p>
		<label>Client's Name (optional)</label><br />
		<input type="text" value="<?php echo $client_name; ?>" name="testimonial[client_name]" size="40" />
	</p>
	<p>
		<label>Business/Site Name (optional)</label><br />
		<input type="text" value="<?php echo $source; ?>" name="testimonial1" size="40" />
	</p>
	<p>
		<label>Link (optional)</label><br />
		<input type="text" value="<?php echo $link; ?>" name="testimonial[link]" size="40" />
	</p>
	<?php
}

There are three fields you should include when setting up the data for your testimonial: the client’s name, their business and a link to their site. Sometimes, you might not have all three but the least amount of information you should require is the client’s name.

Tip: Whenever you add a metabox, be sure to use a nonce to secure the form. It’s a must. Read more about nonces in the WordPress codex.

Saving the Custom Meta

Since you’ve added a custom metabox, you’ll need to make sure that all the data is validated and saved. You need to hook into the save_post action and set up a callback function.

add_action( 'save_post', 'testimonials_save_post' );
function testimonials_save_post( $post_id ) {
	if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
		return;

	if ( ! empty( $_POST['testimonials'] ) && ! wp_verify_nonce( $_POST['testimonials'], 'testimonials' ) )
		return;

	if ( ! empty( $_POST['post_type'] ) && 'page' == $_POST['post_type'] ) {
		if ( ! current_user_can( 'edit_page', $post_id ) )
			return;
	} else {
		if ( ! current_user_can( 'edit_post', $post_id ) )
			return;
	}

	if ( ! wp_is_post_revision( $post_id ) && 'testimonials' == get_post_type( $post_id ) ) {
		remove_action( 'save_post', 'testimonials_save_post' );

		wp_update_post( array(
			'ID' => $post_id,
			'post_title' => 'Testimonial - ' . $post_id
		) );

		add_action( 'save_post', 'testimonials_save_post' );
	}

	if ( ! empty( $_POST['testimonial'] ) ) {
		$testimonial_data['client_name'] = ( empty( $_POST['testimonial']['client_name'] ) ) ? '' : sanitize_text_field( $_POST['testimonial']['client_name'] );
		$testimonial_data['source'] = ( empty( $_POST['testimonial']['source'] ) ) ? '' : sanitize_text_field( $_POST['testimonial']['source'] );
		$testimonial_data['link'] = ( empty( $_POST['testimonial']['link'] ) ) ? '' : esc_url( $_POST['testimonial']['link'] );

		update_post_meta( $post_id, '_testimonial', $testimonial_data );
	} else {
		delete_post_meta( $post_id, '_testimonial' );
	}
}

Customizing the List View

After you’ve created your first testimonial, you’ll see it appear in the list view of your Custom Post Type; however, you won’t see any of the custom meta data.

That’s an easy fix: You just need to add a couple more functions to customize the list view columns so that all the info you want to see will appear.

add_filter( 'manage_edit-testimonials_columns', 'testimonials_edit_columns' );
function testimonials_edit_columns( $columns ) {
	$columns = array(
		'cb' => '<input type="checkbox" />',
		'title' => 'Title',
		'testimonial' => 'Testimonial',
		'testimonial-client-name' => 'Client\'s Name',
		'testimonial-source' => 'Business/Site',
		'testimonial-link' => 'Link',
		'author' => 'Posted by',
		'date' => 'Date'
	);

	return $columns;
}

add_action( 'manage_posts_custom_column', 'testimonials_columns', 10, 2 );
function testimonials_columns( $column, $post_id ) {
	$testimonial_data = get_post_meta( $post_id, '_testimonial', true );
	switch ( $column ) {
		case 'testimonial':
			the_excerpt();
			break;
		case 'testimonial-client-name':
			if ( ! empty( $testimonial_data['client_name'] ) )
				echo $testimonial_data['client_name'];
			break;
		case 'testimonial-source':
			if ( ! empty( $testimonial_data['source'] ) )
				echo $testimonial_data['source'];
			break;
		case 'testimonial-link':
			if ( ! empty( $testimonial_data['link'] ) )
				echo $testimonial_data['link'];
			break;
	}
}

That’s pretty much everything you need to set up testimonials in the WordPress admin. But what about displaying them on the front end? Let’s look at a few different ways to display your testimonials.


Display Testimonials

If you’d like to display a testimonial somewhere in one of your theme’s page templates, you’ll need to create a function to do so. Here’s a quick one that’ll allow you to display client testimonials. You can use the parameters to select one specific testimonial using an ID, or even display a random one by passing an ‘orderby’ value.

/**
 * Display a testimonial
 *
 * @param  int $post_per_page  The number of testimonials you want to display
 * @param  string $orderby  The order by setting  https://codex.wordpress.org/Class_Reference/WP_Query#Order_.26_Orderby_Parameters
 * @param  array $testimonial_id  The ID or IDs of the testimonial(s), comma separated
 *
 * @return  string  Formatted HTML
 */
function get_testimonial( $posts_per_page = 1, $orderby = 'none', $testimonial_id = null ) {
	$args = array(
		'posts_per_page' => (int) $posts_per_page,
		'post_type' => 'testimonials',
		'orderby' => $orderby,
		'no_found_rows' => true,
	);
	if ( $testimonial_id )
		$args['post__in'] = array( $testimonial_id );

	$query = new WP_Query( $args  );

	$testimonials = '';
	if ( $query->have_posts() ) {
		while ( $query->have_posts() ) : $query->the_post();
			$post_id = get_the_ID();
			$testimonial_data = get_post_meta( $post_id, '_testimonial', true );
			$client_name = ( empty( $testimonial_data['client_name'] ) ) ? '' : $testimonial_data['client_name'];
			$source = ( empty( $testimonial_data['source'] ) ) ? '' : ' - ' . $testimonial_data['source'];
			$link = ( empty( $testimonial_data['link'] ) ) ? '' : $testimonial_data['link'];
			$cite = ( $link ) ? '<a href="' . esc_url( $link ) . '" target="_blank">' . $client_name . $source . '</a>' : $client_name . $source;

			$testimonials .= '<aside class="testimonial">';
			$testimonials .= '<span class="quote">&ldquo;</span>';
			$testimonials .= '<div class="entry-content">';
			$testimonials .= '<p class="testimonial-text">' . get_the_content() . '<span></span></p>';
			$testimonials .= '<p class="testimonial-client-name"><cite>' . $cite . '</cite>';
			$testimonials .= '</div>';
			$testimonials .= '</aside>';

		endwhile;
		wp_reset_postdata();
	}

	return $testimonials;
}

Here’s the CSS that I use to style the testimonials.

.testimonial {
	padding-left: 60px;
	position: relative;
	z-index: 0;
	font-size: 16px;
	}

	aside.testimonial {

		}

	.testimonial .quote {
		position: absolute;
		left: 0;
		top: -25px;
		font-size: 300px;
		font-family: Georgia, serif;
		color: #f2f2f2;
		z-index: -1;
		line-height: 1;
		}

	.testimonial-text {
		font-style: italic;
		}

	.testimonial-client-name {
		text-align: right;
		font-size: 14px;
		}

		.testimonial-client-name cite {
			font-style: normal;
			}

Testimonials Shortcode

You might also want to display testimonials within your post or page content. That’s not a problem. All you need to do is hook into the WordPress Shortcode API.

add_shortcode( 'testimonial', 'testimonial_shortcode' );
/**
 * Shortcode to display testimonials
 *
 * [testimonial posts_per_page="1" orderby="none" testimonial_id=""]
 */
function testimonial_shortcode( $atts ) {
	extract( shortcode_atts( array(
		'posts_per_page' => '1',
		'orderby' => 'none',
		'testimonial_id' => '',
	), $atts ) );

	return get_testimonial( $posts_per_page, $orderby, $testimonial_id );
}

Testimonials Widget

Widgets are great. They’re easy to use and can add so much functionality to your site. So let’s set up a simple testimonials widget so you can display your client’s testimonials in any of your theme’s widgetized areas.

/**
 * Testimonials Widget
 */
class Testimonial_Widget extends WP_Widget {
	public function __construct() {
		$widget_ops = array( 'classname' => 'testimonial_widget', 'description' => 'Display testimonial post type' );
		parent::__construct( 'testimonial_widget', 'Testimonials', $widget_ops );
	}

	public function widget( $args, $instance ) {
		extract( $args );
		$title = apply_filters( 'widget_title', empty( $instance['title'] ) ? '' : $instance['title'], $instance, $this->id_base );
		$posts_per_page = (int) $instance['posts_per_page'];
		$orderby = strip_tags( $instance['orderby'] );
		$testimonial_id = ( null == $instance['testimonial_id'] ) ? '' : strip_tags( $instance['testimonial_id'] );

		echo $before_widget;

		if ( ! empty( $title ) )
			echo $before_title . $title . $after_title;

		echo get_testimonial( $posts_per_page, $orderby, $testimonial_id );

		echo $after_widget;
	}

	public function update( $new_instance, $old_instance ) {
		$instance = $old_instance;
		$instance['title'] = strip_tags( $new_instance['title'] );
		$instance['posts_per_page'] = (int) $new_instance['posts_per_page'];
		$instance['orderby'] = strip_tags( $new_instance['orderby'] );
		$instance['testimonial_id'] = ( null == $new_instance['testimonial_id'] ) ? '' : strip_tags( $new_instance['testimonial_id'] );

		return $instance;
	}

	public function form( $instance ) {
		$instance = wp_parse_args( (array) $instance, array( 'title' => '', 'posts_per_page' => '1', 'orderby' => 'none', 'testimonial_id' => null ) );
		$title = strip_tags( $instance['title'] );
		$posts_per_page = (int) $instance['posts_per_page'];
		$orderby = strip_tags( $instance['orderby'] );
		$testimonial_id = ( null == $instance['testimonial_id'] ) ? '' : strip_tags( $instance['testimonial_id'] );
		?>
		<p><label for="<?php echo $this->get_field_id( 'title' ); ?>">Title:</label>
		<input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>" /></p>

		<p><label for="<?php echo $this->get_field_id( 'posts_per_page' ); ?>">Number of Testimonials: </label>
		<input class="widefat" id="<?php echo $this->get_field_id( 'posts_per_page' ); ?>" name="<?php echo $this->get_field_name( 'posts_per_page' ); ?>" type="text" value="<?php echo esc_attr( $posts_per_page ); ?>" />
		</p>

		<p><label for="<?php echo $this->get_field_id( 'orderby' ); ?>">Order By</label>
		<select id="<?php echo $this->get_field_id( 'orderby' ); ?>" name="<?php echo $this->get_field_name( 'orderby' ); ?>">
			<option value="none" <?php selected( $orderby, 'none' ); ?>>None</option>
			<option value="ID" <?php selected( $orderby, 'ID' ); ?>>ID</option>
			<option value="date" <?php selected( $orderby, 'date' ); ?>>Date</option>
			<option value="modified" <?php selected( $orderby, 'modified' ); ?>>Modified</option>
			<option value="rand" <?php selected( $orderby, 'rand' ); ?>>Random</option>
		</select></p>

		<p><label for="<?php echo $this->get_field_id( 'testimonial_id' ); ?>">Testimonial ID</label>
		<input class="widefat" id="<?php echo $this->get_field_id( 'testimonial_id' ); ?>" name="<?php echo $this->get_field_name( 'testimonial_id' ); ?>" type="text" value="<?php echo $testimonial_id; ?>" /></p>
		<?php
	}
}

add_action( 'widgets_init', 'register_testimonials_widget' );
/**
 * Register widget
 *
 * This functions is attached to the 'widgets_init' action hook.
 */
function register_testimonials_widget() {
	register_widget( 'Testimonial_Widget' );
}

Testimonials Archive Page Template

Since testimonials require custom meta, you can’t rely on the default archive page template to display them correctly. In order to set up a custom archive page, you need to create a file called archive-testimonials.php and add it to your theme’s main folder.

<?php
/**
 * Archive template for client testimonials
 */

get_header(); ?>

	<section id="primary" class="site-content">

		<div id="content" role="main">
			<header class="archive-header">
				<h1 class="archive-title">Testimonials</h1>
			</header><!-- #archive-header -->

			<?php while ( have_posts() ) : the_post();
				$testimonial_data = get_post_meta( get_the_ID(), '_testimonial', true );
				$client_name = ( empty( $testimonial_data['client_name'] ) ) ? '' : $testimonial_data['client_name'];
				$source = ( empty( $testimonial_data['source'] ) ) ? '' : ' - ' . $testimonial_data['source'];
				$link = ( empty( $testimonial_data['link'] ) ) ? '' : $testimonial_data['link'];
				$cite = ( $link ) ? '<a href="' . esc_url( $link ) . '" target="_blank">' . $client_name . $source . '</a>' : $client_name . $source;
				?>

				<article id="post-<?php the_ID(); ?>" <?php post_class( 'testimonial' ); ?>>
					<span class="quote">&ldquo;</span>
					<div class="entry-content">
						<p class="testimonial-text"><?php echo get_the_content(); ?><span></span></p>
						<p class="testimonial-client-name"><cite><?php echo $cite; ?></cite></p>
					</div>
				</article>

			<?php endwhile; ?>

			<?php
			global $wp_query;

			if (  1 < $wp_query->max_num_pages ) : ?>
				<nav class="archive-navigation" role="navigation">
					<div class="nav-previous alignleft"><?php next_posts_link( '<span class="meta-nav">&larr;</span> Older posts' ); ?></div>
					<div class="nav-next alignright"><?php previous_posts_link( 'Newer posts <span class="meta-nav">&rarr;</span>' ); ?></div>
				</nav><!-- .archive-navigation -->
				<?php
			endif;
			?>
		</div>

	</section><!-- #primary -->

<?php get_sidebar(); ?>
<?php get_footer(); ?>

Conclusion

Hopefully you won’t feel too overwhelmed by the amount of code above. You might not actually have to use it all since it really depends on what your needs will be. You might only need the shortcode or just the archive template. Either way, going through this tutorial should prepare you for many situations that you might encounter when adding client testimonials to your site.

If you have any comments or feedback on anything you read above, please feel free to discuss it below.

Note: Want to add some source code? Type <pre><code> before it and </code></pre> after it. Find out more
  • http://laranz.in/ lawrence77

    At least a demo screenshot??

    • c.bavota
      Author

      The default design in TwentyTwelve would look like this:

      • http://laranz.in/ lawrence77

        Nice… Add it to the tutorial, everyone don’t check the comments :)

  • Wesley

    Hi great tutorial,

    Just 1 thing, where does the code go for the following:

    Testimonial Custom Post Type,

    Adding a Metabox,

    Saving the Custom Meta,
    Customizing the List View,

    Display Testimonials,

    Testimonials Shortcode,
    Testimonials Widget

    does it all go in the functions.php?

    • c.bavota
      Author

      You can add all of those to functions.php. The widget code can either go in its own file or in functions.php. Totally up to you. Just remember to include the file using PHP if you actually separate it.

      Through the link at the top of the tutorial you can download a plugin which would give you a good idea of the structure used to lay it all out.

  • Pingback: Wptuts+: Creating Client Testimonials With Custom Post Types | bavotasan.com

  • http://twitter.com/remicorson Rémi Corson

    Clean, nice, and simple! Thanks!

  • Pingback: Tweet Parade (no.11 Mar 2013) | gonzoblog

  • Donald Pipowitch

    Nice tut, but please add screenshots to this post, like lawrence77 mentioned. I mainly read this artices with a RSS reader and don’t see comments by default.

  • http://www.mathewporter.co.uk/ Matt Porter

    Thats a great post. When i have created my own shortcodes in the past on a custom theme, I have created a ‘shortcodes.php’ or similar for these.

  • http://twitter.com/swisslayer Marko Bolliger

    thx!

  • http://www.studio89a.com/ Jason Rogers

    What about adding rich snippets to this code?

  • http://www.facebook.com/pham.d.nghia11 Nghia Kades

    Great tutorial, but why don’t you add roles for subscriber, or create another user type and let them write their testimonials themselves.

    $wp_roles->add_cap(‘subcriber’,”read_{$capability_type}”);
    $wp_roles->add_cap(‘subcriber’,”edit_{$capability_type}s”);
    $wp_roles->add_cap(‘subcriber’,”edit_published_{$capability_type}s”);

    i think your post type is more useful with this (sorry for my bad english)

  • tballard

    Or you could use TB-Testimonials its pretty awesome ;)

  • Pingback: Best Wordpress Tutorials Which Will Make Your Life Easy - Webkia

  • Akeisa Lowe

    Does this plugin have a section for a feedback paragraph? I see name, business, and site url but no text area for the client’s feed back -_- Please, help.

  • Akeisa Lowe

    I’ve created an archive-testimonials.php file pasted the template code inside…but when I view a testimonial the page is blank and the url reads: “http://website.com/test/testimonials/testimonial-1669″.

    What can I do to get the pages working and display testimonials…?

  • Akeisa Lowe

    One more question…how can I add a featured image to each testimonial?

  • Nixon Parker

    Nice tutorial but if you can share demo screenshots, then it will be great for me.

  • http://www.facebook.com/jochie.nabuurs Jochie Nabuurs

    There is a bug in the code when copy paste:
    The business field is not saved. The name of the input is testimonial1 where it should be testimonial

  • http://www.facebook.com/jochie.nabuurs Jochie Nabuurs

    Below are some screenshots of the backend as requested by some users. I don’t have the frontend code up and running so i can’t show that. But i suppose with some imagination you could figure that out yourself.

  • http://www.facebook.com/engarifctg Mohammed Arifuzzaman

    Please help me…I want to show only “Client Name” and tried with
    get_post_meta( get_the_ID(), ‘client_name’, true )
    but not working….what did i do wrong?

  • Waqas Hasan

    Great work :-)

  • http://www.facebook.com/people/Eric-Banico/100000426677538#!/profile.php?id=100000426677538 eric

    Another Awesome tutorial! is it possible to add a captcha on this without using any plugins? thanks ..