Event Posts in WordPress

WordPress is a constantly maturing platform. Just over a year ago I wrote a tutorial about creating custom post types for events- but huge improvements around advanced meta data queries made my previous approach seem hackish. This post is a much revised update with code examples.

If you need an out-of-the-box solution and aren’t interested in customizing the code, one of these plugins might be a quicker and better solution:

  • Sugar Event Calendar (Lite)
  • The Events Calender
  • GigPress (For Musicans)

The Basics

Building an events post combines three concepts:

  • Custom Post Types
  • Custom Metaboxes
  • Advanced Meta Queries

Download the Code

I built a working plugin with everything I describe here up on GitHub. You can install the “Events Posts” plugin, and move the file “archive-events.php” into your theme in order to follow along.

Download the Code on GitHub

The Post Type

I create a new post type for events. You could also use a regular post if you don’t want a separation between your regular posts and event posts, but for the purposes of this tutorial it makes it easier to explain.

If you’re not familiar with custom post types, read Justin Tadlock’s excellent write up.

Here’s how you would create a custom post type for events (view on GitHub):

function ep_eventposts() {

	/**
	 * Enable the event custom post type
	 * codex.wordpress.org/Function_Reference/register_post_type
	 */

	$labels = array(
		'name' => __( 'Events', 'eventposttype' ),
		'singular_name' => __( 'Event', 'eventposttype' ),
		'add_new' => __( 'Add New Event', 'eventposttype' ),
		'add_new_item' => __( 'Add New Event', 'eventposttype' ),
		'edit_item' => __( 'Edit Event', 'eventposttype' ),
		'new_item' => __( 'Add New Event', 'eventposttype' ),
		'view_item' => __( 'View Event', 'eventposttype' ),
		'search_items' => __( 'Search Events', 'eventposttype' ),
		'not_found' => __( 'No events found', 'eventposttype' ),
		'not_found_in_trash' => __( 'No events found in trash', 'eventposttype' )
	);

	$args = array(
    	'labels' => $labels,
    	'public' => true,
		'supports' => array( 'title', 'editor', 'thumbnail', 'comments' ),
		'capability_type' => 'post',
		'rewrite' => array("slug" => "event"), // Permalinks format
		'menu_position' => 5,
		'menu_icon' => plugin_dir_url( __FILE__ ) . '/images/calendar-icon.gif',  // Icon Path
		'has_archive' => true
	); 

	register_post_type( 'event', $args );
}

add_action( 'init', 'ep_eventposts' );

The Metaboxes

For the user to be able to select an event start time or event end time, you’ll need to define a couple meta boxes. My example puts two metaboxes in the right side of the post, and one for location under the main editor.

I’ve written other tutorials about metaboxes if you need a more in-depth overview.

This is a long code snippet, but the basic idea is that we get the current time and populate the metaboxes of a fresh post with that. When someone saves a post, it checks that they have permissions to edit, and then overwrites the metabox data if it has changed.

/**
 * Adds event post metaboxes for start time and end time
 * codex.wordpress.org/Function_Reference/add_meta_box
 *
 * We want two time event metaboxes, one for the start time and one for the end time.
 * Two avoid repeating code, we'll just pass the $identifier in a callback.
 * If you wanted to add this to regular posts instead, just swap 'event' for 'post' in add_meta_box.
 */

function ep_eventposts_metaboxes() {
	add_meta_box( 'ept_event_date_start', 'Start Date and Time', 'ept_event_date', 'event', 'side', 'default', array( 'id' => '_start') );
	add_meta_box( 'ept_event_date_end', 'End Date and Time', 'ept_event_date', 'event', 'side', 'default', array('id'=>'_end') );
	add_meta_box( 'ept_event_location', 'Event Location', 'ept_event_location', 'event', 'normal', 'default', array('id'=>'_end') );
}
add_action( 'admin_init', 'ep_eventposts_metaboxes' );

// Metabox HTML

