Video icon 64
Learn to Code. Start your free trial today.
Advertisement

Integrating Google Rich Snippets Into a WordPress Theme

by
Student iconAre you a student? Get a yearly Tuts+ subscription for $45 →

We're all familiar with the way Google presents search results – with a page title and a little snippet of text for each result. With Google Rich Snippets we can add useful information to the web search result snippet to make it stand out from other results and ultimately attract more visitors. While there are already plugins that provide this kind of functionality in WordPress, there are situations when relaying on a third party plugin is not advisable. In this tutorial, we are going to integrate the microdata format into WordPress theme markup to display a culinary recipe, and make it compatible with Google Rich Snippets' requirements.


Introduction to Google Rich Snippets

Let's take a look at an example of a rich snippet:

I've highlighted for you the snippets of additional information that Google "reads" from the page. As you can see, rich snippets add some really useful information to search engine results. In case of recipes that information includes a photo, recipe rating, calories and total time it takes to prepare the dish. All this additional information gives users much better sense of the content on the page, and makes it more likely that users will click on the link and visit your website.

The snippets for each content type look slightly different and provide information relevant to the specific content type.

How to Enable Rich Snippets?

The secret behind rich snippets is structured, semantic markup, that allows Google to understand the page's content. So basically, all you have to do is properly markup your content to describe the particular type of information on your website. In this tutorial we will be focusing on a culinary recipe, but Google supports rich snippets for a number of other content types, namely:

  • Reviews
  • People
  • Products
  • Businesses and Organizations
  • Events
  • Music

For more information on rich snippets and content types, visit the Google Help Center.

When it comes to marking up your content, there are three markup formats to choose from:

  • microdata
  • microformats
  • RDFa

In this tutorial, we'll be integrating microdata markup with schema.org properties, as recommended in Google rich snippets' documentation. It's worth noting, that the schema.org vocabulary is recognized not only by Google, but also by other major search providers – Yahoo! and Microsoft.

Visit Schema.org for more information and examples on how to implement it in your code.


Step 1 Creating Recipe Custom Post Type

Since we will be writing quite a lot of code, we'll create a separate file called recipe-config.php, to hold all our snippets, and include it using the PHP function include. To do that, open the functions.php file in your current theme directory, and paste the following piece of code at the end:

include('recipe-config.php');

Now create a new file called recipe-config.php. All the code that follows should be added to that file.

Let's start by creating a new Custom Post Type called Recipe.

add_action( 'init', 'register_my_culinary_recipe' );
  
function register_my_culinary_recipe() {
    $labels = array(
        'name'               => _x( 'Recipes', 'culinary_recipes' ),
        'singular_name'      => _x( 'Recipe', 'culinary_recipes' ),
        'add_new'            => _x( 'Add New', 'culinary_recipes' ),
        'add_new_item'       => _x( 'Add New Recipe', 'culinary_recipes' ),
        'edit_item'          => _x( 'Edit Recipe', 'culinary_recipes' ),
        'new_item'           => _x( 'New Recipe', 'culinary_recipes' ),
        'view_item'          => _x( 'View Recipe', 'culinary_recipes' ),
        'search_items'       => _x( 'Search Recipes', 'culinary_recipes' ),
        'not_found'          => _x( 'No Recipes found', 'culinary_recipes' ),
        'not_found_in_trash' => _x( 'No recipes found in Trash', 'culinary_recipes' ),
        'parent_item_colon'  => '',
        'menu_name'          => _x( 'Recipes', 'culinary_recipes' )
    );
	$args = array(
        'labels'              => $labels,
        'public'              => true,
        'publicly_queryable'  => true,
        'show_ui'             => true,
        'show_in_menu'        => true,
        'show_in_nav_menus'   => true,
        'exclude_from_search' => false,
        'hierarchical'        => false,
        'has_archive'         => true,
        'rewrite'             => array('slug' => 'recipe')
	);
	register_post_type( 'my_culinary_recipe', $args );  
}

