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:

The Basics

Building an events post combines three concepts:

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
	 * http://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
 * http://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 )

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

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

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

	// 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 http://www.billerickson.net/customize-the-wordpress-query/
 * @param object $query data
function ep_event_query( $query ) {

	// http://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(
				'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 href="' . get_permalink() . '">' . get_the_title() . '</a></li>';
	echo '</ul>';


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.

Posted by:Devin

I'm a WordPress developer based in Austin, Texas. I run a little theme shop called DevPress and work for a startup called Nano. Find me on twitter @devinsays.

39 thoughts on “ Event Posts in WordPress ”

  1. 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.

  2. 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”.

    – 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: http://www.billerickson.net/customize-the-wordpress-query/

  3. 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.

  4. 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' ) )

  5. 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: http://imm.io/exaD

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

    Thank you :)

      1. 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 plugin. Then I altered just the line: $query->set( ‘posts_per_page’, ’20’ ); to show more posts at a time.

        This is what ended up happening: http://imm.io/eHZV

        All the events from the beginning of the months (1-9) show up on the top of the list. Any event after the 9th shows up on the bottom of the list. As I kept adding more events the more apparent this became. It seems like there are two different things going on with the date comparison?

      2. Hi Devin,

        The issue that I described happens for the 1-9 days because those are single digits. If I put in 01 – 09 instead of just 1-9 then the issue doesn’t occur. So to fix that, just do a single digit check on the _day meta and append a 0 to the beginning inside the ep_eventposts_save_meta function and the issue will not happen again. There might be a better way to fix this but it seems to work? Enjoy!

        – Vita

  6. Hi Devin

    Thank you for an awesome tutorial!

    I was wondering though, is the last code snippet supposed to only show upcoming events (and thereby hide past events)?
    In that case it doesn’t seem to work for me. The event posts show up like regular posts sorted by date of creation… I’d like to show the next upcoming events (like on the Archive Page). Do you have any suggestions?

    1. Yes. It should order by _start_eventtimestamp and show items that are only “>” than $current_timestamp. If you’re having issues I’d echo out all those parameters so you see where it might having the issue. The query worked for me in my tests.

      1. I’ve been looking into the issue again today… And it seems to me that the “compare” part of the equation is missing and so is $current_timestamp, but I might be wrong, as i’m not very code-savvy.
        Am looking closer into it tomorrow though…

        (to be more precise im referring to the query that goes into the sidebar or other pagetemplates than the archive-event.php)

      2. I finally figured it out and thought i’d share (guess its actually pretty basic after all).

        This should get the ten soonest upcoming events, hide past events and show the date and title of the events.

        You should be able to paste it anywhere in your wp-theme.

        Thanks again for a kick ass tutorial/snippet it’s a lifesaver (at least it saved me from a bunch of evil headaches)


  7. Is there a way to use this and have say an upcoming events section and past events section?

    Would it be a case of simply making two different templates for those pages and changing the loop to show posts that have passed? What would the code look like if so?


      1. Could you use the return old posts type function along with specifying it to the event post type? or would that work on a different system seen as it’s not technically the post date?

  8. I’m using the second query (“other queries”) to display my events in my sidebar. Is there an easy way to hide past events?

    1. Matt,
      You should be able to call the value of the year with this:
      ID, “_start_year”, true); ?>

      On a related note, does anyone know how I can call/output the name of the start month and date as opposed to just the numbers of the start month and date?


      1. hmm, looks like the last code snippet got cut off, let’s try this:

        echo get_post_meta($post->ID, “_start_year”, true);

  9. I’m trying to display event date using eventposttype_get_the_event_date() function, but somehow I’m getting empty value. Title, truncated event content appear correctly.

    Any idea what can be wrong?


    1. I think that function should looks like

      function eventposttype_get_the_event_date() {
      global $post;
      $eventdate = '';
      $month = get_post_meta($post->ID, '_start_month', true);
      $eventdate = eventposttype_get_the_month_abbr(ltrim($month,'0'));
      $eventdate .= ' ' . get_post_meta($post->ID, '_start_day', true) . ',';
      $eventdate .= ' ' . get_post_meta($post->ID, '_start_year', true);
      $eventdate .= ' at ' . get_post_meta($post->ID, '_start_hour', true);
      $eventdate .= ':' . get_post_meta($post->ID, '_start_minute', true);
      echo $eventdate;

      1. And US version for 12-hours time wit am/pm indicator

        function eventposttype_get_the_event_date() {
        global $post;
        $eventdate = '';
        $month = get_post_meta($post->ID, '_start_month', true);
        $eventdate = eventposttype_get_the_month_abbr(ltrim($month,'0'));
        $eventdate .= ' ' . get_post_meta($post->ID, '_start_day', true) . ',';
        $eventdate .= ' ' . get_post_meta($post->ID, '_start_year', true);
        $eventhour = get_post_meta($post->ID, '_start_hour', true);
        $eventminute = get_post_meta($post->ID, '_start_minute', true);
        $eventdate .= ' at ' . date('g:i A', strtotime($eventhour.':'.$eventminute.':00'));
        echo $eventdate;

  10. This is a great tutorial, and it’s exactly what I’m looking for. There ate two things though. The Location field doesn’t save and the Other Queries code doesn’t filter out old events. Is there a fix for these issues?

    1. I ran into the same issue with the Other Queries code not filtering out older events. I was able to find the answer in the code snippet above.

      Bascially, your query should look like this:

      $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;

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

  11. Hi Jason, I was wondering the same issue and I’ve one solution to save the location:

    You have to change the foreach statement:

    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 31 ) ? 31 : $jj;
    $jj = ($jj 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;

    $events_meta[‘_event_location’] = $_POST[‘_event_location’];

    // 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


  12. Hi there! I’ve found this example very useful, though I’m trying to figure my way round to doing something slightly different:

    On my events archive page, I’d like to first display any upcoming events (I never have more than five, so I’m not worried about any limits there) in ascending order i.e. from event closest to today at the top of the list to the event furthest away in the future.

    On the same page, I’d then like to display past events in descending order i.e. from the most recent old event to the oldest event in the archive (ideally with paging). Seems like a sensible way of doing an events archive to me, but trying to put the query together for a custom post-type is beyond my humble coding skills. Any pointers?

    Thanks in advance!

  13. Hi, I’ve found this code and the comments really useful. However I’m trying to create an if function in my single.php whereby it checks if the event is coming up in the next week. I’ve got…

    $date1 = eventposttype_get_the_eventtimestamp_event_date();
    $date2 = date('YmdHi', strtotime($date .' +7 day'));
    if ( $date1 AB

    And the date1 function links up to… (apologies for the butchered code, I know no better at the moment!)

    function eventposttype_get_the_eventtimestamp_event_date() {
    global $post;
    $eventdate = '';
    $eventdate .= '' . get_post_meta($post->ID, '_start_eventtimestamp', true) . '';

    I can only get it to output A. Any help is appreciated!

  14. Hi,

    I have this code implemented (quite) well on a site I am developing. I don’t suppose anyone knows how to make it so you don’t have to fill out an end time or date? Some of my events are multi-day some are single day you see.

    Thank you in advance.

  15. Thanks, this has been very helpful to me. Could anyone explain how I integrate a checkbox for the user to decide whether the event end date is to be displayed?

  16. Does it work for regular post or do I have to use a new kind of posts that won’t mix with my previous posts?
    That looks like a great tutorial but I don’t understand PHP much….

  17. I’m just trying to show the meta of the event type post by doing:

    $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 ”;
    while ( $events->have_posts() ) : $events->the_post();
    echo ‘‘ . get_the_title() . ‘‘;

    the_meta() // <_ show the meta
    echo '’;

    can somebody tell me why it’s not working?? . I’m trying to make a loop of event by showing title,content, location, and date.

Leave a Reply

Your email address will not be published. Required fields are marked *