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

Posted in Coding, Tutorials3 years ago • Written by 24 Comments

Today our task is to create a simple WordPress plugin to relate posts as a series. With this, when you  create a new post it is automatically listed in your series, so if a user is reading any post of the series he can jump to others, even posts created after that one he is reading.

It may seem a bit too simple, but with this we can learn a lot about:
1. Custom post types
2. Custom fields
3. Metaboxes
4. Actions
5. Shortcodes
6. Basic plugin structure for WordPress
7. Little bit of jQuery
So, let’s rock!

Step 1 – Planning

Before any coding, we need to know what we want to create, so we don’t get lost in the process. There are a lot of tools and ways to use them to help us. The most important thing is to ensure that you at least know the minimum requirements for the project you’re working on, what you can add or cut down, and how things must work.

Our plugin functions list is:

  1. Create a new series, with title, description and posts assigned to it
  2. Add / remove current post to series (from post edit screen)
  3. Theme – Show other posts from series via shortcodes
  4. Theme – Show other posts from series via function (so you can use in your theme)
  5. Create fake series items, for future posts
  6. Edit title of the post when listed in series (it doesn’t have to be post’s WordPress title)

As long as we are using a CMS with a lot of built-in functions we won’t need to worry about external tables, SQL and many other functions that could take a little more time to create.

We can use custom post types to store data, by creating a simple custom post type structure and assign meta fields to it, so we don’t need any additional tables, or any complex changes, we will just use WordPress’ built-in functions. Just write a few lines and you are ready for it.

(Actually I’ve stolen Gilbert Pelegron’s idea, who has explained it much better than me. So, to dig a little deeper, check his post )

With this in mind, we can start to think more specifically about which data we will store and process. At this point we create a simple representation of how data should be stored, so we have all the data we need, without redundancy (so we just get the data from one point). There are a lot of ways to do it, but my idea is to save most data in our series entity, so when we get the series all needed data comes together (without additional calls). In this model, our post entity will just have to store which series it belongs to, and the rest is with the series.

As a simple way to represent it, I’ve made an entity-relationship diagram, with a little help from gliffy.

As you can see, our post type series will store almost all data, for a better performance. And its attributes is:

  • ID - Given by WordPress, when we create a series post.
  • Name - Title, saved as common WordPress’ post title.
  • Description - Little text about series. We will user WordPress’ description for this.
  • Open? - It is useful to know if a series is still open, so is much easier to assign a post to series when you have 1.000 series but just 3 open (you have posts to write for it in future).
  • Posts[], PostsScreenName[], SeriesSize – As we will use custom fields, I think the better way to store posts is create “fake arrays”. Let’s say we have one series with 3 posts, so we will have Post_1 = 10, Post_2 = 15, Post_3 = 32 and PostScreenName_1 = “Hello World”, PostScreenName_2 = “Hello World – Again”, PostScreenName_3 = “Hello, Hello World”. As we have 3 posts, our SeriesSize must to be 3.

Now, we can get started on coding.

Step 2 – Create our plugin and custom post type

Let’s create a folder inside our WordPress install (I recommend you do a blank 3.1.2 install, this is the version that I’m working with). Our folder name will be 1WDSeries and our plugin file will be 1WDSeries.php.

Plugin Basic Structure

Just open this blank file and write something like this:

<pre><!--?<span class="hiddenSpellError" pre=""-->php</pre>
/*
Plugin Name: 1WD Series
Plugin URI: http://www.1stwebdesigner.com/
Description: Creates "Series" custom post type, and makes possible relate normal posts inside one series
Author: Rochester Oliveira
Version: 1.0
Author URI: http://www.1stwebdesigner.com/author/rochester/
*/
?>

Those lines are required WordPress data for plugins. Just put a name, URI, Description and it will de shown in wp-admin, when users manage plugins.

Create post type

Before we start creating anything you must have in mind that any function we create here can be called in WordPress after (if this plugin is activated, for sure) AND any WordPress function can be called from this plugin. So we have to put a prefix in all our functions, to ensure that we will not have any conflict with other plugins or WordPress functions.