Now if you go to the admin area, there should be a new option in the menu called "Recipes". Don't add any recipes just yet, because we need to add some custom meta boxes first.


Step 2 Adding Custom Meta Boxes

The Setup

Because we'll need quite a few custom meta boxes of various types to store all the recipe specific data, I'm going to use the free Custom Meta Boxes and Fields for WordPress library to create them. Of course you could use any other script or create meta boxes from scratch, if you prefer.

Wptuts+ has a great tutorial on the topic of Reusable Custom Meta Boxes

First, we need to download the library from GitHub. As the author suggests, we will store all the script files in 'lib/metabox' folder. So start with creating the 'lib' folder in your theme or child theme, then add the 'metabox' folder inside 'lib'. Unpack and upload all downloaded files to '/wp-content/themes/my-theme/lib/metabox'.

Finally, we need to include the file init.php. Normally you'd include it in your functions.php file but we'll do it in our recipe-config.php, since that's where we store all the recipe specific functions.

function be_initialize_cmb_meta_boxes() {
    if ( !class_exists( 'cmb_Meta_Box' ) ) {
		require_once( 'lib/metabox/init.php' );
    }
}
add_action( 'init', 'be_initialize_cmb_meta_boxes', 9999 );

Once it's done, we can start defining meta boxes.

Defining Meta Boxes

To qualify for Google Rich Snippets, we don't need to provide all properties included in the specification, although each content type has a required minimum. In this tutorial, we are going to incorporate the following properties:

  • name
  • recipeCategory
  • image
  • description
  • ingredients
  • instructions
  • recipeYield
  • prepTime
  • cookTime
  • totalTime
  • datePublished
  • author

Note that we won't have to create custom meta boxes for all the properties. For example, totalTime will be calculated based on prepTime and cookTime.

Let's add some custom meta boxes, shall we?

$prefix = 'mcr_'; // Prefix for all fields

