Top Good Web Design

Web Designer

WordPress Plugin Development – Relate Posts as a Series – Part 2 / 2

So, we started our dive into WordPress plugin development with the first part of this tutorial where we talked a little about planning, basic plugin structure, custom post types, metaboxes and how to add custom functions to WordPress’s defaults actions.

Today we will talk a little more about metaboxes and jQuery, shortcodes, and front-end functionality.

All these things with a pretty practical example of how to make a plugin that will relate posts as a series.

So, let’s rock!

A little recap… and download!

Last time we completed the first three steps:

  1. Planning – We’ve made our entity-relationship diagram to know which data we would have to store
  2. Create our plugin and custom post type – We’ve created the basic file for our plugin, and registered our series custom post type
  3. Post functionality – We’ve made some metaboxes to store custom data, and added them to WordPress’s default post saving function via actions.

If you didn’t read the first part of this tutorial, I highly recommend you to do this now, but if you don’t want to, you can still understand the functions and logic we are using here and make use of it.

Well, for people who are in a hurry, you can download the fully working plugin and make use of it without having to copy/paste all this code ;).

Step 4 – Series functionality

Registering our metabox

After we have control over all our posts related to series, we need to be able to manually edit the series itself.

In order to do this, we will need some metaboxes on them also. So, let’s edit our plugin with this code (here is just the additional code to avoid any confusion with the code that I explained last time, and won’t explain now):