Our magic function to create custom post types is register_post_type($name,$args). Let’s see how it works:

<pre><!--?<span class="hiddenSpellError" pre=""-->php</pre>
 function wd_registerPostType() {
 //for better understanding when create / edit series we change some default labels
 $labels = array(
 'name' => _x('Series', 'wd_series'),
 'singular_name' => _x('Series', 'wd_series'),
 'add_new' => _x('New Series', 'wd_series'),
 'add_new_item' => __('Add New Series'),
 'edit_item' => __('Edit Series'),
 'new_item' => __('New Series'),
 'view_item' => __('View Series'),
 'search_items' => __('Search Series')
 );
 $args = array(
 'labels' => $labels,
 'public' => true,
 'publicly_queryable' => true,
 'show_ui' => true,
 'query_var' => true,
 'rewrite' => array( 'slug' => 'series', 'with_front' => false ),
 'capability_type' => 'post',
 'menu_position' => null,
 'supports' => array('title','editor', 'custom-fields')
 );
 register_post_type('wd_series',$args);
 }
 add_action("init", "wd_registerPostType");
?>
<pre>

The first block set’s up our labels, for different cases for this plugin. That $args part is pretty important because it defines how this custom post type will behave. Important things:

  • public – whether display or not this post in front-end, admin menu
  • rewrite – overwrite permalinks structure for this post
  • capability_type -If it is more like a post (hierarchical and “fixed”), or more like a page (categorized and time-oriented)
  • menu_position – Set its order in admin menu.
  • supports – Which common fields this post will have

That last line is what calls the function, every time we call WordPress.

Step 3 – Post functionality

Creating our metabox

Now we need to add a metabox in the post edit screen. A metabox is a panel in our edit mode, in this case with fixed custom inputs. Thus the user doesn’t need to know the field name to add it (via default custom fields insert), moreover we can do some custom actions with this data (create a post series when we have a field “new series”, for example).

The function we need to call is add_meta_box, and it has some simple parameters. We will call it inside a function, so when we create our series metabox (surely it will need one too) we put it all together.

<pre><!--?<span class="hiddenSpellError" pre=""-->php</pre>
 function wd_call_meta() {
 add_meta_box(
 'series-data', // id of the metabox
 'Series setting', // Title
 'wd_post_options', //callback function that will "echo" the contents
 'post', // which post types will have it (page, post, link, wd_series)
 'side', //positioning: side, advanced
 'low' // positioning: low, high
 );
 }

 if (is_admin()) {
 //register metaboxes
 add_action('admin_menu', 'wd_call_meta');
 }
?>

The first block is our magic function, that says to WordPress “hey, when you edit a post, add this box, with these parameters!” The second function, says “hey, when you are in wp-admin don’t forget to call this function”.

We will now create our wd_post_options function, that defines what we have in our metabox. It is a simple HTML,  that we will just add some CSS to make it prettier. But we have to pay attention to get default values so if we are editing a post (not creating) we show the correct current data to the user (if our post is part of series_id 11, for example, we have to show this to the user).

