Textpattern tips, tutorials and code snippets

Outputting an .ics calendar file from a Textpattern article

In order to allow your site’s visitors to be able to download the dates of event(s), gig(s), class(es), meeting(s), tour(s) to their computer’s calendar, read on and follow these steps:

Required plugins

For this tip, you’ll need three plugins:

If you do not already use them, please install and activate them (instructions here).

Storing events in Textpattern articles

For this example, we’re going to assume the following setup:

1. A section for your events. In this example, it’s called courses.

2. A Textpattern article for each event, assigned to the above section, with five extra custom fields:

  • TITLE: Title of the event if different to txp:title.
  • DTSTART: Event start date and time in the format YYYY-MM-DD HH:MM.
  • DTEND: Event end date and time in the format YYYY-MM-DD HH:MM.
  • LOCATION: Event location
  • URL: URL of an information page if different to that of the event article.

Place any short description of the event that should appear in your calendar in the Excerpt field.

Use categories if you have different kinds of events that should be downloadable as separate calendars, for example containing just beginners, intermediate or advanced classes.

One more thing: set the expiry date of the Textpattern article to shortly after DTEND. That avoids dates in the past being published to the calendar.

Create the forms

Create three forms as follows:

rah_eo_generate-ics
  • Type: misc
  • Input: event = article_id number

This generates an .ics file for a single event.

; Content-Type: text/calendar
; Content-Disposition: attachment; filename=event.ics
<txp:smd_wrap_all>
<txp:rah_gps name="event" />
<txp:php>
  global $variable;
  if( !ctype_digit($variable['event']) ){
    $variable['event'] = '';
  }
</txp:php>
<txp:if_variable name="event" value=""><txp:else />
<txp:article_custom id='<txp:variable name="event" />' section="courses" form="ics-item" limit="1" />
</txp:if_variable>
</txp:smd_wrap_all>

Pass the article id number as the url variable event to output an ics for that article, for example:

<a href="/?rah_external_output=generate-ics&event=<txp:article_id />">Add date to my calendar (download .ics file)</a>

If there is no event variable, or the article id is not in the designated event section (here courses), the generated ics file will contain nothing.

rah_eo_generate-ics-calendar
  • Type: misc
  • Input: cal = category-name or all.

This generates an .ics file for a calendar containing multiple events.

; Content-Type: text/calendar
; Content-Disposition: attachment; filename=event-calendar.ics
<txp:smd_wrap_all><txp:rah_gps name="cal" />
<txp:if_variable name="cal" value=""><txp:else />
<txp:if_variable name="cal" value="all"><txp:variable name="cal" value="" /></txp:if_variable>
  <txp:article_custom category='<txp:variable name="cal" />' form="ics-item" time="any" section="courses" sortdir="asc" limit="10000" expired="0" break="" />
</txp:if_variable>
</txp:smd_wrap_all>

Pass the Textpattern category for the desired event category as the url variable cal to output an ics calendar for those articles:

<a href="/?rah_external_output=generate-ics-calendar&cal=category-name">Add all “Category Title” class dates to my calendar (download .ics file)</a>

Using this, you can output separate calendar dates for all beginner, intermediate or advanced classes.

Alternatively use the following to output all dates from all (non-expired) articles:

<a href="/?rah_external_output=generate-ics-calendar&cal=all">Add all available dates to my calendar (download .ics file)</a>

If there is no cal variable, or there is no such category, or no non-expired articles in the designated event section (here courses), the generated ics file will contain nothing.

What the form does: The above forms are similar. Each sets the file content-type to a calendar and defines the download name, causing the form output to be downloaded instead of displayed in the browser. rah_gps captures the GET variable in the link (i.e. event or cal). The form checks first that they are not empty (and in the case of event that it is also a proper number), and then feeds the result (either the event id or calendar category) into an article_custom tag that calls the next form. smd_wrap_all removes any empty lines at the beginning and end that may cause calendar import problems.

Changes / Adaptations:

  • Change section="courses" in the above two forms to match the section you are using for your event articles.
  • If you are using adi_gps instead of rah_gps, replace txp:rah_gps name="..." with txp:adi_gps quiet="1" name="..." in the above forms.
ics-item
  • Type: article

This contains the .ics file content generated by the article_custom tags in rah_eo_generate-ics and rah_eo_generate-ics-calendar.

