A Hugo theme component to generate iCalendar files from your Hugo content.
This project provides a set of simple templates for Hugo to generate iCalendar files. It follows the 80/20 rule with a special focus on event data and strives to be RFC compliant.
Original work: This theme component is based on hugo-ical-templates by Raoul B.
Add this theme component as a Hugo module to your project's hugo.toml config file:
[module]
[[module.imports]]
path = 'github.com/finkregh/hugo-theme-component-ical'Fetch or update the configured modules:
# if you did not already do this before (change the domain/url)
hugo mod init yourdomain.com
hugo mod get -u ./...You need to configure the Calendar and CalendarWithAlarms output formats in your config:
[outputs]
page = ["HTML", "Calendar", "CalendarWithAlarms"]
section = ["HTML", "Calendar", "CalendarWithAlarms"]
[outputFormats.Calendar]
baseName = "calendar"
mediaType = "text/calendar"
isPlainText = true
permalinkable = true
suffix = "ics"
# Avoid webcal scheme
protocol = "https://"
[outputFormats.CalendarWithAlarms]
baseName = "calendar-alarms"
mediaType = "text/calendar"
isPlainText = true
permalinkable = true
suffix = "ics"
# Avoid webcal scheme
protocol = "https://"The CalendarWithAlarms output format generates iCalendar files that include alarm/reminder components (VALARM) in addition to the event data. This allows calendar applications to display notifications and reminders for events at specified times before the event starts.
Link the generated ics files for download on your html pages:
{{ with .OutputFormats.Get "Calendar" }}
<a href="{{ .RelPermalink }}" type="text/calendar">{{ $.Title }}</a>
{{ end }}For calendars with alarms:
{{ with .OutputFormats.Get "CalendarWithAlarms" }}
<a href="{{ .RelPermalink }}" type="text/calendar">{{ $.Title }} (with alarms)</a>
{{ end }}Some javascript libraries are used to display the calendar entries visually. They are downloaded from npmjs.org when the site is built and then served from here.
If you do not want to display the calendar entries on your website you can skip this section.
# Initial setup (after hugo mod get from above)
hugo mod npm pack
npm update
# To get the latest version later
npm updateThe generated (minified) javascript file can be served as a separate file or directly inside the respective webpage:
<!-- separate .js file -->
<!-- Begin calendar javascript -->
{{ partial "vendor/finkregh/ical/js.html" . }}
<!-- End calendar javascript -->
<!-- inside the html -->
<!-- Begin calendar javascript -->
{{ partial "vendor/finkregh/ical/js-inline.html" . }}
<!-- End calendar javascript -->This generates:
- if the page has a calendar entry (
if and (.OutputFormats.Get "Calendar") (eq .Type "events")):<meta http-equiv="Content-Security-Policy" content="font-src data:" />- JS referenced by a file or inline
Inline does not required an additional request, not-inline does make the HTML bigger (on every page where a calendar exists). Decide for yourself what to use.
Either way the javascript will only be included in places where a calendar entry exists and it will not require loading anything from a third party (besides when building the static files).
The component provides a partial you can include in your layouts/events/.
single.html:
{{ partial "vendor/finkregh/ical/events/single.html" . }}
list.html:
{{ partial "vendor/finkregh/ical/events/list.html" . }}
If you do not have npm installed a pre-built file is also available which you can insert into your templates:
Warning
This has been created manually and will not be updated with each release, use with caution!
{{ $prebuilt := resources.Get "js/vendor/finkregh/ical/minified.min.2c0b8eb566757daf33d80723a369c40de708920b6faeb3f6016302e4d986635d" | resources.Fingerprint "sha256"}}
<script src="{{ $prebuilt.Permalink }}" type="module" {{ if $isProd }}integrity="{{ $prebuilt.Data.Integrity }}"{{ end }} defer></script>
Alternatlively load the javascript from a third party (the versions here might be outdated too!):
<script src="https://cdn.jsdelivr.net/npm/[email protected]/index.global.min.js"></script>
<script src=" https://cdn.jsdelivr.net/npm/@fullcalendar/[email protected]/index.global.min.js "></script>
<script src="https://cdn.jsdelivr.net/npm/@fullcalendar/[email protected]/locales-all.global.min.js"></script>
<script src="https://unpkg.com/ical.js/dist/ical.es5.min.cjs"></script>
{{ $themejs := resources.Get "js/vendor/finkregh/ical/script.js" | js.Build $options | resources.Minify | resources.Fingerprint "sha256"}}
<script src="{{ $themejs.Permalink }}" type="module" {{ if $isProd }}integrity="{{ $themejs.Data.Integrity }}"{{ end }} defer></script>The events are specified in the fontmatter, all parts are optional from the templating perspective, you as the user need to be aware what is required.
The time format is {YEAR}-{MONTH}-{DAY}T{HOUR}:{MINUTE}:{SECOND}+{TIMEZONE_HOUR}:{TIMEZONE_MINUTE}.
---
title: Important Event!11
# First occurrence, this also defines the timeframe for the calendar entry
startDate: 2024-01-08T09:00:00+01:00
endDate: 2024-01-08T09:30:00+01:00
# Location
where: "Meeting Room 1, Main Office"
# Who created the event
orga: "Scrum Master"
# Contact
orgaEmail: "[email protected]"
---You might want to look into the specifications (RFC 5545: Internet Calendaring and Scheduling Core Object Specification (iCalendar), RFC 7986: New Properties for iCalendar) as the examples here only show part of what is possible.
The RRULE implementation supports YEARLY and MONTHLY frequencies with BYMONTH, BYDAY, and BYSETPOS components.
# Every Monday
recurrenceRule:
freq: "WEEKLY"
byDay: "MO"recurrenceRule:
freq: "YEARLY"
byMonth: 4
byDay: "SU"
bySetPos: 3Generates: RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=SU;BYSETPOS=3
recurrenceRule:
freq: "YEARLY"
byMonth: 10
byDay: "MO"
bySetPos: [1, 2]Generates: RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=MO;BYSETPOS=1,2
recurrenceRule:
freq: "MONTHLY"
interval: 3
byDay: "SU"
bySetPos: -1Generates: RRULE:FREQ=MONTHLY;INTERVAL=3;BYDAY=SU;BYSETPOS=-1
The VALARM implementation supports DISPLAY, EMAIL, and AUDIO alarms with flexible trigger configurations.
Note
I did not test these, so try not to wake up (others) in the middle of the night ;-)
alarms:
- action: "DISPLAY"
trigger:
duration: "-PT15M" # 15 minutes before event start
description:
text: "Meeting starts in 15 minutes"
lang: "en"alarms:
- action: "EMAIL"
trigger:
duration: "-PT1H" # 1 hour before event start
description:
text: "Don't forget about the meeting in 1 hour"
lang: "en"
summary:
text: "Meeting Reminder"
lang: "en"
attendee:
- email: "[email protected]"
commonName: "Ahmed Doe"
- email: "[email protected]"
commonName: "Jane Smith"Duration values use ISO 8601 duration format:
PT15M= 15 minutesPT1H= 1 hourP1D= 1 dayP1W= 1 week-PT15M= 15 minutes before (negative for "before")PT15M= 15 minutes after (positive for "after")
This project does not provide a complete implementation of all features the iCalendar specification contains. It rather follows the 80/20 rule and with a special focus on event data.
All the templates strive to be RFC compliant and produce output files that adhere to the specification. No special hacks are included to work around broken calendar software or the like.
This project does not provide a full turn-key solution and some assembly will be required in most cases. Understanding of Hugo and especially Hugo's templating system is still recommended.
The system is highly flexible and should adapt or extend easily to more exotic use cases. On some spots the chosen defaults might be a bit opinionated.
The partial template snippets from this project should help to easily avoid the most common mistakes when creating ics files. However, there is absolutely no validation, neither on the syntactic nor the semantic level. You can always use an external validation service to check the output.
Due to the way the templates work, we do not fold long lines. However, this is actually fine as the RFC writes SHOULD instead of MUST:
Lines of text SHOULD NOT be longer than 75 octets, excluding the line break. Long content lines SHOULD be split into a multiple line representations using a line "folding" technique.
See: https://tools.ietf.org/html/rfc5545#section-3.1
This is the one place where we knowingly break RFC compliance. While this is not correct per se, it hopefully is a minor issue with today's calendar software.
The iCalendar object is organized into individual lines of text, called content lines. Content lines are delimited by a line break, which is a CRLF sequence (CR character followed by LF character).
See: https://tools.ietf.org/html/rfc5545#section-3.1
PRs, issues, comments, etc. are welcome!
You can use the setup in .github/exampleSite/ to test things locally.
Directory structure:
.github/exampleSite/- example implementation, also used to generate the demo site
.github/scripts/- python script used in the CI to validate the generated
.icsfiles
- python script used in the CI to validate the generated
.github/workflows/- tests, deployment of the demo site
assets/js/vendor/finkregh/ical/- JavaScript files
assets/saas/vendor/finkregh/ical/- unused
layouts/*.ics,layouts/_partials/events/,layouts/_partials/ical/- various parts to generate the
.icsfiles
- various parts to generate the
layouts/_partials/vendor/finkregh/ical/js{,-inline}.html<script ...>to get all required JS
layouts/_partials/vendor/finkregh/ical/events/- example template for single/list views
- RFC 5545: Internet Calendaring and Scheduling Core Object Specification (iCalendar)
- RFC 7986: New Properties for iCalendar
From rfc 5545
- 3.2.1. Alternate Text Representation
- 3.2.2. Common Name
- 3.2.3. Calendar User Type
- 3.2.4. Delegators
- 3.2.5. Delegatees
- 3.2.6. Directory Entry Reference
- 3.2.7. Inline Encoding
- 3.2.8. Format Type
- 3.2.9. Free/Busy Time Type
- 3.2.10. Language
- 3.2.11. Group or List Membership
- 3.2.12. Participation Status
- 3.2.13. Recurrence Identifier Range
- 3.2.14. Alarm Trigger Relationship
- 3.2.15. Relationship Type
- 3.2.16. Participation Role
- 3.2.17. RSVP Expectation
- 3.2.18. Sent By
- 3.2.19. Time Zone Identifier
- 3.2.20. Value Data Types
- 3.3.1. Binary
- 3.3.2. Boolean
- 3.3.3. Calendar User Address
- 3.3.4. Date
- 3.3.5. Date-Time
- 3.3.6. Duration
- 3.3.7. Float
- 3.3.8. Integer
- 3.3.9. Period of Time
- 3.3.10. Recurrence Rule
- 3.3.11. Text
- 3.3.12. Time
- 3.3.13. URI
- 3.3.14. UTC Offset
- 3.6.1. Event Component
- 3.6.2. To-Do Component
- 3.6.3. Journal Component
- 3.6.4. Free/Busy Component
- 3.6.5. Time Zone Component
- 3.6.6. Alarm Component
- 3.8.1.1. Attachment
- 3.8.1.2. Categories
- 3.8.1.3. Classification
- 3.8.1.4. Comment
- 3.8.1.5. Description
- 3.8.1.6. Geographic Position
- 3.8.1.7. Location
- 3.8.1.8. Percent Complete
- 3.8.1.9. Priority
- 3.8.1.10. Resources
- 3.8.1.11. Status
- 3.8.1.12. Summary
- 3.8.2.1. Date-Time Completed
- 3.8.2.2. Date-Time End
- 3.8.2.3. Date-Time Due
- 3.8.2.4. Date-Time Start
- 3.8.2.5. Duration
- 3.8.2.6. Free/Busy Time
- 3.8.2.7. Time Transparency
- 3.8.4.1. Attendee
- 3.8.4.2. Contact
- 3.8.4.3. Organizer
- 3.8.4.4. Recurrence ID
- 3.8.4.5. Related To
- 3.8.4.6. Uniform Resource Locator
- 3.8.4.7. Unique Identifier
- 3.8.5.1. Exception Date-Times
- 3.8.5.2. Recurrence Date-Times
- 3.8.5.3. Recurrence Rule
- 3.8.7.1. Date-Time Created
- 3.8.7.2. Date-Time Stamp
- 3.8.7.3. Last Modified
- 3.8.7.4. Sequence Number
From rfc 7986
- 5.1. NAME Property
- 5.2. DESCRIPTION Property
- 5.3. UID Property
- 5.4. LAST-MODIFIED Property
- 5.5. URL Property
- 5.6. CATEGORIES Property
- 5.7. REFRESH-INTERVAL Property
- 5.8. SOURCE Property
- 5.9. COLOR Property
- 5.10. IMAGE Property
- 5.11. CONFERENCE Property
- 6.1. DISPLAY Property Parameter
- 6.2. EMAIL Property Parameter
- 6.3. FEATURE Property Parameter
- 6.4. LABEL Property Parameter
This hugo theme component was scaffolded with the cookiecutter-hugo-theme-component template.