From b7a64ed581cddf8d2f4e987c041bf4a6909cda78 Mon Sep 17 00:00:00 2001
From: maximsmol <1472826+maximsmol@users.noreply.github.com>
Date: Mon, 16 Feb 2026 15:45:02 -0800
Subject: [PATCH 1/2] ical pulling draft
Signed-off-by: maximsmol <1472826+maximsmol@users.noreply.github.com>
---
_events/_events.11tydata.js | 7 +++++-
_events/_ical.md | 11 ++++++++++
_includes/event-card.html | 5 ++++-
_includes/events.html | 2 +-
eleventy.config.js | 43 +++++++++++++++++++++++++++++++++++--
eslint.config.ts | 2 ++
package.json | 1 +
pnpm-lock.yaml | 31 ++++++++++++++++++++++++++
8 files changed, 97 insertions(+), 5 deletions(-)
create mode 100644 _events/_ical.md
diff --git a/_events/_events.11tydata.js b/_events/_events.11tydata.js
index ca5b3f6f..48c295b9 100644
--- a/_events/_events.11tydata.js
+++ b/_events/_events.11tydata.js
@@ -7,6 +7,8 @@ export default {
layout: "event.11ty.tsx",
eleventyComputed: {
date(data) {
+ if (data.event != null) return;
+
const res = DateTime.fromFormat(data.date, "yyyy-MM-dd HH:mm", {
zone: "America/New_York",
}).toJSDate();
@@ -14,7 +16,10 @@ export default {
return res;
},
excerpt(data) {
- const res = data.page.rawInput.trim().split("\n\n")[0];
+ let content = data.page.rawInput;
+ if (data.event != null) content = data.event.content ?? "";
+
+ const res = content.trim().split("\n\n")[0];
data.page.excerpt = res;
return res;
},
diff --git a/_events/_ical.md b/_events/_ical.md
new file mode 100644
index 00000000..d73df16c
--- /dev/null
+++ b/_events/_ical.md
@@ -0,0 +1,11 @@
+---
+pagination:
+ data: ical_events
+ size: 1
+ alias: event
+permalink: "{{ event.url }}"
+eleventyComputed:
+ title: "{{ event.data.title }}"
+---
+
+{{ event.content }}
diff --git a/_includes/event-card.html b/_includes/event-card.html
index 3d3abfb8..ad4b4400 100644
--- a/_includes/event-card.html
+++ b/_includes/event-card.html
@@ -47,7 +47,10 @@
📆
+
+ {%- if event.data.locations -%}
+
📍 {% include locations.html%}
+ {%- endif -%}
diff --git a/_includes/events.html b/_includes/events.html
index e78be12e..94d2fb55 100644
--- a/_includes/events.html
+++ b/_includes/events.html
@@ -1,6 +1,6 @@
{% assign berlin_events = site.data.berlin_events %}
{% assign nl_events = site.data.nl_events %}
-{% assign events = site.events | concat: nl_events | concat: berlin_events | date_sort | filter_tags: include.tags %}
+{% assign events = site.events | concat: ical_events | concat: nl_events | concat: berlin_events | date_sort | filter_tags: include.tags %}
{% assign future_events = events | where_future: site.time %}
{% assign past_events = events | where_past: site.time %}
diff --git a/eleventy.config.js b/eleventy.config.js
index 0137f39c..9981ea6a 100644
--- a/eleventy.config.js
+++ b/eleventy.config.js
@@ -7,6 +7,7 @@ import * as jsxRuntime from "hastscript/jsx-runtime";
import { DateTime } from "luxon";
import markdownIt from "markdown-it";
import markdownItAnchor from "markdown-it-anchor";
+import ical from "node-ical";
import rehypeSlug from "rehype-slug";
import YAML from "yaml";
@@ -67,6 +68,7 @@ export default async (cfg) => {
cfg.addDataExtension("yml", YAML.parse);
cfg.addDataExtension("yaml", YAML.parse);
+ // fixme(maximsmol): calendar events do not get loaded into RSS
const getRssConfig = (collection, name) => ({
type: "atom",
outputPath: `/feed/${collection}.xml`,
@@ -240,6 +242,41 @@ export default async (cfg) => {
});
cfg.addTemplateFormats("mdx");
+ const fetchCalendar = async () => {
+ const data = await ical.fromURL(
+ "https://dev.techworkerscoalition.org/nextcloud/remote.php/dav/public-calendars/EHFzzZQXSQoS3f5w/?export",
+ );
+
+ if (data == null) throw new Error("failed to load calendar data");
+
+ const events = [];
+ for (const [, entry] of Object.entries(data)) {
+ if (entry == null) continue;
+ if (entry.type !== "VEVENT") continue;
+
+ const start = DateTime.fromJSDate(entry.start, {
+ zone: entry.start.tz,
+ });
+ // todo(maximsmol): deal with recurring events
+ // todo(maximsmol): extract image from content
+ events.push({
+ // todo(maximsmol): use a timezone-aware datetime?
+ date: start.toJSDate(),
+ url: `/events/${entry.uid}/`,
+ data: {
+ title: entry.summary,
+ time_zones: entry.start.tz != null ? [entry.start.tz] : undefined,
+ // todo(maximsmol): add locations
+ // locations: [],
+ },
+ content:
+ entry.description != null ? md.render(entry.description) : undefined,
+ });
+ }
+
+ cfg.addGlobalData("ical_events", events);
+ };
+
const remoteDataSrcs = [
{
data: "berlin_press",
@@ -254,7 +291,7 @@ export default async (cfg) => {
url: "https://techwerkers.nl/en/twc-global/index.yaml",
},
];
- const deferred = [];
+ const deferred = [fetchCalendar()];
for (const x of remoteDataSrcs)
deferred.push(
(async () => {
@@ -265,7 +302,9 @@ export default async (cfg) => {
});
const data = YAML.parse(await req.text());
- console.log(`Fetched: ${x.data} from ${x.url}`);
+ console.log(
+ `Fetched ${data.length} ${x.data} items from from ${x.url}`,
+ );
cfg.addGlobalData(x.data, data);
})(),
diff --git a/eslint.config.ts b/eslint.config.ts
index fc93f120..e9e04801 100644
--- a/eslint.config.ts
+++ b/eslint.config.ts
@@ -63,6 +63,8 @@ export default defineConfig([
tseslint.configs.stylisticTypeChecked,
],
rules: {
+ "@typescript-eslint/no-confusing-void-expression": "off",
+
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
},
languageOptions: {
diff --git a/package.json b/package.json
index 08ca5800..962de612 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"luxon": "^3.7.2",
"markdown-it": "^14.1.0",
"markdown-it-anchor": "^9.2.0",
+ "node-ical": "^0.25.2",
"rehype-slug": "^6.0.0",
"tsx": "^4.21.0",
"typescript-eslint": "^8.54.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 93f82e8a..db0a8824 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -94,6 +94,9 @@ importers:
markdown-it-anchor:
specifier: ^9.2.0
version: 9.2.0(@types/markdown-it@14.1.2)(markdown-it@14.1.0)
+ node-ical:
+ specifier: ^0.25.2
+ version: 0.25.2
rehype-slug:
specifier: ^6.0.0
version: 6.0.0(patch_hash=1bf83dfa419d0209fd1fba0392ca594fe12bf7a8aa0cc20d96d4d10ed6f8318f)
@@ -370,6 +373,10 @@ packages:
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
+ '@js-temporal/polyfill@0.5.1':
+ resolution: {integrity: sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==}
+ engines: {node: '>=12'}
+
'@mdx-js/mdx@3.1.1':
resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==}
@@ -1564,6 +1571,9 @@ packages:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
+ jsbi@4.3.2:
+ resolution: {integrity: sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==}
+
jsesc@3.1.0:
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
engines: {node: '>=6'}
@@ -1892,6 +1902,10 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+ node-ical@0.25.2:
+ resolution: {integrity: sha512-DJnT/UqBPxVO7Inx5RMuSSqtyk9oUz2DLiKsvlSjGNycUACSM5gGYSkKLIerwJXTeFxs3X24R9ds4iDwd5z4KA==}
+ engines: {node: '>=18'}
+
node-releases@2.0.27:
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
@@ -2177,6 +2191,9 @@ packages:
resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
engines: {node: '>= 4'}
+ rrule-temporal@1.4.6:
+ resolution: {integrity: sha512-eRruK01K8uu0VWgn+lvKYXGZCER7eI2EaoAhsh5wwHaYMc4jPJ1cvY+bD0hBbYwCucggMpjtuwH2RpEH9oLfWg==}
+
sade@1.8.1:
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
engines: {node: '>=6'}
@@ -2868,6 +2885,10 @@ snapshots:
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
+ '@js-temporal/polyfill@0.5.1':
+ dependencies:
+ jsbi: 4.3.2
+
'@mdx-js/mdx@3.1.1(patch_hash=c3747fa40d5fa8b461c601e2f709b66223999ee269d6985ade30e835bf76f834)':
dependencies:
'@types/estree': 1.0.8
@@ -4363,6 +4384,8 @@ snapshots:
dependencies:
argparse: 2.0.1
+ jsbi@4.3.2: {}
+
jsesc@3.1.0: {}
json-buffer@3.0.1: {}
@@ -4836,6 +4859,10 @@ snapshots:
natural-compare@1.4.0: {}
+ node-ical@0.25.2:
+ dependencies:
+ rrule-temporal: 1.4.6
+
node-releases@2.0.27: {}
node-retrieve-globals@6.0.1:
@@ -5171,6 +5198,10 @@ snapshots:
retry@0.12.0: {}
+ rrule-temporal@1.4.6:
+ dependencies:
+ '@js-temporal/polyfill': 0.5.1
+
sade@1.8.1:
dependencies:
mri: 1.2.0
From d0ed9f946444b1809bba91a92164d3e50aa22183 Mon Sep 17 00:00:00 2001
From: maximsmol <1472826+maximsmol@users.noreply.github.com>
Date: Wed, 4 Mar 2026 11:42:40 -0800
Subject: [PATCH 2/2] pull locations and images (latter doesnt work)
Signed-off-by: maximsmol <1472826+maximsmol@users.noreply.github.com>
---
eleventy.config.js | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
diff --git a/eleventy.config.js b/eleventy.config.js
index 9981ea6a..ded181a1 100644
--- a/eleventy.config.js
+++ b/eleventy.config.js
@@ -243,9 +243,9 @@ export default async (cfg) => {
cfg.addTemplateFormats("mdx");
const fetchCalendar = async () => {
- const data = await ical.fromURL(
- "https://dev.techworkerscoalition.org/nextcloud/remote.php/dav/public-calendars/EHFzzZQXSQoS3f5w/?export",
- );
+ const base =
+ "https://dev.techworkerscoalition.org/nextcloud/remote.php/dav/public-calendars/EHFzzZQXSQoS3f5w/?export";
+ const data = await ical.fromURL(base);
if (data == null) throw new Error("failed to load calendar data");
@@ -254,6 +254,12 @@ export default async (cfg) => {
if (entry == null) continue;
if (entry.type !== "VEVENT") continue;
+ const img =
+ entry.attach?.val != null &&
+ entry.attach?.params.FMTTYPE.startsWith("image/")
+ ? new URL(entry.attach.val, base)
+ : undefined;
+
const start = DateTime.fromJSDate(entry.start, {
zone: entry.start.tz,
});
@@ -266,8 +272,8 @@ export default async (cfg) => {
data: {
title: entry.summary,
time_zones: entry.start.tz != null ? [entry.start.tz] : undefined,
- // todo(maximsmol): add locations
- // locations: [],
+ image: img?.href,
+ locations: entry.location != null ? [entry.location] : undefined,
},
content:
entry.description != null ? md.render(entry.description) : undefined,