WordPress
Article

Creating a Post Series Plugin for WordPress

By Narayan Prusty

A WordPress Post Series plugin enables you to organize your posts serially to create a book or a course. It provides users a path for learning. Posts series plugins can also be used to split a long post into multiple parts.

In this tutorial, I’ll show you how to create a plugin for displaying a series of posts. You can also integrate the same code into a theme, as theme functionality.

Taxonomies Versus Post Series

In WordPress, taxonomies are used to group or organize similar posts together. But WordPress doesn’t provide a way to display all the posts of a particular taxonomy in a customized, serial manner. WordPress taxonomies are displayed using an archive.php file, so we cannot create a post series as a single, indexable post.

So we need a post series, which is actually one post that contains other posts in a serial manner.

How to Create a Post Series

There are many different ways to create a post series. Popular post series plugins found at WordPress.org use custom taxonomies on WordPress posts to create a post series, but in this tutorial I’ll use Custom Post Types instead.

Plugin File Structure

Create a plugin directory named sitepoint-post-series and place two files in this, named sitepoint-post-series.php and sitepoint-post-series.css.

In the sitepoint-post-series.php file, place the code below, so that WordPress recognizes the directory as a plugin and lets you install it.

<?php

/*
Plugin Name: SitePoint Post Series
Plugin URI: http://sitepoint.com/
Description: This used is used to create a post series.
Version: 1.0
Author: Narayan Prusty
*/

You can also add post series functionality to a theme. In this case, you will need to place all the code referred to in this tutorial, in the theme’s functions.php file.

How to Create a Post Series Custom Post Type

First, we need to create a custom post type, where each custom post type represents a post series.

Place the code below in a file called sitepoint-post-series.php:

function sitepoint_post_series_custom_post_type()
{
	register_post_type("sitepoint-postseries", array(
			"labels" => array("name" => __("Post Series"), "singular_name" => __("Post Series")),
			"public" => true, 
			"has_archive" => true,
			"rewrite" => array("slug"=> "post-series"),
			"supports" => array("editor", "title", "excerpt", "thumbnail", "comments"),
			"capability_type" => "post",
			"publicly_queryable" => true,
			"taxonomies" => array("category", "post_tag"),
		)
	);
}	

add_action("init", "sitepoint_post_series_custom_post_type", 2);

/* Flush Rewrite Rules */

function sitepoint_post_series_activation()
{
	sitepoint_post_series_custom_post_type();
	flush_rewrite_rules();
}

register_activation_hook( __FILE__, "sitepoint_post_series_activation");
register_deactivation_hook( __FILE__, "sitepoint_post_series_activation");

Here, we created a custom post type with the same taxonomies that are used by WordPress posts. This is so that you can create a category post series too.

We also added activation and deactivation hooks to flush rewrite rules. This is so that the post series can be viewed on the front end.

Here is what our custom post type looks on the admin screen:

Post Series

Adding a Post Series Meta Box to Posts

Now we need to add meta boxes to the WordPress Posts admin interface. This is so that authors can attach a post to a post series, and provide a serial number to sort the posts inside a post series.

Here is the code to add a meta box to post series:

/* Add Custom Meta Boxes in WordPress Posts */

function sitepoint_post_series_meta_box_markup($object)
{
	wp_nonce_field(basename(__FILE__), "sitepoint-postseries");

	?>
		<div>
			<label for="sitepoint-postseries-serial-number">Serial Number</label>
            <br>
            <input name="sitepoint-postseries-serial-number" type="text" value="<?php echo get_post_meta($object->ID, "sitepoint-postseries-serial-number", true); ?>">

            <br>

            <label for="sitepoint-postseries-id">Name</label>
            <br>
            <select name="sitepoint-postseries-id">
            	<option value="">-</option>
            	<?php
            		$posts = get_posts("post_type=sitepoint-postseries");
            		$selected_series = get_post_meta($object->ID, "sitepoint-postseries-id", true);
            		foreach($posts as $post) 
            		{
            			$id_post = $post->ID; 
            			if($id_post == $selected_series)
            			{
	            			?>
	            				<option selected value="<?php echo $post->ID; ?>"><?php echo $post->post_title; ?></option>	
	            			<?php
            			}
            			else
            			{
	            			?>
	            				<option value="<?php echo $post->ID; ?>"><?php echo $post->post_title; ?></option>	
	            			<?php	
            			}
		            }
            	?>	
            </select>
        </div>
	<?php
}