<txp:if_first_article>
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Apple Inc.//Mac OS X 10.8.5//EN
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3
DTSTART:19700329T020000
TZNAME:MEZ
TZOFFSETTO:+0200
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10
DTSTART:19701025T030000
TZNAME:MEZ
TZOFFSETTO:+0100
END:STANDARD
END:VTIMEZONE
</txp:if_first_article>
BEGIN:VEVENT
CREATED:<txp:posted format="%Y%m%dT%H%M%S" />
UID:<txp:posted format="%Y%m%dT%H%M%S" />-<txp:article_id />@<txp:smd_wrap transform="replace|regex|'https?:\/\/'|,trim|/"><txp:site_url /></txp:smd_wrap>
DTEND;TZID=Europe/Berlin:<txp:smd_wrap transform="date|%Y%m%dT%H%M%S"><txp:custom_field name="DTEND" /></txp:smd_wrap>
TRANSP:OPAQUE
SUMMARY:<txp:smd_wrap delim="@" transform="strip_tags@replace|regex|'([\,;])'|\\\1"><txp:if_custom_field name="TITLE"><txp:custom_field name="TITLE" /><txp:else /><txp:title /></txp:if_custom_field></txp:smd_wrap>
DTSTART;TZID=Europe/Berlin:<txp:smd_wrap transform="date|%Y%m%dT%H%M%S"><txp:custom_field name="DTSTART" /></txp:smd_wrap>
DTSTAMP:<txp:posted format="%Y%m%dT%H%M%S" />
LOCATION:<txp:smd_wrap delim="@" transform="strip_tags@replace|regex|'([\,;])'|\\\1">txp:custom_field name="LOCATION" /></txp:smd_wrap>
DESCRIPTION:<txp:smd_wrap delim="@" transform="strip_tags@replace|regex|'([\,;])'|\\\1"><txp:excerpt /></txp:smd_wrap>
URL;VALUE=URI:<txp:if_custom_field name="URL"><txp:custom_field name="URL" /><txp:else /><txp:permlink /></txp:if_custom_field>
END:VEVENT
<txp:if_last_article>END:VCALENDAR</txp:if_last_article>

What the form does: An ics file comprises an opening BEGIN:VCALENDAR along with a block of timezone information about the calendar at the beginning, and a closing END:VCALENDAR at the end. Between them are one or more events, each defined by a BEGIN:VEVENT ... END:VEVENT block that contains the respective information for each event.

The ics-item form converts all dates to the correct format for the ICS file, strips out any formatting from the text fields and cleans it of potentially problematic characters (\, , and ;). That is what the various smd_wrap tags do. If no TITLE is given, the article title is used and if no URL is given, the article permlink is used.

Changes / Adaptations:

The above example uses the timezone for central mainland Europe. If your calendar is in another timezone, you will need to adapt the timezone details accordingly at two places in the above form (here an example for Shanghai):

BEGIN:VTIMEZONE
TZID:Asia/Shanghai
... {timezone infos} ...
END:VTIMEZONE

and in the event details themselves:

BEGIN:VEVENT
DTSTART;TZID=Asia/Shanghai:...
DTEND;TZID=Asia/Shanghai:...
...
END:VEVENT

The TZID must match in the VTIMEZONE block and the DTSTART and DTEND details. You may need to look up the correct VTIMEZONE details for your required timezone, or alternatively create a calendar item in your calendar and export it (on the mac, simply drag to the desktop), then open the resulting .ics file in a text editor to find out the timezone details.

Adding ics download links to your article and page templates

All that remains is add your links to your article forms and page templates.

For an individual event article, add a link as follows inside your article form / listform:

<a href="/?rah_external_output=generate-ics&event=<txp:article_id />">Add date to my calendar (download .ics file)</a>

This produces an .ics file from the current article.

For a calendar of events, add a link similar to the following:

<a href="/?rah_external_output=generate-ics-calendar&cal=beginners">Add calendar of all Beginner Course class dates to my calendar (download .ics file)</a>

The above example creates an ics download for all articles assigned to the category named “beginners” with category title “Beginners Course”.

And for a calendar of all available events, add a link as follows:

<a href="/?rah_external_output=generate-ics-calendar&cal=all">Add calendar of all available class dates to my calendar (download .ics file)</a>

Note: this is why it is a good idea to specify the article expiry date at or shortly after DTEND, so that these last two links do not add lots of past dates to your users’ calendars.

How do your visitors use ics files?

In most cases, visitors to your site need only click on the download ics file link and their calendar will automatically open and ask to add the event date to the user’s calendar. If this does not happen automatically, they need to manually add / drag / import the .ics file to their calendar. Systems that recognise the location name may also show a map or link to the location.

Each event has a unique identifer (UID) which means that even if a date is added twice to a calendar, users should not have duplicate entries in their calendar.

Problems / Troubleshooting

Some things to check if your events are not being generated correctly:

  • Make sure the rah_eo_… forms have the correct section name in the article_custom tag.
  • Make sure your event articles are published in the right section.
  • Make sure you have added DTSTART and DTEND in the correct format: YYYY-MM-DD HH:MM.
  • Make sure your event article’s publish date is not in the future.
  • Make sure your event article’s expired date is not in the past.

You can test whether the forms are producing the correct output by temporarily removing the first two lines beginning with the semicolons in the rah_eo_… forms. The ics file data will then show in the browser. View the page source code (not what you see on screen) to see if something is wrong with the output. You can then paste this source code into an ical validator, e.g. here or here to help discover possible errors.