function ept_event_date($post, $args) {
	$metabox_id = $args['args']['id'];
	global $post, $wp_locale;

	// Use nonce for verification
	wp_nonce_field( plugin_basename( __FILE__ ), 'ep_eventposts_nonce' );

	$time_adj = current_time( 'timestamp' );
	$month = get_post_meta( $post->ID, $metabox_id . '_month', true );

	if ( empty( $month ) ) {
		$month = gmdate( 'm', $time_adj );
	}

	$day = get_post_meta( $post->ID, $metabox_id . '_day', true );

	if ( empty( $day ) ) {
		$day = gmdate( 'd', $time_adj );
	}

	$year = get_post_meta( $post->ID, $metabox_id . '_year', true );

	if ( empty( $year ) ) {
		$year = gmdate( 'Y', $time_adj );
	}

	$hour = get_post_meta($post->ID, $metabox_id . '_hour', true);
 
    if ( empty($hour) ) {
        $hour = gmdate( 'H', $time_adj );
    }
 
    $min = get_post_meta($post->ID, $metabox_id . '_minute', true);
 
    if ( empty($min) ) {
        $min = '00';
    }

	$month_s = '<select name="' . $metabox_id . '_month">';
	for ( $i = 1; $i < 13; $i = $i +1 ) {
		$month_s .= "\t\t\t" . '<option value="' . zeroise( $i, 2 ) . '"';
		if ( $i == $month )
			$month_s .= ' selected="selected"';
		$month_s .= '>' . $wp_locale->get_month_abbrev( $wp_locale->get_month( $i ) ) . "</option>\n";
	}
	$month_s .= '</select>';

	echo $month_s;
	echo '<input type="text" name="' . $metabox_id . '_day" value="' . $day  . '" size="2" maxlength="2" />';
    echo '<input type="text" name="' . $metabox_id . '_year" value="' . $year . '" size="4" maxlength="4" /> @ ';
    echo '<input type="text" name="' . $metabox_id . '_hour" value="' . $hour . '" size="2" maxlength="2"/>:';
    echo '<input type="text" name="' . $metabox_id . '_minute" value="' . $min . '" size="2" maxlength="2" />';
 
}

function ept_event_location() {
	global $post;
	// Use nonce for verification
	wp_nonce_field( plugin_basename( __FILE__ ), 'ep_eventposts_nonce' );
	// The metabox HTML
	$event_location = get_post_meta( $post->ID, '_event_location', true );
	echo '<label for="_event_location">Location:</label>';
	echo '<input type="text" name="_event_location" value="' . $event_location  . '" />';
}

// Save the Metabox Data

function ep_eventposts_save_meta( $post_id, $post ) {

	if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
		return;

	if ( !isset( $_POST['ep_eventposts_nonce'] ) )
		return;

	if ( !wp_verify_nonce( $_POST['ep_eventposts_nonce'], plugin_basename( __FILE__ ) ) )
		return;

	// Is the user allowed to edit the post or page?
	if ( !current_user_can( 'edit_post', $post->ID ) )
		return;

	// OK, we're authenticated: we need to find and save the data
	// We'll put it into an array to make it easier to loop though
	
	$metabox_ids = array( '_start', '_end' );

	foreach ($metabox_ids as $key ) {
	    
	    $aa = $_POST[$key . '_year'];
		$mm = $_POST[$key . '_month'];
		$jj = $_POST[$key . '_day'];
		$hh = $_POST[$key . '_hour'];
		$mn = $_POST[$key . '_minute'];
		
		$aa = ($aa <= 0 ) ? date('Y') : $aa;
		$mm = ($mm <= 0 ) ? date('n') : $mm;
		$jj = sprintf('%02d',$jj);
		$jj = ($jj > 31 ) ? 31 : $jj;
		$jj = ($jj <= 0 ) ? date('j') : $jj;
		$hh = sprintf('%02d',$hh);
		$hh = ($hh > 23 ) ? 23 : $hh;
		$mn = sprintf('%02d',$mn);
		$mn = ($mn > 59 ) ? 59 : $mn;
		
		$events_meta[$key . '_year'] = $aa;
		$events_meta[$key . '_month'] = $mm;
		$events_meta[$key . '_day'] = $jj;
		$events_meta[$key . '_hour'] = $hh;
		$events_meta[$key . '_minute'] = $mn;
	    $events_meta[$key . '_eventtimestamp'] = $aa . $mm . $jj . $hh . $mn;  
        }

	// Add values of $events_meta as custom fields

	foreach ( $events_meta as $key => $value ) { // Cycle through the $events_meta array!
		if ( $post->post_type == 'revision' ) return; // Don't store custom data twice
		$value = implode( ',', (array)$value ); // If $value is an array, make it a CSV (unlikely)
		if ( get_post_meta( $post->ID, $key, FALSE ) ) { // If the custom field already has a value
			update_post_meta( $post->ID, $key, $value );
		} else { // If the custom field doesn't have a value
			add_post_meta( $post->ID, $key, $value );
		}
		if ( !$value ) delete_post_meta( $post->ID, $key ); // Delete if blank
	}

}