function mcr_create_metaboxes( $meta_boxes ) {
	global $prefix;
	$meta_boxes[] = array(
		'id'         => 'recipe-data',
		'title'      => 'Culinary Recipe',
		'pages'      => array('my_culinary_recipe'),
		'context'    => 'normal',
		'priority'   => 'high',
		'show_names' => true,
		'fields'     => array(
			//TITLE - TEXT
			array(
				'name' => __( 'Recipe Title', 'culinary_recipes' ),
				'id'   => $prefix . 'name',
				'type' => 'text',
			),
			//RECIPE TYPE - TEXT
			array(
				'name' => __( 'Recipe Type', 'culinary_recipes' ),
				'desc' => __( 'The type of dish: for example, appetizer, entree, dessert, etc.', 'culinary_recipes' ),
				'id'   => $prefix . 'type',
				'type' => 'text_medium',
			),
			// IMAGE UPLOAD
			array(
				'name'    => 'Recipe Image',
				'desc'    => 'Image of the dish being prepared.',
				'id'      => $prefix . 'image',
				'type'    => 'file',
				'save_id' => false, // save ID using true
				'allow'   => array('url', 'attachment') // limit to just attachments with array( 'attachment' )
			),
			//SUMMARY - TEXT
			array(
				'name' => __( 'Summary', 'culinary_recipes' ),
				'desc' => __( 'A short summary describing the dish.', 'culinary_recipes' ),
				'id'   => $prefix . 'summary',
				'type' => 'text',
			),
			//INGREDIENTS - TEXTAREA
			array(
				'name' => __( 'Ingredients', 'culinary_recipes' ),
				'desc' => __( 'Put each ingredient in seaprate line.', 'culinary_recipes' ),
				'id'   => $prefix . 'ingredients',
				'type' => 'textarea',
			),
			//DIRECTIONS - TEXTAREA
			array(
				'name' => __( 'Instructions', 'culinary_recipes' ),
				'desc' => __( 'Put each instruction in seaprate line.', 'culinary_recipes' ),
				'id'   => $prefix . 'instructions',
				'type' => 'textarea',
			),
			//YIELD - TEXT
			array(
				'name' => __( 'Yield', 'culinary_recipes' ),
				'desc' => __( 'Enter the number of servings or number of people served', 'culinary_recipes' ),
				'id'   => $prefix . 'yield',
				'type' => 'text_medium',
			),
			//PREP TIME - TITLE
			array(
				'name' => __( 'Prep time', 'culinary_recipes' ),
				'desc' => __( 'How long does it take to prep?', 'culinary_recipes' ),
				'type' => 'title',
				'id'   => $prefix . 'prep_title'
			),
			//PREP TIME HOURS - NUMBER
			array(
				'name' => __( 'Hours', 'culinary_recipes' ),
				'id'   => $prefix . 'prep_time_hours',
				'type' => 'number',
				'std'  => '0',
			),
			//PREP TIME MINUTES- NUMBER
			array(
				'name' => __( 'Minutes', 'culinary_recipes' ),
				'id'   => $prefix . 'prep_time_minutes',
				'type' => 'number',
				'std'  => '0',
			),
			//COOK TIME - TITLE
			array(
				'name' => __( 'Cooking time', 'culinary_recipes' ),
				'desc' => __( 'Total time of cooking, baking etc.', 'culinary_recipes' ),
				'type' => 'title',
				'id'   => $prefix . 'coking_title'
			),
			//COOKING TIME - TEXT
			array(
				'name' => __( 'Hours', 'culinary_recipes' ),
				'id'   => $prefix . 'cook_time_hours',
				'type' => 'number',
				'std'  => '0',
			),
			//COOKING TIME - TEXT
			array(
				'name' => __( 'Minutes', 'culinary_recipes' ),
				'id'   => $prefix . 'cook_time_minutes',
				'type' => 'number',
				'std'  => '0',
			)
		)
	);

	return $meta_boxes;
}

add_filter( 'cmb_meta_boxes' , 'mcr_create_metaboxes' );

With this piece of code we've created a meta box called "Culinary Recipe" that will show only on the Recipes post type edit screen.

The actual field definitions are stored as an array in the 'fields' property. Let's take a closer look:

array(
	'name' => __('Summary', 'culinary_recipes'),
	'desc' => __('A short summary describing the dish.', 'culinary_recipes'),
	'id' => $prefix .'summary',
	'type' => 'text',
),

Adding a new field is as easy as copying one of the array elements (shown above), and changing values for 'name', 'id', 'desc' and 'type'. The Custom Metaboxes and Fields library offers a number of predefined field types, and a convenient method to define your own.

To facilitate separate input of hours and minutes for cook and prep time, I defined our own field type called 'number'. I utilized one of HTML5's new input types – number, and created a simple validation function, casting integer type on the value supplied by the user.

add_action( 'cmb_render_number', 'rrh_cmb_render_number', 10, 2 );
function rrh_cmb_render_number( $field, $meta ) {
	echo '<input type="number" min="0" max="60" class="cmb_text_inline" name="', $field['id'], '" id="', $field['id'], '" value="', '' !== $meta ? $meta : $field['std'], '" />','<p class="cmb_metabox_description">', $field['desc'], '</p>';
}
add_filter( 'cmb_validate_number', 'rrh_cmb_validate_number' );

function rrh_cmb_validate_number( $new ) {
	return (int)$new;
}

Step 3 Displaying the Recipe

Now we are finally ready to write some markup. We could create a separate template file for our custom post type and place the markup directly in that template. Instead, we will be putting all the markup inside a function, and appending it to the post content with the_content() filter. This is important, because there are many plugins that add some kind of content, e.g. social media buttons, to the end of the post. This way we make sure, that all the plugin output is displayed below the recipe.

