Zum Inhalt springen

Kalender & Events ohne CMS – ICS/CalDAV einbinden + eigene Einträge (2025)

· von

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.

Kalenderintegration ohne CMS – ICS/CalDAV

~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>