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' );
		<label>Client's Name (optional)</label><br />
		<input type="text" value="<?php echo $client_name; ?>" name="testimonial[client_name]" size="40" />
		<label>Business/Site Name (optional)</label><br />
		<input type="text" value="<?php echo $source; ?>" name="testimonial[source]" size="40" />
		<label>Link (optional)</label><br />
		<input type="text" value="<?php echo $link; ?>" name="testimonial[link]" size="40" />

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 )

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

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

	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':
		case 'testimonial-client-name':
			if ( ! empty( $testimonial_data['client_name'] ) )
				echo $testimonial_data['client_name'];
		case 'testimonial-source':
			if ( ! empty( $testimonial_data['source'] ) )
				echo $testimonial_data['source'];
		case 'testimonial-link':
			if ( ! empty( $testimonial_data['link'] ) )
				echo $testimonial_data['link'];

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
 * @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>';


	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><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>

		<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>

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.

 * 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>

			<?php endwhile; ?>

			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 -->

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

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


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.

Related Posts
  • Code
    Theme Development
    Custom Controls in the Theme CustomizerTheme customizer custom control 400
    In the last article, we explored the advanced controls available in the Theme Customizer, and how to implement them. We’re going to look at how to create our own custom control, allowing you to choose which Category of Posts are displayed on the home page. To get started, download version 0.6.0 of our Theme Customizer Example.Read More…
  • Code
    Creating a Photo Tag Wall With Twilio Picture Messaging & PHPProcedural to oop php retina preview
    Twilio's recently announced Picture Messaging has vastly opened up what we can do with text messaging, now we can attach photos to our text messages and have them get used in different ways. In our case, we are going to build a Photo Tag Wall, which will contain photos linked to tags that will be displayed on a website.Read More…
  • Code
    Using HighCharts in WP-AdminHighcharts 400
    Charts are a great way to present data. They make data more digestible by making it visually appealing. In WordPress, there is no built-in method for getting posts and pages data in a graphical form. Although, there are certain plugins available which integrate Google Analytics with WordPress, but they are overkill if you want to get only a portion of that data. Also, nothing should keep you from learning new techniques and to dive straight into the subject is the best way to learn.Read More…
  • Code
    A Better Forum List Widget for bbPressBbpress
    When bbPress was still a standalone installation, I had tried it out and wasn't really impressed. Things were clunky and it didn't always work the way it was supposed to. After languishing for a few years, Automattic decided to take bbpress and turn it into a plugin, improving the functionality leaps and bounds and making it a strong contender amongst other forum option for WordPress.Read More…
  • Code
    Making the Best of Google AdSense in WordPressGoogleadsensepreview
    Blog monetization is not a "must", but it's a very important source of motivation. Whether you're blogging alone or along with some authors you gathered, earning even a few bucks a month can change your and/or your authors' approach to your blog. Since Google AdSense is one of the easiest and most popular ways for blog monetization, we're going to see how to use it with a WordPress blog with multiple authors. (Although, this tutorial will also work for single bloggers.) We'll be covering how to set up profile fields for authors' AdSense ads and how to display those ads with a function, with a widget, with a shortcode and automatically inside posts.Read More…
  • Code
    Creating Responsive Pricing Table Plugin for WordPressPricing table plugin
    Pricing tables are a key component of your business that promotes your products and helps users choose between different services you have. Most modern commercial WordPress themes provide built in Pricing Tables. There are also plenty of free and commercial pricing table plugins for WordPress. This tutorial is intended to provide knowledge to WordPress developers on creating a plugin from scratch which enables customization in different projects. Every web design is trying to accomplish responsive features which enable better look and feel on any kind of device. The pricing tables created with this plugin will work on all kinds of devices such as mobiles and tablets as well. So let's get started.Read More…