<?php<br /-->//this function will register our metabox for wd_series custom post type
	function wd_call_meta() {
			'series-data', // id of the <div> we'll add
			'Series Custom Data', //title
			'wd_series_options', // callback function that will echo the box content
			'wd_series', // where to add the box: on "post", "page", or "link" page
			'side', //positioning
			'low' //positioning
//with this we call it!
add_action('admin_menu', 'wd_call_meta');

Our HTML metabox and jQuery enhancements

So, as you can see this function doesn’t have the metabox itself, it just says “Hey, WordPress, you should add function wd_series_options as a metabox for this guy!”. So we now need the metabox itself.

This is a tricky part since this box will give the user the ability to add “fake” posts to the series, so our readers could be interested in this series content. So what do we have now is:

  • Standard posts for a series, added via post editing screen
  • Fake posts for a series, added via series editing screen
  • Delete option, to remove a post from a series

We have also the opening / closing option for a series, and a really important attribute, the size of the series, so we don’t get lost in all our counters.

To get this working we will need some jQuery. We will have a “model” row, and when the user wants to add more lines we will get this model and duplicate it inside our form. We have to pay attention in regards to recovering data. We have to dynamically add rows as they are needed by one series. And we have, of course, some CSS for that.

Let’s do it this way:


function wd_series_options() {
		global $post;
		//get saved data
		$custom    = get_post_custom($post->ID);
		$open      = $custom["open"][0];
		$numFields = $custom["size"][0];

		$openNo = $openYes = "";
		if ($open == "yes") {
			$openYes = "checked = 'checked'";
		} else {
			$openNo = "checked = 'checked'";

		$series_items = array();
		$i = 0;
		while($i < $numFields) {
			$key = "name_".$i;
			$series_items[$i] = $custom[$key][0];

<tr class="space">
				<label><input id="wd_open" name="wd_open" type="radio" value="yes" /> /> Yes, it is open.</label>

				<label><input id="wd_open" name="wd_open" type="radio" value="no" /> /> No, it is closed</label></td>
	<table class="default-line">
			<td><input id="order_K" name="order_K" type="text" value="" size="5" /></td>
			<td><input id="name_K" name="name_K" type="text" value="" /></td>
			<td class="check"><input id="remove_K" name="remove_K" type="checkbox" value="remove" /></td>
	<input type="hidden" id="NumFields" name="NumFields" value="<?php echo $numFields; ?>" />
			<td class="checkboxTd">Remove?</td>
	<div class="ins">
		foreach($series_items as $key => $name) {
			echo "<table>";
				echo "<tr>";
					echo "<td><input id='order_$key' name='order_$key' type='text' value='$key' size='5' /></td>";
					echo "<td><input id='name_$key' name='name_$key' type='text' value='$name' /></td>";
					echo "<td class='check'><input id='remove_$key' name='remove_$key' type='checkbox' value='remove' /></td>";
				echo "</tr>";
			echo "</table>";
		<tr id="addItem">
<td colspan="3"><a id="clickLink" class="link" onclick="addLine();">Add new line</a></td>
<style type="text/css">
	.default-line {
		display: none;
	#series-data table {
		width: 100%
		#series-data td {
			padding-bottom: 10px;
			#series-data .space label {
				padding-right: 20px
		#series-data .check {
			text-align: center;
		#addItem td {
			padding-top: 10px;
			text-align: right;
	.checkboxTd {
		width: 20px;
	.link { cursor: pointer; }
<script type="text/javascript">
	function addLine() {
		var numElem = jQuery("#NumFields").attr('value');
		jQuery(".ins .default-line").fadeIn().removeClass('default-line');
	function correctsLine(nID) {
		//goes switching K to correct number of line
		fieldName  = "order_" + nID;
		jQuery(".ins input#order_K").attr('value', ( parseInt(nID)+1) ).attr('id', fieldName).attr('name', fieldName);

		fieldName  = "name_" + nID;
		jQuery(".ins input#name_K").attr('id', fieldName).attr('name', fieldName);

		fieldName  = "remove_" + nID;
		jQuery(".ins input#remove_K").attr('id', fieldName).attr('name', fieldName);

		//corrects number of fields
		jQuery("#NumFields").attr('value', nID);
		addLine(); //adds first (blank) row

With this code you should see something like this as your series metabox:

Edit our default post saving function

And when you click on “Add new line”, believe me, it should create a fresh and brilliant new line.

Now we have this pretty box, but when you click “Update” nothing happens. This is why we haven’t prepared our WordPress insert post function to treat this data. What we have to do now it to say to WordPress “Hey, when you see this field in wd_series post type, delete all old data and save this new one for me, ok?”.

One important thing to note here is that we must delete all previous data and use some logic to reorder the series when needed.

Our magic here relies on ksort php function, so we save a temporary array and save all the items in the correct order after run this function.

Well, let’s do it:

<?php //we need a function that recieves post_id and $_POST data
function wd_meta($post_id, $post = null) {
//gets our POST custom data and saves it as meta keys, when needed
/* we have to save this metafields: open = Yes / No for open / closed series
size = how many items do we have in this series, so we can adjust our order counter
name_ORDER = the name of the ORDER'th item
post_ORDER = ID of the ORDER'th item
order_POST = Order of the ID (post) */
if( $post->post_type == "wd_series"  ) {
			//update series state (open / closed)
			$open = @$_POST["wd_open"];
			update_post_meta( $post_id, "open", $open );

			$size = @$_POST["NumFields"];
			$i = 0;

			$organize = array();
			while ($i < $size) {
				//let's pre-organize all posts
				$survive = $key = $remove = $order = $name = $post = null;

				$key = "remove_".$i;
				$remove = @$_POST[$key];

				if (empty($remove)) {
					//we won't delete this guy, and we'll put he in his right order
					$key = "order_".$i;
					$order = @$_POST[$key];

					$key = "name_".$i;
					$name = @$_POST[$key];

					$key = "post_".$i;
					$post = get_post_meta($post_id, $key, true);

					if (!empty($name)) {
						$organize[$order] = array( 'name' => $name, 'post' => $post);
						$survive = true;
				//we will pre delete everybody, to prevent trash in here
				$key = "name_".$i;
				delete_post_meta($post_id, $key);

				$key = "post_".$i;
				$post = get_post_meta($post_id, $key, true);
				delete_post_meta($post_id, $key);

				$key = "order_".$post;
				delete_post_meta($post_id, $key);

				//if it won't survive, we delete series_id from post_id
				if(!$survive) {
					$key = "series_id";
					delete_post_meta($post, $key);
			//let's correctly order this
			$i = 0;
			foreach($organize as $item) {

				$key = "name";
				$nam = $item[$key];
				$key = "name_".$i;
				update_post_meta( $post_id, $key, $nam );

				$key  = "post";
				$post = $item[$key];
				if(!empty($post)) {
					$key = "post_".$i;
					update_post_meta( $post_id, $key, $post );
					$key = "order_".$post;
					update_post_meta( $post_id, $key, $i );

			$size = count($organize);
			update_post_meta( $post_id, "size", $size );
	//it's me saying to wordpress "Hey guy, don't forget to save this data when you insert or update posts!"
	add_action("wp_insert_post", 'wd_meta', 10, 2);

Now we have our plugin working, let’s improve it.

Step 5 – Shortcodes and theming functions

Before we can output our posts we have to prepare two kind functions:

  • Common theming functions – Something like wd_series($args), so we can use for theming and widgets
  • Shortcodes – Something like [wd-series] so we can insert it directly from content edit mode.

Common functions

We will need to run a get_post loop, because probably our series will be shown inside another WordPress loop, so we can’t use WordPress’ default loop. Think about it this way: we will show a series when we are INSIDE a post, right? Thus the best way is via get_post.

Then we will just prepare a simple output function based on current post’s ID so it will show all items for the series related to it.

As we store the series related to this post inside “series_id” custom field, we just need to run a get_post for this series_id and output all metadata about series items.

Since for every item the output function is potentially the same, we will create two functions this time, one for items output and other for complete series output, as follow:

<?php //display item with or without link
function wd_item( $name, $postItem ) {
	if ( ! empty ( $postItem ) ) {
		$link = get_permalink($postItem);
		echo "<a title="$name" href="$link">$name</a>";
	} else {
		echo $name;
function wd_series ($series) {
	$title = get_the_title($series);
	$meta  = get_post_custom($series);

	echo "<h3>$title</h3>";
	$size = $meta["size"][0];
	$i = 1;
	echo "<ol>";
	while ($i <= $size) {
		$key = "name_".$i;
		$name = $meta[$key][0];

		$key = "post_".$i;
		$post_item = $meta[$key][0];

		echo "<li>";
			wd_item( $name, $post_item);
		echo "</li>";


		$name = $post_item = null;
	echo "</ol>";


So with the function above we can output our series in our template, and it is pretty customizable as you can see. But what if you want to give your writers the ability to decide where the series content should appear? Well, to do this you will need a shortcode.

Long story short, they give the ability to call functions via post content. So while I’m writing this post I could write [wd-series] and BAM! our series content would have to appear just above this text.

It is pretty easy to register a shortcode, and as long as we have our output function defined it will be even easier. With no more than six lines you can do it:

<?php // [wd-series]
function wd_series_shortcode() {
              global $post;
              $series = get_post_meta($post--->ID, "series_id", true);

add_shortcode( 'wd-series', 'wd_series_shortcode' );

After all this code, you will see something similar to this when you write [wd-series] in your content box:

Are you hungry yet?

This code surely could be improved and I know that some of our brilliant readers could point out some things to make it better. So why not leave a comment and share your thoughts?

And finally, I recommend you dig a little deeper into the  Shortcodes API, since it is a great tool when well used!

Leave a Reply