How-to: Custom Post Type for Events

Chances are you found this page looking for a tutorial on how to create Events with Custom Post Types within WordPress. I will outline the entire process I’ve used for one of my themes that I’m re-coding (in order to make use of this great feature). You can also download the entire custom post type file that includes the code mentioned here. This is the visual output as is in testing today (please see the second tutorial for the code on outputting the design):

Whilst this tutorial shows all the code, I’ll go over the “default” parts quite quickly and then focus on any code relating to the actual Events component. As a designer, I’ve found it very manageable to put together something that 5 years ago would have been so much more work (and who likes manual work?). So if I can do it, anyone can (really).

1) event requirements

Before hitting the code, it’s important to understand what you really need so that you can strive towards the simplest end solution. For the events custom post type of my theme,  I need to be able to show the following items (beyond the default fields):

  • Start Date
  • Start Time
  • End Date
  • End Time

I debated if I even required an end date (2 more fields to fill out for the user), but figured I’d probably be limiting my options for some of the other ideas I have. The great thing about storing dates is that they’re done so using Unix Time (i.e. seconds elapsed since 1/1/1970 as a single integer). This means that we have tons of flexibility when it comes to extracting and showing the dates with default PHP functions. As such, whilst the end user will see 4 fields, we’re only storing 2 fields (thus my custom post type will only require those 2 extra custom fields).

One thing I wanted to avoid was also feature bloat. There are plenty of event plug-ins out there with advanced functionality (recurrence, rsvp, paypal, etc.). I’ll leave those awesome things to the plug-in designers, there’s absolutely no need to reinvent the wheel there. My primary goal is to design themes, and the out of the box functionality I provide is a simple foundation that works with the design (more on that in my next tutorial). So, onto the good stuff:

2) registering our custom post type

I’ll assume you’re already somewhat familiar with this aspect of the custom post type process (it’s actually very well documented within the WordPress Codex). I’m not doing anything special either, so feel free to have a look or just skip right onto the next section. All we’re doing is providing a set of labels or phrases to be used by the Custom Post Type and then defining the characteristics or behavior of the post type itself. Also, if you want to save yourself an afternoon, make sure the actual name of the custom post type (not label) is 20 characters or less.

