How-to: iCal with Custom Post Types

In line with my previous custom post type tutorials for events (backend & frontend), I’d like to show how easy it is to create an iCal feed of those events (download the full file here). There are a few iCal for WordPress plug-ins out there, but none that will handle custom post types right off the bat. Besides, you’ll be setup in no time.

1) iCal requirements

iCal is Apple’s calendar application, but over time has gained widespread support & functionality in other tools (MS Outlook, GMail, Yahoo, etc.) so depending on your type of crowd, this sort of feature can really come in handy (think how easy it would be to grab and store dates to your mobile phone directly from WordPress).

2) ical.php

As is good practice, this sort of stand-alone functionality should be in its own file. The goal of this document will be to 1) create a function that constructs the output, and 2) registering that output as a feed. Let’s get right into it:

3) header information


function tf_events_ical() {

// - start collecting output -

// - file header -
header('Content-type: text/calendar');
header('Content-Disposition: attachment; filename="ical.ics"');

// - content header -
PRODID:-//<?php the_title(); ?>//NONSGML Events //EN
X-WR-CALNAME:<?php the_title(); _e(' - Events','themeforce'); ?>
X-ORIGINAL-URL:<?php echo the_permalink(); ?>
X-WR-CALDESC:<?php the_title(); _e(' - Events','themeforce'); ?>


As you may have seen in the previous tutorials, we’re using ob_start() to collect all outputted content after that point. We then tell the function that the actual document header should contain certain parameters that will 1) help it be identified as a calendar & 2) actually output as a downloadable .ics file when the feed is called upon. This is the right way about it as the end-user will be prompted for a download. They’ll then accept and open the file in a calendar application of their choice (which will import the events). Otherwise the user would just see a text feed that is unusable in this context.

Next up, we pump out some content header information. This stuff is essentially what meta information is to html (i.e. description, keywords, author), although iCal carries different information. I was lucky enough to stumble upon this site which had plenty of information on the subject.

4) events listing

At this point, we’re ready to list each event item in the correct iCal format. Before we create those items though, we’ll need to run a query to pull all the data in (and as you may recall, this looks just like the one in my previous tutorial):


// - grab date barrier -
$today6am = strtotime('today 6:00') + ( get_option( 'gmt_offset' ) * 3600 );
$limit = get_option('pubforce_rss_limit');

// - query -
global $wpdb;
$querystr = "
    SELECT *
    FROM $wpdb->posts wposts, $wpdb->postmeta metastart, $wpdb->postmeta metaend
    WHERE (wposts.ID = metastart.post_id AND wposts.ID = metaend.post_id)
    AND (metaend.meta_key = 'tf_events_enddate' AND metaend.meta_value > $today6am )
    AND metastart.meta_key = 'tf_events_enddate'
    AND wposts.post_type = 'tf_events'
    AND wposts.post_status = 'publish'
    ORDER BY metastart.meta_value ASC LIMIT $limit

$events = $wpdb->get_results($querystr, OBJECT);

// - loop -
if ($events):
global $post;
foreach ($events as $post):

My 6 am time limit is once again in place to only show relevant events (i.e. nothing that has already passed). The rest of the query is quite straightforward, but we’ll need to do one last thing before returning the individual events. iCal requires a special date format, especially in our case where we’ll be appending the character “Z” (that tells the importing calendar that the date will be set to the GMT+0 timezone). For example, 20100130T134500Z, is the 30th of January, 2010 – 1:45pm GMT+0. Appending the Z is good practice, especially if you only have a single person in another timezone.

// - custom variables -
$custom = get_post_custom(get_the_ID());
$sd = $custom["tf_events_startdate"][0];
$ed = $custom["tf_events_enddate"][0];

// - grab gmt for start -
$gmts = date('Y-m-d H:i:s', $sd);
$gmts = get_gmt_from_date($gmts); // this function requires Y-m-d H:i:s
$gmts = strtotime($gmts);

// - grab gmt for end -
$gmte = date('Y-m-d H:i:s', $ed);
$gmte = get_gmt_from_date($gmte); // this function requires Y-m-d H:i:s
$gmte = strtotime($gmte);

// - Set to UTC ICAL FORMAT -
$stime = date('Ymd\THis\Z', $gmts);
$etime = date('Ymd\THis\Z', $gmte);

Pfew, seems like a lot, but it should all make sense. We’re finally ready to output each event which will carry the following format:

// - item output -
DTSTART:<?php echo $stime; ?>
DTEND:<?php echo $etime; ?>
SUMMARY:<?php echo the_title(); ?>
DESCRIPTION:<?php the_excerpt_rss('', TRUE, '', 50); ?>
else :

That wasn’t that hard was it? Our loop completes here and all the content we need to generate is complete.

5) closing off

We then need to turn off output buffering and package it into a single variable that is then echoed. Finally, we register this content as a feed (‘add_feed‘) and add this action once WordPress has completed loading (‘init‘)

// - full output -
$tfeventsical = ob_get_contents();
echo $tfeventsical;