add_action( 'save_post', 'ep_eventposts_save_meta', 1, 2 );

/**
 * Helpers to display the date on the front end
 */

// Get the Month Abbreviation
 
function eventposttype_get_the_month_abbr($month) {
    global $wp_locale;
    for ( $i = 1; $i < 13; $i = $i +1 ) {
                if ( $i == $month )
                    $monthabbr = $wp_locale->get_month_abbrev( $wp_locale->get_month( $i ) );
                }
    return $monthabbr;
}
 
// Display the date
 
function eventposttype_get_the_event_date() {
    global $post;
    $eventdate = '';
    $month = get_post_meta($post->ID, '_month', true);
    $eventdate = eventposttype_get_the_month_abbr($month);
    $eventdate .= ' ' . get_post_meta($post->ID, '_day', true) . ',';
    $eventdate .= ' ' . get_post_meta($post->ID, '_year', true);
    $eventdate .= ' at ' . get_post_meta($post->ID, '_hour', true);
    $eventdate .= ':' . get_post_meta($post->ID, '_minute', true);
    echo $eventdate;
}

Displaying Event Posts on Archive Pages

To display event posts on archive pages you should run a pre_get_posts filter (hat tip to Bill Erickson). This example filter will display the event posts five per page, sorted by their _start_eventtimestamp meta key in ascending order, and only display posts that have a start time that is later than the current time.

The following code could be placed in functions.php. For the example plugin, it’s already in event-posts.php.

/**
 * Customize Event Query using Post Meta
 * 
 * @link www.billerickson.net/customize-the-wordpress-query/
 * @param object $query data
 *
 */
function ep_event_query( $query ) {

	// codex.wordpress.org/Function_Reference/current_time
	$current_time = current_time('mysql'); 
	list( $today_year, $today_month, $today_day, $hour, $minute, $second ) = split( '([^0-9])', $current_time );
	$current_timestamp = $today_year . $today_month . $today_day . $hour . $minute;

	global $wp_the_query;
	
	if ( $wp_the_query === $query && !is_admin() && is_post_type_archive( 'event' ) ) {
		$meta_query = array(
			array(
				'key' => '_start_eventtimestamp',
				'value' => $current_timestamp,
				'compare' => '>'
			)
		);
		$query->set( 'meta_query', $meta_query );
		$query->set( 'orderby', 'meta_value_num' );
		$query->set( 'meta_key', '_start_eventtimestamp' );
		$query->set( 'order', 'ASC' );
		$query->set( 'posts_per_page', '5' );
	}

}

add_action( 'pre_get_posts', 'ep_event_query' );

Other Queries

If you just wanted to display the event posts in the sidebar or on a different template (and not worry about paging), you could do something like this:

 $args = array( 'post_type' => 'event',
'meta_key' => '_start_eventtimestamp',
'orderby'=> 'meta_value_num',
'order' => 'ASC',
'posts_per_page' => 20,
 );
 $events = new WP_Query( $args );

