A Hugo theme component to generate iCalendar (.ics) files from Hugo content with recurrence rules and alarm support.
Original work: This theme component is based on hugo-ical-templates by Raoul B.
- Timezone Support: UTC conversion for universal compatibility (specify timestamps with explicit timezone or without and use timezone from settings)
- Recurrence Rules: RRULE patterns with BYSETPOS, BYDAY support
- Alarm System: DISPLAY, EMAIL, and AUDIO alarms
- Status Management: Event status handling(CONFIRMED, TENTATIVE, CANCELLED)
- JavaScript Calendar Display: Optional FullCalendar.js integration
- Responsive Design: Mobile-friendly calendar views
- Download Links: Direct iCal file download functionality
- Multiple Output Formats: Both standard and alarm-enabled calendar files
The project uses a modern template structure compatible with Hugo v0.146.0+:
layouts/
├── list.calendar.ics # List template for calendar format
├── list.calendarwithalarms.ics # List template with VALARM support
├── single.calendar.ics # Single page calendar template
├── single.calendarwithalarms.ics # Single page with alarms template
├── _default/
│ └── baseof.html # HTML base template
├── _partials/
│ ├── calendar_js.html # Calendar JavaScript
│ ├── calendar_single.html # Single event display
│ ├── calendar_section.html # Calendar section display
│ ├── header.ics # Generic iCal header partial
│ ├── event.ics # Generic event partial
│ ├── event-with-alarms.ics # Event with alarm support
│ ├── timezone.ics # Timezone definition partial
│ ├── recurrence_human_readable.html # Human-readable recurrence
│ ├── events/
│ │ └── event-card.html # Event card component
│ ├── ical/ # iCal component library (50+ partials)
│ │ ├── cal_props.ics # Calendar properties
│ │ ├── comp_event.ics # VEVENT component
│ │ ├── comp_time_zone.ics # VTIMEZONE component
│ │ ├── comp_valarm.ics # VALARM component
│ │ ├── dt_*.ics # Data type formatters
│ │ ├── param_*.ics # Parameter formatters
│ │ └── prop_*.ics # Property formatters
│ └── recurrence/ # Recurrence pattern handlers
│ ├── daily_frequency.html
│ ├── weekly_frequency.html
│ ├── monthly_frequency.html
│ └── yearly_frequency.html
└── events/
├── list.html # Events list HTML template
└── single.html # Events single HTML template
- Recommended: v0.150.0+ (as specified in
hugo.toml) - Tested With: v0.160.1+extended
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:
# Initialize Hugo modules (if not done before)
hugo mod init yourdomain.com
# Get the module
hugo mod get -u ./...Either specify every single calendar-related timestamp with explicit timezone
(e.g. startDate: 2024-04-21T09:00:00+02:00) or apply these settings:
Timezone handling involves two separate concerns that require two configuration settings:
- Hugo's
timeZone(top-level config) -- controls how front matter dates without timezone offsets are parsed. This is a built-in Hugo setting (docs). params.ical.timezone-- tells the ICS templates which IANA timezone to use when generating.icscalendar files (fortime.AsTimereparsing and VTIMEZONE output).
Both must be set. Hugo's timeZone is not accessible in templates, so the ICS templates rely on the param.
# hugo.toml (or config/_default/hugo.toml)
# Required: Hugo uses this to parse front matter dates without timezone offsets
timeZone = "Europe/Berlin"
[params.ical]
# Required: ICS templates use this for calendar file generation
timezone = "Europe/Berlin"Hugo also supports per-language timeZone for multilingual sites:
[languages.en]
timeZone = "America/New_York"
[languages.de]
timeZone = "Europe/Berlin"Configure the Calendar and CalendarWithAlarms output formats in your hugo.toml:
[outputs]
page = ["HTML", "Calendar", "CalendarWithAlarms"]
section = ["HTML", "Calendar", "CalendarWithAlarms"]
[outputFormats.Calendar]
baseName = "calendar"
mediaType = "text/calendar"
isPlainText = true
permalinkable = true
suffix = "ics"
protocol = "https://"
[outputFormats.CalendarWithAlarms]
baseName = "calendar-alarms"
mediaType = "text/calendar"
isPlainText = true
permalinkable = true
suffix = "ics"
protocol = "https://"The CalendarWithAlarms output format generates iCalendar files that include alarm/reminder components (VALARM) in addition to the event data.
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 }}Enable visual calendar display with JavaScript libraries downloaded from npmjs.org:
# Initial setup (after hugo mod get)
hugo mod npm pack
npm installInclude the JavaScript in your templates:
<!-- Separate .js file -->
{{ partial "calendar_js.html" . }}
<!-- Conditional JavaScript loader -->
{{ partial "calendar_js_conditional.html" . }}Use the provided partials in your layouts/events/ templates:
{{ partial "calendar_single.html" . }}{{ partial "calendar_section.html" . }}Events are specified in the front matter:
---
title: Important Meeting
startDate: 2024-01-08T09:00:00+01:00
endDate: 2024-01-08T09:30:00+01:00
where: "Meeting Room 1, Main Office"
orga: "Scrum Master"
orgaEmail: "scrummaster@example.org"
---Dates in front matter can be written with or without explicit timezone offsets:
# Without offset -- Hugo interprets using the configured timeZone
startDate: 2026-03-15T14:00:00
# With explicit offset -- the offset takes precedence over any config
startDate: 2026-03-15T14:00:00+01:00Both formats work correctly. When no offset is present, Hugo's timeZone setting determines how the time is interpreted. When an offset is present, it is used as-is.
Individual events can override the timezone for ICS generation using the icaltimezone front matter parameter:
---
title: "Auckland Meetup"
startDate: 2026-03-15T14:00:00+13:00
icaltimezone: "Pacific/Auckland"
---The ICS template timezone resolution order is:
- Page parameter:
icaltimezone - Site parameter:
params.ical.timezone - Build fails if neither is set
Generated .ics files convert all times to UTC for maximum compatibility:
Input (front matter):
startDate: 2026-03-15T14:00:00 # Interpreted as 14:00 CET (Europe/Berlin)Output (.ics file):
DTSTART;VALUE=DATE-TIME:20260315T130000ZThis UTC-only approach means:
- All calendar clients support UTC and convert to the user's local timezone for display
- No VTIMEZONE components needed, resulting in smaller
.icsfiles - No timezone database maintenance or DST rule updates needed
- Follows RFC 5545 Section 3.3.5
HTML event pages display times in the configured timezone:
<time datetime="2026-03-15T14:00:00+01:00">
March 15, 2026, 2:00:00 pm CET
</time>This relies entirely on Hugo's built-in timeZone config for correct parsing and formatting.
recurrenceRule:
freq: "WEEKLY"
byDay: "MO"recurrenceRule:
freq: "YEARLY"
byMonth: 4
byDay: "SU"
bySetPos: 3recurrenceRule:
freq: "YEARLY"
byMonth: 10
byDay: "MO"
bySetPos: [1, 2]recurrenceRule:
freq: "MONTHLY"
interval: 3
byDay: "SU"
bySetPos: -1alarms:
- 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: "ahmed.doe@example.com"
commonName: "Ahmed Doe"
- email: "jane.smith@example.com"
commonName: "Jane Smith"PT15M= 15 minutesPT1H= 1 hourP1D= 1 dayP1W= 1 week-PT15M= 15 minutes before (negative for "before")PT15M= 15 minutes after (positive for "after")
- English (EN):
i18n/en.toml - German (DE):
i18n/de.toml
- Event Metadata: Event details, date/time, location, organizer
- Recurrence Patterns: Human-readable recurrence descriptions
- Calendar Interface: Download links, calendar views, navigation
- Status Messages: Event status, cancellation notices
- Template Elements: Form labels, buttons, technical details
{{ i18n "ical_event_details" }}
{{ i18n "ical_download_ics" (dict "title" .Title) }}
{{ i18n "ical_recurrence_every_interval" (dict "count" 2 "unit" "weeks") }}- Create new translation file:
i18n/[lang].toml - Copy structure from
i18n/en.toml - Translate all keys maintaining parameter placeholders
- Test with content in the new language
Successful builds should show:
- Exit Code: 0 (no errors)
- Template Resolution: All templates resolving correctly
- iCal Validation: RFC 5545 compliant output
- No ERROR Messages: Only informational WARN messages for debugging
Use the setup in .github/exampleSite/ to test changes locally:
# Run development server with example site
hugo server --source .github/exampleSite
# Build and validate
hugo --source .github/exampleSiteAlways pass complete context to partials:
{{- partial "component.ics" (dict "Page" . "Site" $.Site "Params" .Params) -}}Include error checking:
{{- if not .Page -}}
{{- errorf "Page context required for %s" .Name -}}
{{- end -}}Implement graceful fallbacks for missing components:
{{- $sectionSpecific := printf "_partials/component.%s.ics" .Section -}}
{{- if templates.Exists $sectionSpecific -}}
{{- partial (printf "component.%s.ics" .Section) . -}}
{{- else -}}
{{- warnf "Section-specific component not found: %s, using generic" $sectionSpecific -}}
{{- partial "component.ics" . -}}
{{- end -}}# Test build with example site
PR_NUMBER=0 just testWe use both Python and JavaScript validation to make sure ics parsing quirks of the used libraries do not lead to false-positives.
Individual Test Targets:
# Python validation only
PR_NUMBER=0 just test_python
# JavaScript validation only
PR_NUMBER=0 just test_js
# Debug mode with validation
PR_NUMBER=0 just test_debugThe templates and partials include optional debugging output:
{{- if or (eq hugo.Environment "development") (eq hugo.Environment "debug") }}{{ warnidf "debug-template-used" "Template used: %s" templates.Current.Name }}{{ end -}}
{{- if or (eq hugo.Environment "development") (eq hugo.Environment "debug") }}{{ warnidf "debug-partial-used" "Partial used: %s" templates.Current.Name }}{{ end -}}PRs, issues, comments, and suggestions are welcome!
This project has been generated with help of LLMs as well as a lot of swearing (hugo templating and documentation, go module handling and versioning), long stretches of ignorance, etc... I tried to make sure that this template monster behaves properly as I want to use it for a website myself. Having people show up due to broken calendar entries would not be so nice.
As we all know there is no ethical consumption under capitalism. If the usage of an LLM is a no-go you have hereby been informed.
Due to template limitations, long lines are not folded. This is acceptable as RFC 5545 specifies SHOULD rather than MUST:
Lines of text SHOULD NOT be longer than 75 octets, excluding the line break.
This implementation follows these RFCs as far as possible:
- RFC 5545: Internet Calendaring and Scheduling Core Object Specification (iCalendar)
- RFC 7986: New Properties for iCalendar
- Event Component (VEVENT)
- Alarm Component (VALARM)
- To-Do Component (VTODO)
- Journal Component (VJOURNAL)
- Free/Busy Component (VFREEBUSY)
This Hugo theme component was scaffolded with the cookiecutter-hugo-theme-component template.