function mcr_display_recipe($content) {

	global $post;
	$recipe = '';

	if ( is_singular( 'my_culinary_recipe' ) ) {
		$recipe .= '<div class="recipe">';
			$recipe .= '<div itemscope itemtype="http://schema.org/Recipe" >';
				$recipe .= '<h2 itemprop="name">'. get_post_meta($post->ID,'mcr_name',true) .'</h2>';
				$recipe .= '<img class="alignright" itemprop="image" src="'. get_post_meta($post->ID,'mcr_image',true) .'" />';
				$recipe .= '<span class="mcr_meta"><b>Recipe type:</b> <time itemprop="recipeCategory">'. get_post_meta($post->ID,'mcr_type',true) .'</time></span>';
				$recipe .= '<span class="mcr_meta"><b>Yield:</b> <span itemprop="recipeYield">'. get_post_meta($post->ID,'mcr_yield',true) .'</span></span>';
				$recipe .= '<span class="mcr_meta"><b>Prep time:</b> <time content="'. mcr_time('prep','iso') .'" itemprop="prepTime">'. mcr_time('prep') .'</time></span>';
				$recipe .= '<span class="mcr_meta"><b>Cook time:</b> <time content="'. mcr_time('cook','iso') .'" itemprop="cookTime">'. mcr_time('cook') .'</time></span>';
				$recipe .= '<span class="mcr_meta"><b>Total time:</b> <time content="'. mcr_total_time('iso') .'" itemprop="totalTime">'. mcr_total_time() .'</time></span>';
				$recipe .= '</br>';
				$recipe .= '<hr />';
				$recipe .= '<span itemprop="description">'. get_post_meta($post->ID,'mcr_summary',true) .'</span><br />';
				$recipe .= '<h3>Ingredients:</h3> '. mcr_list_items('ingredients');
				$recipe .= '<h3>Directions:</h3> '. mcr_list_items('instructions');
				$recipe .= '<span class="mcr_meta">Published on <time itemprop="datePublished" content="'. get_the_date('Y-m-d') .'">'. get_the_date('F j, Y') .'</time></span>';
				$recipe .= '<span class="mcr_meta">by <span itemprop="author">'. get_the_author() .'</span></span>';
			$recipe .= '</div>';
		$recipe .= '</div>';
	}

	return $content . $recipe;
}
add_filter('the_content', 'mcr_display_recipe', 1);

Let's get over the code. First, we pull the global $post object, which gives us access to various useful information about the post being displayed.

Then we use the conditional tag is_singular() to check if a single post of the type 'my_culinary_recipe' is currently being displayed. This is because we didn't create a separate template for our custom post type and thus WordPress is using the more general single.php template (or index.php if there's no single.php) to display the recipe. Using the if statement we make sure that recipe markup won't be displayed on regular posts.

Finally, we retrieve the recipe data using the get_post_meta() function, and place it inside the markup structured according to the microdata format.

Helper Functions

You might notice that I used some additional functions – mcr_time(), mcr__total_time() and mcr_list_items() to retrieve and prepare the data for display. Let's take a look!

Time related properties (prepTime, cookTime and totalTime) expect values in ISO 8601 duration format. To account for that, both of our time related functions will take a format as a parameter, and prepare output accordingly.

function mcr_time($type = 'prep', $format = null) {

	global $post;

	$hours = get_post_meta($post->ID,'mcr_'.$type.'_time_hours',true);
	$minutes = get_post_meta($post->ID,'mcr_'.$type.'_time_minutes',true);
	$time = '';
	if ($format == 'iso') {
		if ($hours > 0) {
			$time = 'PT'.$hours.'H';
			if($minutes > 0) {
				$time .= $minutes.'M';
			}
		}
		else {
			$time = 'PT'.$minutes.'M';
		}
	}
	else {
		if ($hours > 0) {
			if ($hours == 1) {
				$time = $hours.' hour ';
			}
			else {
				$time = $hours.' hrs ';
			}
			if ($minutes > 0) {
				$time .= $minutes.' mins';
			}
		}
		else {
			$time = $minutes.' mins';
		}
	}
	return $time;
}