if ( $events->have_posts() ) :
	echo '<ul>';
	while ( $events->have_posts() ) : $events->the_post();
		echo '<li><a class="' . get_permalink() . '">' . get_the_title() . '</a></li>';
	endwhile;
	echo '</ul>';
endif;

Extending

This tutorial isn’t meant to be a full-fledged plugin- just an example to get you started. Several improvements could be made, such as using a jquery datepicker to select the date (or even just making sure you end time is after your start time and a valid date). If your primary users are in the United States or other countries on a 12-hour clock, you might want to use an AM/PM selector.

If you’re interested in learning more, check out Noel Tock’s excellent events tutorial which covers some of these examples.

About Devin

I'm a WordPress developer based in Austin, Texas. I run a little theme shop called DevPress, and am @devinsays on twitter.
Custom Post Types

38 thoughts on “Event Posts in WordPress

  1. Derek says:

    I’m glad other people are finding that the new supports in WordPress have quickly made old approaches seem cumbersome and archaic.

    This looks like a pretty good basis for an event list. As much as I am enjoying register_post_types and its ability to create just about everything I need, I noticed that you did not include custom_fields support. It makes me wonder, since I always make sure to include it (adding post meta from the back-end can be a lifesaver): Have you been able to edit custom meta fields directly from the dashboard without enabling that support?

    Seems like a silly question.

    Reply
    1. Devin says:

      No, if you wanted to use custom fields as well you’d need to enable that in the supports. Good point.

      Reply
  2. Bill Erickson says:

    Great tutorial. My only complaint is that you’re using a custom query rather than modifying the default for an event archive (archive-events.php). This is especially important when it comes to your pagination “bug fix”.

    Example:
    – WP default is 10 posts per page
    – Your custom query has 4 posts per page and uses ‘paged’ to determine the page
    – There’s 5 posts in the events post type.

    Going to page 2 will 404. WordPress will first look for events 11-20 which don’t exist, then return a 404 page, before getting to your custom query.

    Instead, hook something like this to pre_get_posts: https://gist.github.com/1238281

    More info: www.billerickson.net/customize-the-wordpress-query/

    Reply
    1. Devin says:

      I’d say that’s a valid complaint. I updated the example plugin and post to use your pre_get_posts method. Makes much more sense now. Thanks!

      Reply
  3. Dan says:

    Hey Devin, I downloaded your github version and saw the contents of event-posts.php dump in my header. Noticed that on line 1 of event-posts.php you didn’t properly open your php tag. May want to check into that. Checking out the plugin further now! Thank you for your contribution.

    Reply
    1. Devin says:

      Thanks. Updated it.

      Reply
  4. Kwame says:

    Hi Devin,

    Thanks for the code. I am using this in a project I am currently working on. I want to build a “search events by date/month” search form where I can search for events occurring between say, January and July. How do I get the input boxes to grab the dates for the search form to process the request.

    I have added this to functions.php:

    'meta_query' => array( array( 'key' => 'event_date', 'value' => array( $month, $month_s), 'compare' => 'BETWEEN', 'type'=>'DATE' ) )

    Reply
  5. Niklas Högefjord says:

    As always, you provide the WP community with excellent tutorials! Thanks once again Devin for a good article. This will be of help in future projects!

    Reply
  6. Vita says:

    Hi Devin,

    Thank you for sharing this amazing tutorial and plugin! I have found great use for it. I have stumbled upon an issue that I can’t quite figure out and was hoping you could lend a hand.

    I was able to get all the event posts to show up correctly in chronological order except for the partial month of February. Any event that I set the start time to be in February (1-9) ends up on the top of the list. Any February event that starts on the 10th or after shows up fine. Here is an example: imm.io/exaD

    Would you happen to know why this is happening and how to fix this issue?

    Thank you :)

    Reply
    1. Devin says:

      Did you alter the code at all? It looks like eventtimestamp is correct. Are you sure it’s been ordered by that parameter?

      Reply
      1. Vita says:

        Hi Devin,

        Thank you for your reply!

        Yes, I altered some code in the plugin. I just created a fresh new install of WordPress and a fresh install of your plu