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.