<pre><!--?<span class="hiddenSpellError" pre=""-->php</pre>
 //posts meta
 function wd_post_options() {

 global $post;
 $custom = get_post_custom($post->ID);
 $exist_series = $custom["series_id"][0]; //it comes as array, we have to get the index 0 value
 $order_series = $custom["series_order"][0];
 $closing = $name = "";
 if(!empty($exist_series)) {
 $open = get_post_meta($exist_series, "open", true);
 if ($open == "no") {
 $size = get_post_meta($exist_series, "size", true);
 if ($size == $order_series) { //so, we have the last post in a closed series, this post is the "closing" series post
 $closing = "checked = 'checked'"; //html value that we add to checkbox, to 'check' it, by default
 }
 $key = "name_".$order_series;
 $name = get_post_meta($exist_series, $key, true);
 }
 }
?>
 <table>
 <tr>
 <td><label>Add to Series (leave blank to remove)</label></td>
 <td>
 <label>
 <select name="wd_exist_series" id="wd_exist_series">
 <option>--Select existing series</option>
 <!--?<span class="hiddenSpellError" pre=""-->php
 $series_print = 0; //when this post is assigned to a closed series, we have to show it :D
 $args = array( 'post_type' => 'wd_series', 'posts_per_page' => -1, 'meta_key' => 'open', 'meta_value' => 'yes'  ) ;
 $loop = get_posts( $args ); //we get all open series, to show them as options in our select
 foreach( $loop as $post ) :	setup_postdata($post);
 if ($post->ID == $exist_series) {
 $selected = "selected = 'selected'";
 $series_print = 1;
 } else {
 $selected = "";
 }
 echo "<option value='".$post->ID."' $selected>".get_the_title()."</option>";
 endforeach;

 if (empty($series_print)) {
 $data = get_post($exist_series);
 echo "<option value='".$data->ID."' selected = 'selected'>".get_the_title($exist_series)."</option>";
 }
 ?>
 </select>
 </label><br />
 <label><input id="wd_new_series" name="wd_new_series" type="text" value="New Series Name" /></label>
 </td>
 </tr>
 <tr>
 <td><label for="wd_order">Order in series (leave blank to be last)</label></td>
 <td>
 <input id="wd_order" name="wd_order" type="text" value="<?<span class=" />php echo $order_series; ?>" />
 </td>
 </tr>
 <tr>
 <td>Close series?</td>
 <td>
 <label><input id="wd_end" class="hiddenSpellError" name="wd_end" type="checkbox" value="Yes" />php echo $closing; ?> /> Yes</label>
 </td>
 </tr>
 <tr>
 <td colspan="2"><label for="wd_name">Display name in series</label></td>
 </tr>
 <tr>
 <input id="wd_name" name="wd_name" type="text" value="<?<span class=" />php echo $name; ?>" />
 </tr>
 </table>
<style type="text/css">
 #series-data table {
 width: 100%
 }
 #series-data tr td {
 padding-bottom: 10px;
 }
 #series-data .nospace td {
 padding-bottom: 0
 }
</style>
<!--?<span class="hiddenSpellError" pre=""-->php
 }
?>

Creating the inputs itself, doesn’t save any data. It is just an input that WordPress ignores when saving default post data, so we need to change it. This is a job for our fantastic add_action function.

Save all this data with custom function

We need to run a function every time we have a post saved or edited. At this point, we have just $_POST data so we need to add our custom fields, and create our custom post type when this post is the first of its series. At this point we will deal with some series custom fields (because it stores almost all data), so it may seem quite strange now, but in the end it’s gonna be all right  :D

