Kalender & Events ohne CMS – ICS/CalDAV einbinden + eigene Einträge (2025)
Externe Kalender (Google/Outlook) per ICS integrieren und mit eigenen Einträgen kombinieren – inklusive Ort, Kategorien und einfacher Pflege. Perfekt für Projekte wie AffenHub, Vereins- oder Event-Seiten.

~10 Min. Lesezeit · Veröffentlicht am
🗓️ Zielbild: Eine URL je Kalender, täglicher Pull → Normalisierung → Merge mit events.json → Ausgabe als events.min.json.
Überblick & Architektur
// cron (täglich) → node scripts/fetch-cal.mjs
ICS-Feeds ----\
\-- parse/normalize --> merged events.json --> build --> dist/assets/events.json
Eigene JSON ----/
ICS einbinden (Google/Outlook)
In Google/Outlook den „privaten ICS-Link“ kopieren. Im Projekt als Quelle hinterlegen (ENV/Config).
iCal-Parsing in Node/PHP
// Node (ics-parser beispielhaft)
import { parseICS } from './lib/ics-parse.js'
const icsText = await fetch(process.env.GOOGLE_ICS).then(r => r.text())
const items = parseICS(icsText) // → { uid, start, end, summary, location, categories, url }
// PHP (vereinfacht)
$ics = file_get_contents($_ENV['GOOGLE_ICS']);
$events = IcsParser::parse($ics); // Normalisieren auf gemeinsames Schema
Merge mit eigenen Events (JSON)
// events.local.json (vom System editierbar)
[
{"uid":"local-1","start":"2025-11-03T18:00:00+01:00","end":"2025-11-03T20:00:00+01:00",
"title":"Afterwork Ride","location":"Essen","categories":["Sport","Bike"],"url":""}
]
// Merge (Node)
const merged = [...icsItems, ...localItems]
.filter(e => new Date(e.end) >= new Date()) // nur zukünftige
.sort((a,b) => new Date(a.start) - new Date(b.start));
await fs.writeFile('dist/assets/events.json', JSON.stringify(merged));
Kategorien & Orte
Ein gemeinsames Schema hilft bei Filtern: {title, start, end, location:{name,lat,lng}, categories:[...], url, source}.
Frontend-Darstellung (Filter/Listen)
<div x-data="{ q:'', cat:'', items:[] }" x-init="items = await (await fetch('/assets/events.json')).json()">
<input x-model="q" placeholder="Suche nach Titel/Ort" class="input">
<select x-model="cat"><option value="">Alle</option><option>Workshop</option><option>Sport</option></select>
<ul>
<template x-for="e in items.filter(i=> (!cat||i.categories?.includes(cat)) && (i.title+i.location).toLowerCase().includes(q.toLowerCase()))">
<li class="py-2 border-b border-white/10">
<strong x-text="new Date(e.start).toLocaleString()"></strong> ·
<span x-text="e.title"></span>
<span class="text-white/60" x-text="e.location"></span>
</li>
</template>
</ul>
</div>