function sitepoint_post_series_custom_meta_box()
{
	add_meta_box("sitepoint-postseries", "Post Series", "sitepoint_post_series_meta_box_markup", "post", "side", "low", null);
}

add_action("add_meta_boxes", "sitepoint_post_series_custom_meta_box");

Here we add two fields to the meta box. The text field is used by the author to enter the serial number, and the drop down is used to select the post series name to which the post belongs to. If you don’t want to add a post to a post series, then either one or both fields should be left blank.

Here is how it looks on the admin post screen:

WordPress Post Series

Now we need to save the meta box fields when the form is saved. Here is the code to do that:

/* Callback to Save Meta Data */

function sitepoint_post_series_save_custom_meta_box($post_id, $post, $update)
{

	if(!isset($_POST["sitepoint-postseries"]) || !wp_verify_nonce($_POST["sitepoint-postseries"], basename(__FILE__)))
		return $post_id;

	if(!current_user_can("edit_post", $post_id))
		return $post_id;

	if(defined("DOING_AUTOSAVE") && DOING_AUTOSAVE)
		return $post_id;

	$slug = "post";
	if($slug != $post->post_type)
        return;

	$serial_number = null;
	if(isset($_POST["sitepoint-postseries-serial-number"]))
    {
        $serial_number = $_POST["sitepoint-postseries-serial-number"];
    }
    else
    {
    	$serial_number = "";
    }
    update_post_meta($post_id, "sitepoint-postseries-serial-number", $serial_number);

	$series_id = null;
	if(isset($_POST["sitepoint-postseries-id"]))
    {
        $series_id = $_POST["sitepoint-postseries-id"];
    }
    else
    {
    	$series_id = "";
    }

    $previous_series_id = get_post_meta($post_id, "sitepoint-postseries-id", true);

    update_post_meta($post_id, "sitepoint-postseries-id", $series_id);

    //no series, removing series, adding new series or changing series

    if($previous_series_id == "" && $series_id == "")
    {
    	sitepoint_post_series_save_settings($series_id, $serial_number, $post_id);
    }
    else if($previous_series_id != "" && $series_id == "")
    {
    	sitepoint_post_series_save_settings($previous_series_id, "", $post_id);	
    }
    else if($previous_series_id == "" && $series_id != "")
    {
    	sitepoint_post_series_save_settings($series_id, $serial_number, $post_id);
    }
    else if($previous_series_id != "" && $series_id != "")
    {
    	sitepoint_post_series_save_settings($previous_series_id, "", $post_id);
    	sitepoint_post_series_save_settings($series_id, $serial_number, $post_id);	
    }    
}

add_action("save_post", "sitepoint_post_series_save_custom_meta_box", 10, 3);

Here we are saving the meta box content and then calling the function sitepoint_post_series_save_settings with different argument values depending on whether the user is removing a series, adding a series or changing a series.

Here is the code for the sitepoint_post_series_save_settings function

/* Store WordPress posts and Post Series CTY relations as WordPress Settings. */

function sitepoint_post_series_save_settings($series_id, $serial_number, $post_id)
{
    if($series_id != "" && $serial_number != "")
    {
	    $post_series_list = get_option("post_series_" . $series_id . "_ids", "");

	    if($post_series_list == "")
	    {
	    	$post_series_list_array = array($post_id);
	    	$post_series_list = implode (", ", $post_series_list_array);

	    	update_option("post_series_" . $series_id . "_ids", $post_series_list);
	    }
	    else
	    {
	    	$post_series_list_array = explode(',', $post_series_list);

	    	if(in_array($post_id, $post_series_list_array))
	    	{
	    		//do nothing
	    	}
	    	else
	    	{
	    		$post_series_list_array[] = $post_id;
	    		$post_series_list = implode (", ", $post_series_list_array);
	    		update_option("post_series_" . $series_id . "_ids", $post_series_list);
	    	}
	    }
    }
    else if($series_id == "" || $serial_number == "")
    {
    	$post_series_list = get_option("post_series_" . $series_id . "_ids", "");

    	if($post_series_list == "")
    	{
    	}
    	else
    	{
    		$post_series_list_array = explode(',', $post_series_list);

    		if(in_array($post_id, $post_series_list_array))
    		{
    			//here remove the post id from array.
    			if(($key = array_search($post_id, $post_series_list_array)) !== false) {
				    unset($post_series_list_array[$key]);
				}
    			$post_series_list = implode (", ", $post_series_list_array);
	    		update_option("post_series_" . $series_id . "_ids", $post_series_list);
	    	}
    		else
    		{
    		}
    	}
    }
}