<pre><!--?<span class="hiddenSpellError" pre=""-->php</pre>
//saves our post custom data
 function wd_meta($post_id, $post = null) {
 /*
 Custom Fields in posts: series_id
 */
 if ($post->post_type == "post") {
 $title = get_the_title($post_id);

 $old_series = get_post_meta($post_id, "series_id", true);
 //compare old series data, and see if it needs to be rewritten
 $exist_series = (int) @$_POST["wd_exist_series"];
 $new_series   = @$_POST["wd_new_series"];
 $order        = @$_POST["wd_order"];
 $name         = @$_POST["wd_name"];
 $closing      = @$_POST["wd_end"];

 if(!empty($new_series) && $new_series != "New Series Name") {
 //our series have changed, let's delete this post from old series and "resave" it!
 $key = "order_".$post_id;
 $old_order =  get_post_meta($old_series, $key, true);
 delete_post_meta($old_series, $key);

 $key = "post_".$old_order;
 delete_post_meta($old_series, $key);

 $key = "name_".$old_order;
 delete_post_meta($old_series, $key);

 //update old series size
 $old_size = get_post_meta($old_series, "size", true);
 $old_size--;
 update_post_meta($old_series, "size", $old_size);

 //we have a new series, so let's create it, dude!
 $args = array(
 'post_title'  => $new_series,
 'post_status' => 'publish',
 'post_type'   => 'wd_series'
 );
 $series_id = wp_insert_post( $args );

 if (empty($name)) {
 //we don't have any name, so let's use post name
 $name = $title;
 }
 //add this post to series_id and its wd_order and wd_name
 update_post_meta($series_id, "size", 1);
 update_post_meta($series_id, "post_1", $post_id);
 update_post_meta($series_id, "name_1", $name);
 $key = "order_".$post_id;
 update_post_meta($series_id, $key, 1);

 update_post_meta( $post_id, "series_id", $series_id );
 }
 elseif (!empty($exist_series)) {
 // we have a series, let's see if it is still the same
 if ( $exist_series != $old_series) {
 //our series have changed, let's delete this post from old series and "resave" it!
 $key = "order_".$post_id;
 $old_order =  get_post_meta($old_series, $key, true);
 delete_post_meta($old_series, $key);

 $key = "post_".$old_order;
 delete_post_meta($old_series, $key);

 $key = "name_".$old_order;
 delete_post_meta($old_series, $key);

 //update old series size
 $old_size = get_post_meta($old_series, "size", true);
 $old_size--;
 update_post_meta($old_series, "size", $old_size);

 //update series size
 $size = get_post_meta($exist_series, "size", true);
 $size++;
 update_post_meta($exist_series, "size", $size);

 if (!empty($new_order)) {
 //we do have a new order to set, so let's follow it
 $order = $new_order;
 } else {
 //we don't have any order to set, so let's check it as last item
 $order = $size;
 }
 $key = "post_".$order;
 update_post_meta( $exist_series, $key, $post_id );

 if (empty($name)) {
 //we don't have any name, so let's use post name
 $name = $title;
 }
 $key = "name_".$order;
 update_post_meta( $exist_series, $key, $name );

 $key = "order_".$post_id;
 update_post_meta( $exist_series, $key, $order );

 //save new series on this post
 update_post_meta( $post_id, "series_id", $exist_series );
 }
 }
 //closing if needed
 if (!empty($closing)) {
 update_post_meta( $series_id, "open", "no" );
 } else {
 update_post_meta( $series_id, "open", "yes" );
 }
 }
 }
 //add custom meta when we save posts
 add_action("wp_insert_post", 'wd_meta', 10, 2);
?>

Are you hungry yet?

I think we’ve had enough fun for today. In our next post, we add our series meta box, deal with its data and show all this stuff in our theme (via functions and shortcodes). It will be amazing :D

As I know you are a good padawan, and want to know more about all this stuff we worked with here, so here are some good links for reading:

