The events page (/events) displays Tech for Palestine events fetched from a
public ICS calendar feed (Mattermost's Events Calendar plugin, hosted at
chat.techforpalestine.org). See ARCHITECTURE.md for the SSR/islands model
these components use. Events used to come from Notion — that integration was removed; Notion is
still used for FAQ, ideas, agenda, signatories, and community calls (see NOTION.md).
ICS Feed → eventsClient.ts → API Route → Frontend Component
↓ ↓
/api/events Events.tsx
Both pages group events into categories (by the feed's CATEGORIES tags) and split each
category into upcoming (featured) vs. past events. The grouping logic is shared
(src/utils/eventSections.ts); each page renders it with its own UI.
fetchEvents(locals?): fetches the ICS feed URL fromEVENTS_ICS_URL(viagetEnv()), parses it (line unfolding, property parsing, text unescaping — no external ICS library), and returns a flatEventItem[]sorted by date descending.- Events tagged
testinginCATEGORIESare always dropped. URLin the feed is overloaded (map pin for in-person events, registration link for online events) — only known registration hosts (zoom.us,docs.google.com,streamyard.com) are treated asregisterLink; anything else becomeslocationLink.- Server-side only — never runs in the browser, since the feed URL contains an auth token.
SECTION_DEFS: ordered list of sections — Occupied Tech Podcast, Community Calls, In-Person Events, Roundtable, Book Club — each with the feed tags that route into it.groupIntoSections(events): buckets events into sections by tag (first match wins), splits each intoupcoming/past, and appends a catch-all Others section for events whose tags don't match a named section. A named section with zero events is omitted entirely.
src/pages/events.astro→src/components/Events.tsx— Material-UI styled page, one section per category, upcoming events shown before past within each section.- It fetches initial data server-side (
fetchEvents(Astro.locals)) and re-fetches client-side from/api/eventson mount if no SSR data was passed.
src/components/events/EventsNew.tsx belonged to the abandoned -new redesign and is
unreachable from any live page. See ARCHITECTURE.md.
- Calls
fetchEvents(locals). - Returns fresh data without caching (
no-cache, no-store, must-revalidateheaders — also enforced unconditionally by thecache-controlmiddleware on all/api/*routes, see ARCHITECTURE.md). - Reports errors via
reportError()and returns a generic 500 on failure.
interface EventItem {
id: string; // ICS UID
title: string; // Event title (SUMMARY)
date: string; // "YYYY-MM-DD", local wall-clock date from the feed
status: string; // ICS STATUS (usually "CONFIRMED")
location: string; // Free-text location; empty if LOCATION is a bare URL
locationLink: string; // Map link for in-person events, "" otherwise
image: string; // IMAGE/ATTACH URL, or /images/default.jpg
link: string; // Best available link (register, then recording)
time?: string; // Formatted local time, e.g. "9:00 AM"
description?: string; // DESCRIPTION with the trailing Organizer/Tags/Recording/Banner block stripped
registerLink?: string; // Registration URL (X-REGISTRATION-URL or a known registration host)
recordingLink?: string; // X-RECORDING-URL
tags: string[]; // Lowercased CATEGORIES values, used for section grouping
dateUtcIso: string | null; // Resolved UTC instant, used for upcoming/past comparisons
}Feed images (IMAGE/ATTACH properties) are served as direct chat.techforpalestine.org URLs
— there is no proxy/cache layer. On load error, the frontend falls back to the default image:
<img
src={event.image}
onError={(e) => {
if (target.src !== "/images/default.jpg") {
target.src = "/images/default.jpg";
}
}}
/>EVENTS_ICS_URL=https://chat.techforpalestine.org/plugins/com.techforpalestine.events-calendar/api/v1/public/calendars/by-id/<calendar-id>/ics?token=<token>This is a bearer-style secret embedded in the URL — set it via the Cloudflare Pages dashboard
in production and .dev.vars locally (never hardcode it in source or wrangler.toml). It's
resolved through getEnv() and only ever fetched server-side.
src/
├── pages/
│ ├── events.astro # Events page entry point (MUI)
│ └── api/events.ts # API endpoint
├── components/
│ └── Events.tsx # MUI events component, category sections
├── store/
│ └── eventsClient.ts # ICS feed fetch + parse
└── utils/
├── eventSections.ts # Category grouping (shared by both pages)
└── icalDate.ts # TZID → UTC date resolution (shared with community calls)
- There's no background polling — the user must click refresh (MUI page) or reload the page.
- Check
EVENTS_ICS_URLis set and the feed is reachable. - Check browser console / Sentry for fetch failures.
- Check the event's
CATEGORIEStags in the source calendar (Mattermost Events Calendar plugin) against the tag → section mapping insrc/utils/eventSections.ts. - Events tagged
testingare always dropped, regardless of other tags.
Returns a flat array of event objects sorted by date (newest first); pages group them into
categories client- or server-side via groupIntoSections.
Response:
[
{
"id": "dd579e59-53f1-4a9f-9df7-fd09997b092d@events.t4p",
"title": "Entrepreneurs for Palestine Official Launch",
"date": "2025-02-26",
"status": "CONFIRMED",
"location": "",
"locationLink": "",
"image": "https://chat.techforpalestine.org/.../banner",
"link": "https://youtu.be/w22SyQY1bwI",
"time": "9:00 AM",
"description": "Join us for the official launch of T4P's newest initiative...",
"registerLink": "",
"recordingLink": "https://youtu.be/w22SyQY1bwI",
"tags": ["online"],
"dateUtcIso": "2025-02-26T08:00:00.000Z"
}
]pnpm dev # Start Astro dev server on :4321Set EVENTS_ICS_URL in .dev.vars before testing locally.