This function creates a string, which stores the WordPress post ID’s that belong to a particular series. And then it stores the strings as a WordPress setting.

Now we’re done with all the admin area code. You should now be able to create posts and assign them to a series. And also assign categories and tags to each series.

Now let’s code the front end to display the post series.

Making Post Series Visible on the Index Page and Archive Pages

The custom post type is not yet visible in the index and archive pages. To make it visible on these pages as well, you just need to add the code below:

/* Displaying Custom Post Types on Index Page */

function sitepoint_post_series_pre_posts($q)
{
	if(is_admin() || !$q->is_main_query() || is_page())
        return;

    $q->set("post_type", array("post", "sitepoint-postseries"));
}

add_action("pre_get_posts", "sitepoint_post_series_pre_posts");

Here we’re using pre_get_posts hook to add a post series to the $q variable, which is used by the main loop to displays posts.

Displaying Posts of a Post Series

We need to filter the content of the post series type and add posts belonging to the series.

Here is the code to add posts of a post series in a post series page.

function sitepoint_post_series_content_filter($content)
{	
	$slug = "sitepoint-postseries";
	if($slug != get_post_type())
        return $content;

	$post_series_list = get_option("post_series_" . get_the_ID() . "_ids", "");
	$post_series_list_array = explode(',', $post_series_list);

	$post_series_serial_number = array();

	foreach($post_series_list_array as $key => $value)
	{
		$serial_number = get_post_meta($value, "sitepoint-postseries-serial-number", true);
		$post_series_serial_number[$value] = $serial_number;
	}

	asort($post_series_serial_number);

	$html = "<ul class='sitepoint-post-series'>";

	foreach($post_series_serial_number as $key => $value) 
	{

		$post = get_post($key);
		
		$title = $post->post_title;
		
		$excerpt = $post->post_content;
		$shortcode_pattern = get_shortcode_regex();
        $excerpt = preg_replace('/' . $shortcode_pattern . '/', '', $excerpt);
        $excerpt = strip_tags($excerpt); 
        $excerpt = esc_attr(substr($excerpt, 0, 150));

        $img = "";

        if(has_post_thumbnail($key))
        {
        	$temp = wp_get_attachment_image_src(get_post_thumbnail_id($key), array(150, 150));
        	$img = $temp[0];
        }
        else
        {
        	$img = "http://lorempixel.com/150/150/abstract";
        }

        $html = $html . "<li><h3><a href='" . get_permalink($key) . "'>" . $title . "</a></h3><div><div class='sitepoint-post-series-box1'><img src='" . $img . "' /></div><div class='sitepoint-post-series-box2'><p>" . $excerpt . " ...</p></div></div><div class='clear'></div></li>";
	} 

	$html = $html . "</ul>";

	return $content . $html;
}

add_filter("the_content", "sitepoint_post_series_content_filter");

This displays the posts using HTML unordered list tag. For posts without an image we are loading a image from Lorempixel cloud service to generate random texture images.

We are retrieving the posts of a post series from the setting string, which we saved during the saving of meta data.

Adding Post Series Information to Posts

We can also add a post series box on posts that belong to a post series to indicate to the user that the post belongs to a specific posts series. Here’s the code to do that:

/* Adding Content to WordPress Posts which belong to a Series */

function sitepoint_post_series_post_content_filter($content)
{	
	$slug = "post";
	if($slug != get_post_type())
        return $content;

	$serial_number = get_post_meta(get_the_ID(), "sitepoint-postseries-serial-number", true);    
	$series_id = get_post_meta(get_the_ID(), "sitepoint-postseries-id", true);

    if(get_post_status($series_id) == "publish")
    {
        $html = "";
        
        if($series_id != "" || $serial_number != "")
        {
            $html = "<div class='sitepoint-post-series-post-content'><div>This post is a part " . $serial_number . " of <a href='" . get_permalink($series_id) . "'>" . get_the_title($series_id) . "</a> post series.</div></div>";    
        }

        $content = $html . $content; 
    }

    return $content;
}