The mcr_time() function prepares output for cook and prep times, it accepts two parameters:

  • $type (required) is the type of time we want to display. Accepts two values – 'prep' (default) or 'cook'
  • $format (optional) – indicates that output should be formatted according to ISO 8601 duration format. Accepts only one value – 'iso'.
function mcr_total_time($format = null) {

	global $post;
	$prep_hours = get_post_meta($post->ID,'mcr_prep_time_hours',true);
	$prep_minutes = get_post_meta($post->ID,'mcr_prep_time_minutes',true);
	$cook_hours = get_post_meta($post->ID,'mcr_cook_time_hours',true);
	$cook_minutes = get_post_meta($post->ID,'mcr_cook_time_minutes',true);
	$total_minutes = ($prep_hours + $cook_hours)*60 + $prep_minutes + $cook_minutes;
	$hours = 0;
	$minutes = 0;

	if ($total_minutes >= 60) {
		$hours = floor($total_minutes / 60);
		$minutes = $total_minutes - ($hours * 60);
	}
	else {
		$minutes = $total_minutes;
	}
	$total_time = '';
	if ($format == 'iso') {
		if ($hours > 0 ) {
			$total_time = 'PT'.$hours.'H';
			if ($minutes > 0) {
				$total_time .= $minutes.'M';
			}
		}
		else {
			$total_time = 'PT'.$minutes.'M';
		}
	}
	else {
		if ($hours > 0 ) {
			if ($hours == 1) {
				$total_time = $hours.' hour ';
			}
			else {
				$total_time = $hours.' hrs ';
			}
			if ($minutes > 0) {
				$total_time .= $minutes.' mins';
			}
		}
		else {
			$total_time = $minutes.' mins';
		}
	}
	return $total_time;
}

The mcr_total_time() function calculates and prepares output for recipe total time. Accepts only one parameter – $format, analogous to the $format parameter in the mcr_time() function.

The last helper function displays lists of items – ingredients or instructions, according to the $type parameter.

function mcr_list_items($type = 'ingredients') {

	global $post;

	if (get_post_meta($post->ID, 'mcr_'. $type, true)) {
		$get_items = get_post_meta($post->ID, 'mcr_'. $type, true);
		$items = explode("\r", $get_items);
		$list = '';
	}
	else {
		return;
	}
	if ($type=='ingredients') {
		$list .= '<ul>';
		foreach ($items as $item) {
			$list .= '<li><span itemprop="ingredients">' . trim($item) . '</span></li>';
		}
		$list .= '</ul>';
	}
	elseif ($type=='instructions') {
		$list .= '<ol itemprop="recipeInstructions">';
		foreach ($items as $item) {
			$list .= '<li>' . trim($item) . '</li>';
		}
		$list .= '</ol>';
	}
	else {
		$list .= 'Invalid list type.';
	}
	return $list;
}

Now it's time to add some content. Navigate to the Recipes section in the administration area, and add a recipe. The output might need some styling, but if you view the post, you should see the recipe below regular content.

That's it! The only thing left, is to check if our markup is correct with the rich snippet testing tool from Google.

This is the rich snippet preview generated from our HTML markup:

You can test your markup by supplying either a URL or a code snippet to the testing tool.

Once you've added rich snippets markup, wait for the Google crawler to discover it. When Google notices the new markup, it should start displaying rich snippets for your website in search results. You can also submit a request form, to tell Google about rich snippets on your website, but you should give it some time first.


Conclusion

In this tutorial I showed you how to integrate a microdata format with a schema.org vocabulary to display culinary recipes. This example should serve you as a blueprint that you can use to enable rich snippets for other content types. Have you used Google Rich Snippets for anything in your projects? Let us know in the comments below.

Advertisement