// 1. Custom Post Type Registration (Events)
add_action( 'init', 'create_event_postype' );
function create_event_postype() {
$labels = array(
    'name' => _x('Events', 'post type general name'),
    'singular_name' => _x('Event', 'post type singular name'),
    'add_new' => _x('Add New', 'events'),
    'add_new_item' => __('Add New Event'),
    'edit_item' => __('Edit Event'),
    'new_item' => __('New Event'),
    'view_item' => __('View Event'),
    'search_items' => __('Search Events'),
    'not_found' =>  __('No events found'),
    'not_found_in_trash' => __('No events found in Trash'),
    'parent_item_colon' => '',
$args = array(
    'label' => __('Events'),
    'labels' => $labels,
    'public' => true,
    'can_export' => true,
    'show_ui' => true,
    '_builtin' => false,
    'capability_type' => 'post',
    'menu_icon' => get_bloginfo('template_url').'/functions/images/event_16.png',
    'hierarchical' => false,
    'rewrite' => array( "slug" => "events" ),
    'supports'=> array('title', 'thumbnail', 'excerpt', 'editor') ,
    'show_in_nav_menus' => true,
    'taxonomies' => array( 'tf_eventcategory', 'post_tag')
register_post_type( 'tf_events', $args);

3) custom taxonomy

This step is optional, but always a safe play for anyone who’d like the ability to categorize their events. Again, I’m not doing anything out of the ordinary (except maybe making it hierarchical). Don’t forget to attach the taxonomy to the post type:

function create_eventcategory_taxonomy() {
$labels = array(
    'name' => _x( 'Categories', 'taxonomy general name' ),
    'singular_name' => _x( 'Category', 'taxonomy singular name' ),
    'search_items' =>  __( 'Search Categories' ),
    'popular_items' => __( 'Popular Categories' ),
    'all_items' => __( 'All Categories' ),
    'parent_item' => null,
    'parent_item_colon' => null,
    'edit_item' => __( 'Edit Category' ),
    'update_item' => __( 'Update Category' ),
    'add_new_item' => __( 'Add New Category' ),
    'new_item_name' => __( 'New Category Name' ),
    'separate_items_with_commas' => __( 'Separate categories with commas' ),
    'add_or_remove_items' => __( 'Add or remove categories' ),
    'choose_from_most_used' => __( 'Choose from the most used categories' ),
register_taxonomy('tf_eventcategory','tf_events', array(
    'label' => __('Event Category'),
    'labels' => $labels,
    'hierarchical' => true,
    'show_ui' => true,
    'query_var' => true,
    'rewrite' => array( 'slug' => 'event-category' ),
add_action( 'init', 'create_eventcategory_taxonomy', 0 );

4) creating & showing columns

This is where it starts to get more interesting. Within the dashboard, custom post types are listed the same way as regular posts & pages are, however we’re the ones who need to make it all happen. This is what our final output will look like:

As you’ll see from the code below, we need to create a function to define our headers (tf_events_edit_columns), and then another function to define with which content we’re going to populate it with (tf_events_custom_columns). Beyond the standard fields (title, description, etc.), I’m also going to show my times here, and this is thought process behind the event-related fields:

  • Dates – Here I’m grabbing the dates and hardcoding which format they should be output in. Dates are more or less universal, whereby times aren’t (i.e. 24hr vs AM/PM). Seeing as (in my case) the end date will almost always be the same as the start, I’ve subdued its visual effect to reduce the clutter a bit.
  • Times – As I mentioned above, 24hr vs AM/PM is an important factor. So to make this easy on the end-user we’ll grab the local format using get_option(‘time_format’) which we can then feed right into the date() function.
// 3. Show Columns
add_filter ("manage_edit-tf_events_columns", "tf_events_edit_columns");
add_action ("manage_posts_custom_column", "tf_events_custom_columns");
function tf_events_edit_columns($columns) {
$columns = array(
    "cb" => "<input type=\"checkbox\" />",
    "tf_col_ev_cat" => "Category",
    "tf_col_ev_date" => "Dates",
    "tf_col_ev_times" => "Times",
    "tf_col_ev_thumb" => "Thumbnail",
    "title" => "Event",
    "tf_col_ev_desc" => "Description",
return $columns;
function tf_events_custom_columns($column)
global $post;
$custom = get_post_custom();
switch ($column)
case "tf_col_ev_cat":
    // - show taxonomy terms -
    $eventcats = get_the_terms($post->ID, "tf_eventcategory");
    $eventcats_html = array();
    if ($eventcats) {
    foreach ($eventcats as $eventcat)
    array_push($eventcats_html, $eventcat->name);
    echo implode($eventcats_html, ", ");
    } else {
    _e('None', 'themeforce');;
case "tf_col_ev_date":
    // - show dates -
    $startd = $custom["tf_events_startdate"][0];
    $endd = $custom["tf_events_enddate"][0];
    $startdate = date("F j, Y", $startd);
    $enddate = date("F j, Y", $endd);
    echo $startdate . '<br /><em>' . $enddate . '</em>';
case "tf_col_ev_times":
    // - show times -
    $startt = $custom["tf_events_startdate"][0];
    $endt = $custom["tf_events_enddate"][0];
    $time_format = get_option('time_format');
    $starttime = date($time_format, $startt);
    $endtime = date($time_format, $endt);
    echo $starttime . ' - ' .$endtime;
case "tf_col_ev_thumb":
    // - show thumb -
    $post_image_id = get_post_thumbnail_id(get_the_ID());
    if ($post_image_id) {
    $thumbnail = wp_get_attachment_image_src( $post_image_id, 'post-thumbnail', false);
    if ($thumbnail) (string)$thumbnail = $thumbnail[0];
    echo '<img src="';
    echo bloginfo('template_url');
    echo '/timthumb/timthumb.php?src=';
    echo $thumbnail;
    echo '&h=60&w=60&zc=1" alt="" />';
case "tf_col_ev_desc";

In order to save space, I’ve combined my start & end dates into one column, as well as my start and end times. Again, this makes it all a little easier on the user. Here is also the CSS used which I’m only calling within the admin area (it’s a great help as it can really reduce those wide columns on larger monitors):

/* Columns CPT Events */
th#tf_col_ev_date, th#tf_col_ev_cat {width:150px}
td.tf_col_ev_date em {color:gray;}
th#tf_col_ev_times {width:150px}
th#tf_col_ev_thumb {width:100px}

5) show meta box

We then need to provide our users with fields they can fill out, for which a custom meta box is required. It’d be nice if WordPress would automatically do that for a predefined set of field types (text, date, checkbox, dropdown, etc.), however this hasn’t made its way into the system yet. So with the current state of things, you’ll need to beautify the look of it yourself. On a positive note, WordPress 3.1 will use jQuery 1.4.4 and jQuery UI 1.8.6., at least we’ll be somewhat up-to-date there. For the purpose of this tutorial, I’ve left out any fancy formatting or branding, this being the final look:

In the code below, we’ll register the meta-box, link it to the custom post type and then create the form. Again, here is the thought process behind the event-related fields:

  • Split the Database Date Value – As you may recall, I’m using 2 custom fields in the database to output 4 visible data-entry fields. I do this by using the date() function for both, but only outputting the *date* within the first variable, and only the *time* within the second.
  • If Empty, show Today – Zero in Unix Time is 1970. Clearly, it’d be tough to scroll through 40 years worth of months for the end-user (talk about the quickest refund in the history of WordPress). As such, if we have no value in the database (i.e. when we’re creating a new event), we’ll populate it with todays date. If you don’t want the current time to be shown (remember how we split 1 database field into 2 meta fields), then we’ll also need to change the new time field to 0.
  • Strong Date Format – You’ll notice here that I predefined the date format to “D, M d, Y“. When we’re going to use the strtotime() function later on to save our data, it needs to function for all sorts of dates. When experimenting with shorter date formats I noticed that often the day & month would get switched (US vs Euro date styles). By using “D, M d, Y” (i.e. “Fri, Feb 11, 2011” I not only have something that is visually functional but also feeds back into the database without any issues.

When looking at the code, you could argue that we need more controls & validation. I understand that to a degree, but people aren’t THAT stupid. If I’m asking them to input “14:00” for 2pm, 99% will do it on the first try and the remaining 1% would have learned it on their second attempt. Label me pragmatic, but don’t call my clients stupid :)

// 4. Show Meta-Box
add_action( 'admin_init', 'tf_events_create' );
function tf_events_create() {
    add_meta_box('tf_events_meta', 'Events', 'tf_events_meta', 'tf_events');
function tf_events_meta () {
// - grab data -
global $post;
$custom = get_post_custom($post->ID);
$meta_sd = $custom["tf_events_startdate"][0];
$meta_ed = $custom["tf_events_enddate"][0];
$meta_st = $meta_sd;
$meta_et = $meta_ed;
// - grab wp time format -
$date_format = get_option('date_format'); // Not required in my code
$time_format = get_option('time_format');
// - populate today if empty, 00:00 for time -
if ($meta_sd == null) { $meta_sd = time(); $meta_ed = $meta_sd; $meta_st = 0; $meta_et = 0;}
// - convert to pretty formats -
$clean_sd = date("D, M d, Y", $meta_sd);
$clean_ed = date("D, M d, Y", $meta_ed);
$clean_st = date($time_format, $meta_st);
$clean_et = date($time_format, $meta_et);
// - security -
echo '<input type="hidden" name="tf-events-nonce" id="tf-events-nonce" value="' .
wp_create_nonce( 'tf-events-nonce' ) . '" />';
// - output -
<div class="tf-meta">
    <li><label>Start Date</label><input name="tf_events_startdate" class="tfdate" value="<?php echo $clean_sd; ?>" /></li>
    <li><label>Start Time</label><input name="tf_events_starttime" value="<?php echo $clean_st; ?>" /><em>Use 24h format (7pm = 19:00)</em></li>
    <li><label>End Date</label><input name="tf_events_enddate" class="tfdate" value="<?php echo $clean_ed; ?>" /></li>
    <li><label>End Time</label><input name="tf_events_endtime" value="<?php echo $clean_et; ?>" /><em>Use 24h format (7pm = 19:00)</em></li>

Also, some very basic CSS to structure it a little:

/* Metabox */
.tf-meta {  }
.tf-meta ul li { height: 20px; clear:both; margin: 0 0 15px 0;}
.tf-meta ul li label { width: 100px; display:block; float:left; padding-top:4px; }
.tf-meta ul li input { width:125px; display:block; float:left; }
.tf-meta ul li em { width: 200px; display:block; float:left; color:gray; margin-left:10px; padding-top: 4px}

Furthermore, I tweaked the jQuery a little to show 3 months right off the bat as well as the calendar icon. I think this helps the end-user (and especially my clients). Keep in mind that the following code is in a separate .js file, not in the document directly (as you’d need to switch out the $ with jQuery). The last section of the article will also show you how to implement the datepicker:

    dateFormat: 'D, M d, yy',
    showOn: 'button',
    buttonImage: '/yourpath/icon-datepicker.png',
    buttonImageOnly: true,
    numberOfMonths: 3

6) save meta box

Saving the custom data deserves a little section of it’s own, but the code is all quite standard. Check Nonce, ok? Check if Current User is allowed, ok? Save the data. The only special part is where we take the 4 events-related input fields and merge them back to the 2 database fields. As we’ve kept everything nice and simple so far, it’s no issue, we’ll just merge the date & time fields and wrap it in a strtotime() function, like so: $fulldate = strtotime ( $date . $time ). As we’ve been using “safe” date & time formats, there’ll be no issue here. Obviously the actual code requires all the other WordPress parameters to make it work, but nothing out of the ordinary:

// 5. Save Data
add_action ('save_post', 'save_tf_events');
function save_tf_events(){
global $post;
// - still require nonce
if ( !wp_verify_nonce( $_POST['tf-events-nonce'], 'tf-events-nonce' )) {
    return $post->ID;
if ( !current_user_can( 'edit_post', $post->ID ))
    return $post->ID;
// - convert back to unix & update post
return $post;
$updatestartd = strtotime ( $_POST["tf_events_startdate"] . $_POST["tf_events_starttime"] );
update_post_meta($post->ID, "tf_events_startdate", $updatestartd );
return $post;
$updateendd = strtotime ( $_POST["tf_events_enddate"] . $_POST["tf_events_endtime"]);
update_post_meta($post->ID, "tf_events_enddate", $updateendd );

7) customize update messages

I think this part is often overlooked in custom post types tutorials but makes the usability aspect so much cleaner. Whenever a user updates something, we’d like it to use events or a similar phrase to update the user instead of “post”. My final code is below, but it’s almost an exact match to what’s in the Codex under the register_post_type page.

// 6. Customize Update Messages
add_filter('post_updated_messages', 'events_updated_messages');
function events_updated_messages( $messages ) {
  global $post, $post_ID;
  $messages['tf_events'] = array(
    0 => '', // Unused. Messages start at index 1.
    1 => sprintf( __('Event updated. <a href="%s">View item</a>'), esc_url( get_permalink($post_ID) ) ),
    2 => __('Custom field updated.'),
    3 => __('Custom field deleted.'),
    4 => __('Event updated.'),
    /* translators: %s: date and time of the revision */
    5 => isset($_GET['revision']) ? sprintf( __('Event restored to revision from %s'), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false,
    6 => sprintf( __('Event published. <a href="%s">View event</a>'), esc_url( get_permalink($post_ID) ) ),
    7 => __('Event saved.'),
    8 => sprintf( __('Event submitted. <a target="_blank" href="%s">Preview event</a>'), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
    9 => sprintf( __('Event scheduled for: <strong>%1$s</strong>. <a target="_blank" href="%2$s">Preview event</a>'),
      // translators: Publish box date format, see
      date_i18n( __( 'M j, Y @ G:i' ), strtotime( $post->post_date ) ), esc_url( get_permalink($post_ID) ) ),
    10 => sprintf( __('Event draft updated. <a target="_blank" href="%s">Preview event</a>'), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
  return $messages;

8) Custom jQuery UI for Events only

Last but not least, in order to make the jQuery Datepicker work in the WordPress admin area, we’ll want do some configuring. We’ll de-register the default WP jQuery UI file and then register our custom files downloaded from the official jQuery site (or linked to from the Google jQuery CDN). You’ll notice the pubforce-admin.js below, that will contain our datepicker settings mentioned above in the meta box section of this tutorial.

// 7. JS Datepicker UI
function events_styles() {
    global $post_type;
    if( 'tf_events' != $post_type )
    wp_enqueue_style('ui-datepicker', get_bloginfo('template_url') . '/css/jquery-ui-1.8.9.custom.css');
function events_scripts() {
    global $post_type;
    if( 'tf_events' != $post_type )
    wp_enqueue_script('jquery-ui', get_bloginfo('template_url') . '/js/jquery-ui-1.8.9.custom.min.js', array('jquery'));
    wp_enqueue_script('ui-datepicker', get_bloginfo('template_url') . '/js/jquery.ui.datepicker.min.js');
    wp_enqueue_script('custom_script', get_bloginfo('template_url').'/js/pubforce-admin.js', array('jquery'));
add_action( 'admin_print_styles-post.php', 'events_styles', 1000 );
add_action( 'admin_print_styles-post-new.php', 'events_styles', 1000 );
add_action( 'admin_print_scripts-post.php', 'events_scripts', 1000 );
add_action( 'admin_print_scripts-post-new.php', 'events_scripts', 1000 );

9) your turn to speak !

It may look like a lot of code (feel free to download the entire code here), but for the functionality and flexibility provided it’s really automated. If you have any questions, feedback or suggestions, feel free to comment below. Like said, I’m not a developer so I’m more then open to ideas and would also update the code above (& provide credit). Re-tweet if you feel like it too ;) Thank you!

I have already released Part 2: Query, Loop & Design Output , check it out!

Here are also the next tutorials planned in connection with this event post type:

other articles in this series

  1. Front-end: Design & Shortcodes
  2. Front-end: iCal Export
  3. Front-end: Implementing FullCalender

related links