If you enjoyed this article, get email updates (it's free).

Join over 77,235 Subscribers Today.

43 Written ArticlesWebsiteGoogle+

I'm a web designer and entrepreneur from Itajubá (MG), Brasil. I love writing about obscure topics and doing some cool stuff. And also I do some FREE stuff, check it out: http://www.roch.com.br/

24 Comments Best Comments First
  • Júlio

    Wednesday, May 11th, 2011 14:53

    1

    Very good article. Detailed and easy to understand. I’ll make my first plugin for this article.
    While awaiting the second part.

    0
    • Rochester Oliveira

      Wednesday, May 11th, 2011 16:49

      5

      Thank you, julio!

      And keep coming, we have much more great content to publish!

      0
  • Manuel

    Wednesday, May 11th, 2011 23:11

    10

    Hey ya! – That’s the information I was looking for, long time without reading your blog! Keep the good work! :)

    0
    • Rochester Oliveira

      Thursday, May 12th, 2011 20:42

      13

      Hey manuel!

      Keep coming to see part2 of this tutorial!

      Thank you,
      []‘s

      0
  • Kavya Hari

    Thursday, May 12th, 2011 12:50

    11

    It would be useful to all the folks :) thanks a lot for sharing your post on here :)

    0
    • Rochester Oliveira

      Thursday, May 12th, 2011 20:43

      14

      Hi Kavya,
      I think it is useful too, and we have much more to go :D

      []‘s

      0
  • cassia

    Wednesday, May 11th, 2011 15:38

    2

    you could insert the button of google translator.
    Make it easier for people like me who understand very little English but admire your work.

    0
    • Rochester Oliveira

      Wednesday, May 11th, 2011 16:49

      4

      Agreed!
      I have some Brazilian colleagues that understand a little English.. It would be helpful!
      []‘s

      0
    • Rean John Uehara

      Wednesday, May 11th, 2011 16:30

      3

      Hi cassia, I’ll inform the site admin about it and see what we can do. :) Thanks!

      0
      • Rochester Oliveira

        Wednesday, May 11th, 2011 16:51

        6

        Hey Rean, seems like in homepage all bullets are outside content area (firefox 3.6 now, haven’t tested in others)..

        0
        • Rean John Uehara

          Wednesday, May 11th, 2011 16:58

          7

          You’re right. They’re inside now, but still lying on the border. Can’t seem to properly display it. Maybe there shouldn’t be bullets before the tag..

          0
          • Rochester Oliveira

            Wednesday, May 11th, 2011 17:54

            8

            Yes, if you want to change it to a “fake ol” (with 1 – .. 2 – ….. and paragraphs) I think is the easiest way to solve it..
            There is another post that I wrote with ul before !–more–, I’ll change it now :D

            0
          • Rean John Uehara

            Wednesday, May 11th, 2011 18:12

            9

            That’s a smart suggestion, didn’t occur to me. :)) Thanks!

            0
  • Alher

    Thursday, May 12th, 2011 14:20

    12

    I think your article is so much informative for developing a new plugins.I will must try to follow your instruction properly.I hope your next article part-2 will published as early as possible.

    0
    • Rochester Oliveira

      Thursday, May 12th, 2011 20:45

      15

      Hi!
      Let me tell you a secret, actually he have more plugins tutorials.

      Keep coming to read them all soon :D

      []‘s

      0
  • Rochester Oliveira

    Monday, January 2nd, 2012 19:30

    21

    Hey, if you need any aditional help, just ask!

    0
  • Rochester Oliveira

    Monday, January 2nd, 2012 19:29

    20

    Hey chalazion, that’s what I like to hear ;)

    []‘s

    0
  • Rashmika

    Wednesday, December 28th, 2011 16:36

    19

    Very good article and Detailed and easy to understand.

    0
    • Rochester Oliveira

      Monday, January 2nd, 2012 19:31

      22

      Hi Rashmika,

      I’m glad you liked it!

      Hope to see you again soon!

      []‘s

      0
  • Rochester Oliveira

    Monday, September 19th, 2011 16:39

    16

    Hi id meneo,

    Thank you :) I’m glad you liked!

    []‘s

    0
  • Ramnadh

    Monday, December 19th, 2011 14:52

    18

    Really good article. I was looking for similar kind of tutorial. Thanks for well explained tutorial. Explanation for every part of plugin is very good.

    0
    • Rochester Oliveira

      Monday, January 2nd, 2012 19:31

      23

      Hey Ramnadh, if you are stuck with anything, just let me know! I’m glad you liked it!

      []‘s

      0
  • Baheen

    Friday, November 4th, 2011 15:53

    17

    I want to make map-link but could’t success please help me. If you have some map-link website please show me.

    0
    • Rochester Oliveira

      Monday, January 2nd, 2012 19:28

      24

      Sorry, bheem sen mali, I don’t get it.

      0
  • Rochester Oliveira

    Monday, January 2nd, 2012 19:30

    21

    Hey, if you need any aditional help, just ask!

    0
  • Rochester Oliveira

    Monday, January 2nd, 2012 19:29

    20

    Hey chalazion, that’s what I like to hear ;)

    []‘s

    0
  • Rashmika

    Wednesday, December 28th, 2011 16:36

    19

    Very good article and Detailed and easy to understand.

    0
    • Rochester Oliveira

      Monday, January 2nd, 2012 19:31

      22

      Hi Rashmika,

      I’m glad you liked it!

      Hope to see you again soon!

      []‘s

      0
  • Ramnadh

    Monday, December 19th, 2011 14:52

    18

    Really good article. I was looking for similar kind of tutorial. Thanks for well explained tutorial. Explanation for every part of plugin is very good.

    0
    • Rochester Oliveira

      Monday, January 2nd, 2012 19:31

      23

      Hey Ramnadh, if you are stuck with anything, just let me know! I’m glad you liked it!

      []‘s

      0
  • Baheen

    Friday, November 4th, 2011 15:53

    17

    I want to make map-link but could’t success please help me. If you have some map-link website please show me.

    0
    • Rochester Oliveira

      Monday, January 2nd, 2012 19:28

      24

      Sorry, bheem sen mali, I don’t get it.

      0
  • Rochester Oliveira

    Monday, September 19th, 2011 16:39

    16

    Hi id meneo,

    Thank you :) I’m glad you liked!

    []‘s

    0
  • Alher

    Thursday, May 12th, 2011 14:20

    12

    I think your article is so much informative for developing a new plugins.I will must try to follow your instruction properly.I hope your next article part-2 will published as early as possible.

    0
    • Rochester Oliveira

      Thursday, May 12th, 2011 20:45

      15

      Hi!
      Let me tell you a secret, actually he have more plugins tutorials.

      Keep coming to read them all soon :D

      []‘s

      0
  • Kavya Hari

    Thursday, May 12th, 2011 12:50

    11

    It would be useful to all the folks :) thanks a lot for sharing your post on here :)

    0
    • Rochester Oliveira

      Thursday, May 12th, 2011 20:43

      14

      Hi Kavya,
      I think it is useful too, and we have much more to go :D

      []‘s

      0
  • Manuel

    Wednesday, May 11th, 2011 23:11

    10

    Hey ya! – That’s the information I was looking for, long time without reading your blog! Keep the good work! :)

    0
    • Rochester Oliveira

      Thursday, May 12th, 2011 20:42

      13

      Hey manuel!

      Keep coming to see part2 of this tutorial!

      Thank you,
      []‘s

      0
  • cassia

    Wednesday, May 11th, 2011 15:38

    2

    you could insert the button of google translator.
    Make it easier for people like me who understand very little English but admire your work.

    0
    • Rean John Uehara

      Wednesday, May 11th, 2011 16:30

      3

      Hi cassia, I’ll inform the site admin about it and see what we can do. :) Thanks!

      0
      • Rochester Oliveira

        Wednesday, May 11th, 2011 16:51

        6

        Hey Rean, seems like in homepage all bullets are outside content area (firefox 3.6 now, haven’t tested in others)..

        0
        • Rean John Uehara

          Wednesday, May 11th, 2011 16:58

          7

          You’re right. They’re inside now, but still lying on the border. Can’t seem to properly display it. Maybe there shouldn’t be bullets before the tag..

          0
          • Rochester Oliveira

            Wednesday, May 11th, 2011 17:54

            8

            Yes, if you want to change it to a “fake ol” (with 1 – .. 2 – ….. and paragraphs) I think is the easiest way to solve it..
            There is another post that I wrote with ul before !–more–, I’ll change it now :D

            0
          • Rean John Uehara

            Wednesday, May 11th, 2011 18:12

            9

            That’s a smart suggestion, didn’t occur to me. :)) Thanks!

            0
    • Rochester Oliveira

      Wednesday, May 11th, 2011 16:49

      4

      Agreed!
      I have some Brazilian colleagues that understand a little English.. It would be helpful!
      []‘s

      0
  • Júlio

    Wednesday, May 11th, 2011 14:53

    1

    Very good article. Detailed and easy to understand. I’ll make my first plugin for this article.
    While awaiting the second part.

    0
    • Rochester Oliveira

      Wednesday, May 11th, 2011 16:49

      5

      Thank you, julio!

      And keep coming, we have much more great content to publish!

      0

Comments are closed.

54.243.23.129 - unknown - unknown - US