Creating WordPress Custom Post Types in a Plugin
In a recent discussion in the WordPress group on LinkedIn, I shared how I go about making custom post types. A few of the group members expressed interest in a little more information, so I decided I would write up this tutorial on creating custom post types in WordPress.
I’m going to create an example plugin that allows you to add news to your site. There are a few requirements that need to be met. Those are as follows:
- Allow me to display a news listing anywhere in my site content, preferably through the use of a shortcode.
- Allow me to display a list of news titles in the sidebar of my site/blog. I want to be able to set how many are displayed and I want to disable this display at anytime, preferably through the widget panel in admin.
- Make the data entry of the news topic feel like I’m adding a standard post or page content.
So now that we have some requirements basic requirements, let’s look at what functionality we need.
- An admin panel that works like a traditional blog post
- A shortcode registration
- An admin widget that will be administered via the widgets panel
Based on those pieces of functionality, we are going to take advantage of the WordPress custom post type so that the data entry is very familiar to the user and we hook into WordPress’ functionality for saving and storing data.
So let’s get started.
First we need to create our plugin. Let’s start by setting up our plugin to work within WordPress. I create my plugins as classes so the syntax may look a little funky if you don’t create yours this way.
<?php /* Plugin Name: WP-723-News Version: 1.0 Description: Creates a custom News post type to display in content and a sidebar widget Author: Davey Montooth - 723Media, LLC Author URI: http://www.723media.com Plugin URI: http://www.723media.com/wp-723-news */ global $wp_version; $exit_msg = 'WP-723-News requires WordPress 3.0 or newer. <a href="http://codex.wordpress.org/Upgrading_WordPress">Click here to upgrade.</a>'; if(version_compare($wp_version, "3.0", "<")) { exit($exit_msg); } if(! class_exists('WP723News') ) : class WP723News { } else : exit('WP-723-News has already been set up.'); endif; $WP723News = new WP723News(); if(isset($WP723News)) { register_activation_hook(__FILE__, array(&$WP723News, 'install')); register_deactivation_hook(__FILE__, array(&$WP723News, 'uninstall')); } ?>
Let’s break this down a bit. At this stage, we have a class that is a shell of a plugin.
This block is your plugin description, which will show up in your plugins list when you click “Plugins” in the menu:
/*
Plugin Name: WP-723-News
Version: 1.0
Description: Creates a custom News post type to display in content and a sidebar widget
Author: Davey Montooth - 723Media, LLC
Author URI: http://www.723media.com
Plugin URI: http://www.723media.com/wp-723-news
*/This next block does a version check to make sure the user is running the minimum version of WordPress to run your plugin. For this example, we are looking for WordPress 3.0 and up. If the user doesn’t have the minimum version required, it’s commonplace to let them know that they need to upgrade and to provide a link to download the latest version.
global $wp_version; $exit_msg = 'WP-723-News requires WordPress 3.0 or newer. <a href="http://codex.wordpress.org/Upgrading_WordPress">Click here to upgrade.</a>'; if(version_compare($wp_version, "3.0", "<")) { exit($exit_msg); }
The next block will instantiate our class if it isn’t already. If it is, then we display a message stating that it already exists. This get called in the block following this:
if(! class_exists('WP723News') ) : class WP723News { } else : exit('WP-723-News has already been set up.'); endif;
In this block, we instantiate our class and activate our plugin. We hook into WordPress’ activate functionality and pass in our install function:
$WP723News = new WP723News(); if(isset($WP723News)) { register_activation_hook(__FILE__, array(&$WP723News, 'install')); register_deactivation_hook(__FILE__, array(&$WP723News, 'uninstall')); }
So that is our bare bones plugin class. Now we need to add some real functionality. For the rest of this tutorial, I will add code blocks that will need to go between your class braces:
class WP723News { }
Let’s start with our constructor. Our plugin needs to register a variety of things so we use our constructor to register our plugin’s features. Let’s take a look at the code and then we can break it down:
function WP723News() { # Register Custom Post Type add_action('init', array(&$this, 'register_post_type')); # Register Widget add_action('widgets_init', array(&$this,'register_widget')); # Register Shortcodes add_shortcode('display-news-listings', array(&$this, 'news_listings')); }
In the first line, we add an action to register our custom post type and pass it a reference to our ‘register_post_type’ function.
add_action('init', array(&$this, 'register_post_type'));
Since we want to let the user control the sidebar output using a widget, we register one with the following line:
add_action('widgets_init', array(&$this,'register_widget'));
Next, we need a way to display our news listings in our page content. To pull this off, we register a shortcode that the user can enter into their content editor.
add_shortcode('display-news-listings', array(&$this, 'news_listings'));
Now that we have set up our plugin to register a custom post type, sidebar widget and a shortcode to display listings, let’s add the code to actually do this.
After your constructor, you can drop this function in to actually register the “news” post type:
function register_post_type() { $labels = array( 'name' => __('News', 'post type general name'), 'singular_name' => __('news', 'post type singular name'), 'add_new' => __('Add News', 'news'), 'add_new_item' => __('Add News Topic'), 'edit' => __('Edit', 'news'), 'edit_item' => __('Edit News'), 'new_item' => __('New News Topic'), 'view_item' => __('View News'), 'search_items' => __('Search News'), 'not_found' => __('Nothing found'), 'not_found_in_trash' => __('Nothing found in Trash'), 'parent_item_colon' => '' ); $args = array( 'labels' => $labels, 'public' => true, 'publicly_queryable' => true, 'show_ui' => true, 'query_var' => true, 'rewrite' => false, 'capability_type' => 'post', 'hierarchical' => false, 'menu_position' => null, 'supports' => array('title','editor','comments') ); register_post_type( 'news' , $args ); }
I won’t cover each argument in this tutorial (perhaps in a follow-up) but what you are doing here is assigning the string value to all of the labels used by your plugin in the “labels” array. These are the page headers that you see above your editing panel, in your menu, etc.
In the args array, you set a variety of options that determine how your plugin will behave. The ones that I want to point out are “rewrite” and “supports”. Rewrite allows your custom post type to inherit your site’s permalink structure. This one can be a little tricky if you don’t set a single page template. To make sure it works regardless of permalink structure, you can set “rewrite” to “false” as I have done for this tutorial.
Supports lets you define which meta boxes will appear in the editor panel. For this example, I have enabled support for “Title”, “Editor” and “Comments”. This tells WordPress that I want to enter a title, I want to add content with the content editor and I want to be able to enable/disable comments for each news article.
There is much more information available when registering custom post types that is somewhat out of scope of this tutorial but you can begin exploring the codex here http://codex.wordpress.org/Post_Types to get more in-depth info.
At this point, you can activate your plugin and start adding news articles. Now that your post type is registered, we want to let your users display them in a sidebar widget.
Let’s register the sidebar widget. In our constructor we added a widgets_init action and told it to point to a function named ‘register_widget’. This is that fucntion:
function register_widget() { if ( !function_exists('register_sidebar_widget') ) { return; } register_sidebar_widget(array('723 News', 'widgets'), array(&$this, 'render_news_widget')); register_widget_control(array('723 News', 'widgets'), array(&$this, 'news_widget_control'), 300, 200); }
What this function does is, check to see if we are able to register sidebar widgets. If we can, then we register the widget and point it to a function to render the widget in the sidebar – “render_news_widget”.
We also need to control the options for our widget in the admin panel. So we register a widget control and point it to our “news_widget_control” function. We also set some dimensions for the widget control form. In this example, we set it to 300px high by 200px wide.
Now that our widget is registered, let’s take a look at the functions to render it on the public side and control it on the admin side.
We’ll start with controlling the options.
function news_widget_control()
{
$options = get_option('widget_news');
if (!is_array($options))
{
$options = array('title'=>'723 News', 'display'=>'5');
}
if ($_POST['news-widget-submit'])
{
$options['title'] = strip_tags(stripslashes($_POST['news-widget-title']));
$options['display'] = strip_tags(stripslashes($_POST['news-widget-display']));
update_option('widget_news', $options);
}
$title = htmlspecialchars($options['title'], ENT_QUOTES);
$display = htmlspecialchars($options['display'], ENT_QUOTES);
?>
<p>
<label for="news-widget-title">Title</label>
<input style="width: 200px;" id="news-widget-title" name="news-widget-title" type="text" value="<?php echo $title; ?>" />
</p>
<p>
<label for="news-widget-display">Display</label>
<input style="width: 200px;" id="news-widget-display" name="news-widget-display" type="text" value="<?php echo $display; ?>" />
</p>
<input type="hidden" id="news-widget-submit" name="news-widget-submit" value="1" />
<?php
}Let’s break this function down.
First, we grab the options for our widget – “widget_news”. Next, we check to see if an options collection was returned and if not, we set some default options. The default options are our title and a default number to display.
After this we check to see if our options form was posted. If so, we update the options in the database.
Finally, regardless of whether we are saving a post or not, we display our form.
Now let’s look at rendering the widget in the sidebar in our theme:
function render_news_widget($args) { extract($args); $options = get_option('widget_news'); $title = $options['title']; $display = $options['display']; if ($display < 1) $display = 1; echo $before_widget . $before_title . $title . $after_title; global $wpdb; $sql = "SELECT * FROM " . $wpdb->posts . " WHERE post_type='news' AND post_status='publish' ORDER BY post_date DESC LIMIT $display"; $posts = $wpdb->get_results($sql); $output = ''; $output .= '<ul>'; if (!empty($posts)) { foreach ($posts as $post) { $output .= '<li><a rel="bookmark" href="'.get_permalink($post->ID).'">' . $post->post_title .'</a></li>'; } } else { $output .= "<li>No Recent News</li>"; } $output .= '</ul>'; echo $output; echo $after_widget; }
To sum this function up, we grab the options that we save in our widget control function.
In this function we echo out the default beginning WordPress structure for a widget:
echo $before_widget . $before_title . $title . $after_title;
Then we create a custom query to pull our custom post type and limit it by the display number entered in the widget in the admin panel:
$sql = "SELECT * FROM " . $wpdb->posts . " WHERE post_type='news' AND post_status='publish' ORDER BY post_date DESC LIMIT $display";
From there, we create an output variable and if there are news articles, we loop through them and build a list or, if there aren’t any, we display a message of “No Recent News”.
So now we have our custom post type and a widget to display them in the sidebar. Let’s take a look at the code to render the shortcode:
function news_listings() { global $wpdb; $table = $wpdb->prefix . 'posts'; $sql = "SELECT * FROM " . $wpdb->posts . " WHERE post_type='news' AND post_status='publish' ORDER BY post_date DESC"; $posts = $wpdb->get_results($sql); $output = ''; if(count($posts) > 0) { $output .= '<ul class="news">'; foreach($posts as $post) { $output .= '<li>'; $output .= '<h3><a href="'. get_permalink($post->ID) .'">'. $post->post_title .'</a></h3>'; $output .= '</li>'; } $output .= '</ul>'; } else { $output .= '<p>Sorry, there is no recent news.</p>'; } return $output; }
This function is actually very similar to our rendering of the widget. We use a custom query to get our post type collection. Then we loop through the collection and return HTML output. In this example, we return a UL with links to our news articles by title.
If you save and activate your plugin, you can now display a widget in the sidebar and throw this shortcode into your page content: [display-news-listings].
That’s it. You now have a plugin to register a custom post type, display it in a widget and display it in your page content using a shortcode.
This plugin is simply to illustrate the process of creating custom post types, registering widgets and registering shortcodes. You can extend this as much as you need or use it as a guide to begin creating your own custom post types.
In a future tutorial, I’ll cover how to add custom meta boxes with custom fields, custom icons for your post types and more. Stay tuned.
Here’s the total code for the plugin:
<?php /* Plugin Name: WP-723-News Version: 1.0 Description: Creates a custom News post type to display in content and a sidebar widget Author: Davey Montooth - 723Media, LLC Author URI: http://www.723media.com Plugin URI: http://www.723media.com/wp-723-news */ global $wp_version; $exit_msg = 'WP-723-News requires WordPress 3.0 or newer. <a href="http://codex.wordpress.org/Upgrading_WordPress">Click here to upgrade.</a>'; if(version_compare($wp_version, "3.0", "<")) { exit($exit_msg); } if(! class_exists('WP723News') ) : class WP723News { function WP723News() { # Register Custom Post Type add_action('init', array(&$this, 'register_post_type')); # Register Widget add_action('widgets_init', array(&$this,'register_widget')); # Register Shortcodes add_shortcode('display-news-listings', array(&$this, 'news_listings')); } function register_post_type() { $labels = array( 'name' => __('News', 'post type general name'), 'singular_name' => __('news', 'post type singular name'), 'add_new' => __('Add News', 'news'), 'add_new_item' => __('Add News Topic'), 'edit' => __('Edit', 'news'), 'edit_item' => __('Edit News'), 'new_item' => __('New News Topic'), 'view_item' => __('View News'), 'search_items' => __('Search News'), 'not_found' => __('Nothing found'), 'not_found_in_trash' => __('Nothing found in Trash'), 'parent_item_colon' => '' ); $args = array( 'labels' => $labels, 'public' => true, 'publicly_queryable' => true, 'show_ui' => true, 'query_var' => true, 'rewrite' => false, 'capability_type' => 'post', 'hierarchical' => false, 'menu_position' => null, 'supports' => array('title','editor','comments') ); register_post_type( 'news' , $args ); } function register_widget() { if ( !function_exists('register_sidebar_widget') ) { return; } register_sidebar_widget(array('723 News', 'widgets'), array(&$this, 'render_news_widget')); register_widget_control(array('723 News', 'widgets'), array(&$this, 'news_widget_control'), 300, 200); } function news_widget_control() { $options = get_option('widget_news'); if (!is_array($options)) { $options = array('title'=>'723 News', 'display'=>'5'); } if ($_POST['news-widget-submit']) { $options['title'] = strip_tags(stripslashes($_POST['news-widget-title'])); $options['display'] = strip_tags(stripslashes($_POST['news-widget-display'])); update_option('widget_news', $options); } $title = htmlspecialchars($options['title'], ENT_QUOTES); $display = htmlspecialchars($options['display'], ENT_QUOTES); ?> <p> <label for="news-widget-title">Title</label> <input style="width: 200px;" id="news-widget-title" name="news-widget-title" type="text" value="<?php echo $title; ?>" /> </p> <p> <label for="news-widget-display">Display</label> <input style="width: 200px;" id="news-widget-display" name="news-widget-display" type="text" value="<?php echo $display; ?>" /> </p> <input type="hidden" id="news-widget-submit" name="news-widget-submit" value="1" /> <?php } function render_news_widget($args) { extract($args); $options = get_option('widget_news'); $title = $options['title']; $display = $options['display']; if ($display < 1) $display = 1; echo $before_widget . $before_title . $title . $after_title; global $wpdb; $sql = "SELECT * FROM " . $wpdb->posts . " WHERE post_type='news' AND post_status='publish' ORDER BY post_date DESC LIMIT $display"; $posts = $wpdb->get_results($sql); $output = ''; $output .= '<ul>'; if (!empty($posts)) { foreach ($posts as $post) { $output .= '<li><a rel="bookmark" href="'.get_permalink($post->ID).'">' . $post->post_title .'</a></li>'; } } else { $output .= "<li>No Recent News</li>"; } $output .= '</ul>'; echo $output; echo $after_widget; } function news_listings() { global $wpdb; $table = $wpdb->prefix . 'posts'; $sql = "SELECT * FROM " . $wpdb->posts . " WHERE post_type='news' AND post_status='publish' ORDER BY post_date DESC"; $posts = $wpdb->get_results($sql); $output = ''; if(count($posts) > 0) { $output .= '<ul class="news">'; foreach($posts as $post) { $output .= '<li>'; $output .= '<h3><a href="'. get_permalink($post->ID) .'">'. $post->post_title .'</a></h3>'; $output .= '</li>'; } $output .= '</ul>'; } else { $output .= '<p>Sorry, there is no recent news.</p>'; } return $output; } } else : exit('WP-723-News has already been set up.'); endif; $WP723News = new WP723News(); if(isset($WP723News)) { register_activation_hook(__FILE__, array(&$WP723News, 'install')); register_deactivation_hook(__FILE__, array(&$WP723News, 'uninstall')); } ?>
If you have questions or comment, please feel free to leave a comment.
Davey:
Thank You for this very helpful tutorial! I am going to study it carefully over the next several days.
I’m going to comment separately on Linked In.
Thanks again!
Paula, No problem at all. I hope it helps.
Great article. Thanks. I’ve been looking for an article about how to write a plugin as a class. I am going to incorporate this into a plugin I am writing that will not only define a custom post type, but also set up some custom meta data fields for the post type. I am curious as to why you chose to not also write the widget code as a class extending the WP_Widget class.
Greg,
Thanks for stopping by. I didn’t write the widget as a class mostly because it is a pretty basic widget in this example. If the widget were going to provide more functionality, I would move it out into it’s own class. Regarding your plan to set up some custom meta data fields, I am going to write a follow up article to add that and more to this plugin.
If you want to share your work as a tutorial, I will gladly share a link here.
Check back for the follow up tutorials.
Greg,
Based on your comment, I checked out the Widget API a little more. I can see some ideas for going that route. I typically don’t do complex widgets but now I may check into it. Thanks.
I am going through your code, and noticed that the install and uninstall functions are missing from the class. What code typically is put into these functions? Specially, what code is put in the install function that is not put into the constructor?
Thanks for bringing that up. I typically use my constructor for registration only (setting up hooks and actions, registering js and css, etc). For this tutorial, the plugin is very basic so I didn’t touch on the install and uninstall yet.
What I use the install activation hook for is for table creation, copying of plugin specific template pages, etc. I then use the uninstall to drop tables or delete any files that I create/copy during the install.
You could technically use the constructor for this as well but I like to separate my plugins out into registration and installation as much as possible.
I’ve now got my custom post type working with a custom meta box consisting of a date field populated by a date picker. I did not have to do any work for this. There is a most excellent series of tutorials, the culmination of which is a class called RW_Meta_Box. All you need do is instantiate this class, and pass in an array of setup info (what fields you want, type, name, etc) into the constructor of this class and it does all the heavy lifting. He even as got a color picker type of field. Incredible. See:
http://www.deluxeblogtips.com/2010/04/how-to-create-meta-box-wordpress-post.html
http://www.deluxeblogtips.com/2010/05/howto-meta-box-wordpress.html
http://www.deluxeblogtips.com/2011/03/meta-box-script-update-v30.html
http://www.deluxeblogtips.com/p/meta-box-script-for-wordpress.html
Thanks for the links Greg. I’ll check that out.