function add_tf_events_ical_feed () {
    // - add it to WP RSS feeds -
    add_feed('tf-events-ical', 'tf_events_ical');


At this point, we’re done and your feed (or rather downloadable file) is accessed through

your turn to speak

Can an iCal feed help your site or do you already use some form of it? Are there any enhancements you’d add, or code you’d write differently? Feel free to share anything you want, always great to get different opinions!

  • Great post, thank you. You mention that this creates a downloadable file. Would you then have to re-download it each time you made a change and then import it into your calendar app like Google Calendar or do you just add the feed URL once?

  • Temposaur

    @Chris: i’m not sure what you mean, but “” is the url of the feed, which you can enter to Your favourite Calendar App…

  • Tobias Nygren

    You can totally do that. I don’t think theres much difference between this solution and the one Facebook or is using for their event exports.

  • Guest

    Akron, NY LVD Strippit introduces Sirius, an automation ready flying optics laser cutting system. Sirius is designed to provide efficient processing of parts at optimal speeds and accelerations to suit the part geometry, offering reliable cutting performance at an affordable price performance ratio. Vertical Lathe is offered in a standard and a Plus model. The Sirius Plus is optimized with additional features and automation capabilities. Sirius is designed with a modular construction, permitting the user to select the configuration that works best for the application and budget. As a standard unit, the laser cutting system features 3 m by 1. 5 m integrated shuttle tables, which maximize uptime by allowing one table to be loaded while the machine is cutting on the other table. Table change time is a mere 25 seconds. Sirius Plus is engineered as automation ready and can be expanded with the addition of various components to form an automated load unload system. An optional compact tower system creates a productive, flexible manufacturing cell that can be operated “lights out. “The Drilling machine, working in concert with the material handling unit, provides full capabilities for loading and unloading, and includes a shelving unit for storing raw material and finished parts. Sirius is equipped with a laser cutting head that accommodates a 5 inch or 7. 5 inch quick change lens for fast changeover and minimal set up. These water cooled quick change lenses can be exchanged very easily, using a self centering system. Lens calibration is programmable and quick to achieve.

  • Fantastic set of tutorials – one thing I would point out is that the description value should be escaped for backlashes, commas and semi-colons by preceding them with a backslash. Also, you will loose any line breaks unless you insert ‘n’. This is something I noticed in developing an ‘events’ plug-in (resisting urge for shameless plug… :D)

  • Fantastic set of tutorials – one thing I would point out is that the description value should be escaped for backlashes, commas and semi-colons by preceding them with a backslash. Also, you will loose any line breaks unless you insert ‘n’. This is something I noticed in developing an ‘events’ plug-in (resisting urge for shameless plug… :D)

  • Wow, this looks really interesting!
    One question though; you say ‘this sort of stand-alone functionality should be in its own file’…
    I’m not sure how to do this; I’m supposed to call the ‘tf_events_ical()’ function from my functions.php right?
    Could you give me an example an how to do this?

  • I figured it out, but I’m stuck on my timestamp and really need some help…

    I’m using my own code for my custom post type and datepicker.
    My event date output is in the following format: yy-mm-dd
    So I try to convert it into yours: Y-m-d before converting it into the $gmts variable:

    $start_date = get_post_meta($post->ID, ‘eventstartdate’, true);
    $sd = date(“Y-m-d”, strtotime($start_date));

    $gmts = date(‘Y-m-d H:i:s’, $sd);
    $gmts = get_gmt_from_date($gmts); // this function requires Y-m-d H:i:s, hence the back & forth.
    $gmts = strtotime($gmts);

    But all my dates show up like this: 19691231T233250Z
    What am I doing wrong?

    Thanks for your time!

  • Well since I’m the only one here;
    Linebreaks should be done like this: ‘echo $stime . “n”;’

  • Sorry for being slow on my end.. few comments:

    @ Chris, that’s correct, though at the time I searched for a better solution, but it seems as if there’s no issue handling this way.

    @ Stephen, thanks for that, certainly makes sense.

    @ Toine, weird thing on the permalink. Assuming you’ve changed a fair bit of code to put it inline with 3.3, i.e. the SELECT query isn’t necessary anymore. Just seems to be pulling from somewhere else..

  • Why is my .ics file adding several spaces at the top? If I delete those spaces, the .ics file validates. Otherwise, it fails. If I take out the header lines from the code, the output displays nicely without the initial annoying spaces.

  • Kusumtwr

    hi thanks for good article on use of ical, i am using “RS events Multiday” on my blog and displaying upcoming events widget at side bar, i want show these upcoming events list on another blog in installed at sub directory. please suggest how can i achieve this, on the first blog “R S events multiday” is working fine for me but i think it doesn’t create calendar feed.

  • Anand S

    Is there any chance to create iCal files and save those into local server and attach them as a attachment? Please let me know.


  • Daniel

    For those of us who are not fluent in PHP – is there any out of the box solution (plugin) that lets me create an iCal feed from any give n custom post type? Thanks!

  • Joe

    I must have missed something. I get an error “ERROR: tf-events-ical. is not a valid feed template.”

  • Cory

    I’m running into the same issue as Joe…

  • Neil

    Adding this to functions.php worked for me (I put Noel’s code into ical.php):

    $ical = TEMPLATEPATH . ‘/ical.php’;
    load_template( $ical );

  • I was able to get this to work, after modifying it a bit (iCal doesn’t like tabs before declarations and the “n” suggestion below was helpful). One issue that I’m running into is two debug errors: “PHP Notice: Trying to get property of non-object in …/wp-includes/rewrite.php on line 88” and “PHP Warning: in_array() expects parameter 2 to be array, null given in …/wp-includes/rewrite.php on line 88”

    My guess is that there is an issue with the feed or headers? I’d love some suggestions, there is not a lot of information on building iCal for WordPress custom post types. Thanks for the great post!