diff --git a/layouts/_default/baseof.calendar.html b/layouts/_default/baseof.calendar.html
new file mode 100644
index 0000000..bddf04a
--- /dev/null
+++ b/layouts/_default/baseof.calendar.html
@@ -0,0 +1 @@
+{{ block "main" . }}{{ .Content }}{{ end }}
diff --git a/layouts/_default/list.calendar.html b/layouts/_default/list.calendar.html
new file mode 100644
index 0000000..3259690
--- /dev/null
+++ b/layouts/_default/list.calendar.html
@@ -0,0 +1,21 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CCCB//Calendar//DE
+{{ with .Title }}
+X-WR-CALNAME:{{ . }}
+{{ end }}
+CALSCALE:GREGORIAN
+METHOD:PUBLISH
+{{ range .Pages }}
+{{ if .Date }}
+BEGIN:VEVENT
+UID:{{ .File.UniqueID }}@berlin.ccc.de
+DTSTAMP:{{ .Date.Format "20060102T150405Z" }}
+DTSTART:{{ .Date.Format "20060102T150405Z" }}
+SUMMARY:{{ .Title }}
+DESCRIPTION:{{ .Summary | plainify }}
+URL:{{ .Permalink }}
+END:VEVENT
+{{ end }}
+{{ end }}
+END:VCALENDAR
diff --git a/layouts/_default/list.xml.xml b/layouts/_default/list.xml.html
similarity index 100%
rename from layouts/_default/list.xml.xml
rename to layouts/_default/list.xml.html
diff --git a/layouts/_default/single.calendar.html b/layouts/_default/single.calendar.html
new file mode 100644
index 0000000..5a22ec4
--- /dev/null
+++ b/layouts/_default/single.calendar.html
@@ -0,0 +1,21 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CCCB//Calendar//DE
+{{ with .Title }}
+X-WR-CALNAME:{{ . }}
+{{ end }}
+CALSCALE:GREGORIAN
+METHOD:PUBLISH
+{{ if .Date }}
+BEGIN:VEVENT
+UID:{{ .File.UniqueID }}@berlin.ccc.de
+DTSTAMP:{{ .Date.Format "20060102T150405Z" }}
+DTSTART:{{ .Date.Format "20060102T150405Z" }}
+{{ with .Params.event.end }}DTEND:{{ dateFormat "20060102T150405Z" . }}{{ end }}
+SUMMARY:{{ .Title }}
+DESCRIPTION:{{ .Summary | plainify }}
+URL:{{ .Permalink }}
+{{ with .Params.location }}LOCATION:{{ . }}{{ end }}
+END:VEVENT
+{{ end }}
+END:VCALENDAR
diff --git a/tools/gen_upcoming.py b/tools/gen_upcoming.py
index 33b093d..0f69f6a 100755
--- a/tools/gen_upcoming.py
+++ b/tools/gen_upcoming.py
@@ -3,23 +3,13 @@
import sys
import logging
import locale
-from dateutil.parser import parse as _parse
+from dateutil.parser import parse
from datetime import datetime, timedelta
from dateutil.rrule import rruleset, rrulestr
import icalendar
-def parse(value):
- # Some templates emit DTSTART with a trailing "Z" (e.g. 20260507T190000Z)
- # which produces tz-aware datetimes, while others emit naive local times.
- # Normalize to naive so comparisons and sorting don't mix the two.
- dt = _parse(value)
- if dt.tzinfo is not None:
- dt = dt.replace(tzinfo=None)
- return dt
-
-
def vevent_to_event(event, rrstart=None):
if rrstart == None:
begin = parse(event["DTSTART"].to_ical())