Main points about ICalendar

  • iCalendar is a media type for storing and exchanging calendar events and other calendar-based data;

  • References can be found here and on the rfc5545 specification;

  • It’s an open text-based protocol;

  • Its MIMME type or media type is text/calendar;

  • filename extension can be .ical, .ics, .ifb, .icalendar;

An ICalendar file might look like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//kiv.software/docurenew v1.0//EN
BEGIN:VEVENT
UID:docurenew.kiv.software-main-5
CREATED:20220509T133807Z
DTSTAMP:20220509T133809Z
SEQUENCE:1652103489017
DTSTART:20240509T080000Z
DURATION:PT1H
SUMMARY:fake passport expires today! Hope you renewed it when I reminded you 1 year ago...
END:VEVENT
BEGIN:VEVENT
UID:docurenew.kiv.software-reminder-5
CREATED:20220509T133807Z
DTSTAMP:20220509T133809Z
SEQUENCE:1652103489017
DTSTART:20230509T080000Z
DURATION:PT1H
SUMMARY:fake passport expires in 1 year time! Get to it!
END:VEVENT
END:VCALENDAR
  • This file will create two calendar events one year apart. We can define several calendar events(BEGIN:VEVENT/END:VEVENT) in one ICalendar definition;

  • The date-times are in an format based on the ISO 8601, eg: DTSTART:19970714T173000Z ;

  • Each event is identified by an unique global id stored in UID;

  • A calendar event can be updated by incrementing the SEQUENCE attribute. A calendar event with the same UID and an higher SEQUENCE will replace/update the previous event on the supported calendar application.

Server-side generation with Remix

We can, for example, generate the calendar events dynamically by using a resource route(eg: calendar[.]ics.ts) like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import { Response  } from "@remix-run/node";
import type { LoaderFunction } from '@remix-run/node';
import * as data_model from "~/data_model";    
import { getCurrentSession } from "~/session.server";
import * as utils from "~/utils";

export const loader: LoaderFunction = async ({ params, request }) =>{

    const { id } = params;
    if(!id) throw new Response("id is required", {status: 400});

    const session = await getCurrentSession(request);

    const user = data_model.getUser(session.get('userId'))
    if(!user) throw new Response("user not found", {status: 404});

    const document = await data_model.getDocumentById(id, user.id);
    if(!document) throw new Response("document not found", {status: 404});

    const filename = `${document.name.replace(/\s+/g, '-')}-calendar-event.ics`;

    console.log({ document })

    let update_sequence = Date.now();

    let ics = `
BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//kiv.software/docurenew v1.0//EN
BEGIN:VEVENT
UID:docurenew.kiv.software-main-${document.id}
CREATED:${utils.dateToUtcTimestamp(document.created_at)}
DTSTAMP:${utils.dateToUtcTimestamp('now')}
SEQUENCE:${update_sequence}
DTSTART:${document.expiration_date_tz}
DURATION:PT1H
SUMMARY:${document.name} expires today! Hope you renewed it when I reminded you ${document.remind_before} ago...
END:VEVENT
BEGIN:VEVENT
UID:docurenew.kiv.software-reminder-${document.id}
CREATED:${utils.dateToUtcTimestamp(document.created_at)}
DTSTAMP:${utils.dateToUtcTimestamp('now')}
SEQUENCE:${update_sequence}
DTSTART:${document.remind_date_tz}
DURATION:PT1H
SUMMARY:${document.name} expires in ${document.remind_before} time! Get to it!
END:VEVENT
END:VCALENDAR
    `.trim();

    return new Response(ics, {
        headers: {
            'Content-Type': 'text/calendar',
            'Content-Disposition': `attachment; filename="${filename}"` 
        }
    })
}

Here we add the dynamic data to the calendar event, add the proper Content-Type header and Content-Disposition header to be able to set the name of the generated file. Upon navigating to the URL(eg /documents/{id}/calendar.ics) the file will be downloaded and then we can open it with the default calendar app to confirm and save the events.