Here we are just displaying a post series name and which part of this post is from the series.

You can also add the next and previous post of the series by using the below implementation of the sitepoint_post_series_post_content_filter function:

function sitepoint_post_series_post_content_filter($content)
{	
	$slug = "post";
	if($slug != get_post_type())
        return $content;

	$serial_number = get_post_meta(get_the_ID(), "sitepoint-postseries-serial-number", true);    
	$series_id = get_post_meta(get_the_ID(), "sitepoint-postseries-id", true);

	if($serial_number != "" && $series_id != "")
	{
		//find next and previous post too.

		$post_series_list = get_option("post_series_" . $series_id . "_ids", "");
		$post_series_list_array = explode(',', $post_series_list);

		$post_series_serial_number = array();

		foreach($post_series_list_array as $key => $value)
		{
			$serial_number = get_post_meta($value, "sitepoint-postseries-serial-number", true);
			$post_series_serial_number[$value] = $serial_number;
		}

		asort($post_series_serial_number);

		$post_series_serial_number_reverse = array();

		$iii = 1;

		foreach($post_series_serial_number as $key => $value) 
		{
			$post_series_serial_number_reverse[$iii] = $key;
			$iii++;
		}

		$index = array_search(get_the_ID(), $post_series_serial_number_reverse);

		if($index == 1)
		{
			$html = "<div class='sitepoint-post-series-post-content'><div>This post is a part of <a href='" . get_permalink($series_id) . "'>" . get_the_title($series_id) . "</a> post series.</div><div>&#9112; Next: <a href='" . get_permalink($post_series_serial_number_reverse[$index + 1]) . "'>" . get_the_title($post_series_serial_number_reverse[$index + 1]) . "</a></div></div>";
			$content = $html . $content;
		}
		else if($index > 1 && $index < sizeof($post_series_serial_number_reverse))
		{
			$html = "<div class='sitepoint-post-series-post-content'><div>This post is a part of <a href='" . get_permalink($series_id) . "'>" . get_the_title($series_id) . "</a> post series.</div><div>&#9112; Next post in the series is <a href='" . get_permalink($post_series_serial_number_reverse[$index + 1]) . "'>" . get_the_title($post_series_serial_number_reverse[$index + 1]) . "</a></div><div>&#9111; Previous post in the series is <a href='" . get_permalink($post_series_serial_number_reverse[$index - 1]) . "'>" . get_the_title($post_series_serial_number_reverse[$index - 1]) . "</a></div></div>";
			$content = $html . $content;
		}
		else if($index == sizeof($post_series_serial_number_reverse))
		{
			$html = "<div class='sitepoint-post-series-post-content'><div>This post is a part of <a href='" . get_permalink($series_id) . "'>" . get_the_title($series_id) . "</a> post series.</div><div>&#9111; Previous: <a href='" . get_permalink($post_series_serial_number_reverse[$index - 1]) . "'>" . get_the_title($post_series_serial_number_reverse[$index - 1]) . "</a></div></div>";
			$content = $html . $content;
		}
	}

	return $content;

}

The problem with this implementation is that the code hits the MySQL number of times equal to the number of posts in the series. There is a performance issue if you have a lot of posts for a particular post series, but I’ve included it for educational purposes.

Comparing Our Plugin to Other Post Series Plugins

Here are some of the plugins on WordPress.org that enable you to create a post series. I have compared each with the plugin we’ve created above.

  1. Organize Series: Organize Series adds a custom taxonomy to WordPress posts. When you view the custom taxonomy it adds a heading to the archive page, which seems like the title of the post series. And posts in a post series are displayed like taxonomies, which may not be great from a user perspective. In comparison, our plugin uses custom post types to display a single post series so that you can add featured images, text or markup content (and more) to a post series. The posts of a post series look different compared to the archive page in our plugin.
  2. WP Post Series: This also behaves like the Organize Series plugin. One difference is that it doesn’t provide any customization to the post series page.

Our plugin is highly customizable and doesn’t include any WordPress hacks. As such, it is much more compatible.

Conclusion

If you own a development blog, then you could use this plugin to create your own post series, which can increase your engagement and conversion rates. You can even use it to split up your large posts into multiple posts.

Feel free to comment on your experiences with the plugin below.

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in WordPress, once a week, for free.