v1.9.0 — Per-event calendar downloads and a news RSS feed
The pre-Stockholm cut, four days before the Summer School and European Security Conference open at Stockholm University. Visitors who land on the home page in the run-up to the conference get a calendar-savvy events block (per-card Add to calendar dropdown, every event downloadable as a single
.ics), an RSS feed for the news block, and home-page cards that no longer drift between locales because they all derive from JSON. Three themes below; a single canonical index at the bottom.
Calendar plumbing
The website has shipped a single calendar.ics subscribable feed since v1.3.0. v1.9.0 keeps that, and adds one .ics per event under /calendar/<slug>.ics. Slugs derive from each event's UID (summer-school-2026@netsec-cost.eu → /calendar/summer-school-2026.ics); scripts/build-calendar.py refuses non-conforming slugs at generation time so URLs stay predictable, removing an event from JSON auto-deletes its .ics, and the existing calendar-drift CI workflow now watches calendar/** too.
Every event card on the home page carries a new Add to calendar dropdown with four destinations: Google Calendar (prefilled template), Outlook web compose, Apple webcal:// subscription, and direct .ics download. The menu reparents itself to <body> on first open and pins with position: fixed against the trigger's bounding rect, so it escapes the stacking context that .event-card creates via backdrop-filter — otherwise the menu was occluded by the next card down. Same portal pattern as the ESSC member-preview popover; the decision is logged on the Wiki Decisions page.
Home page derives from data
Both the events block and the news block on index.html (+ FR + DE) now render at runtime from JSON. data/events.json and a new data/news.json are the source of truth; the renderers in assets/js/home-events.js and assets/js/home-news.js pick the locale from <html lang>, sort items, and rebuild the relevant <div>. The pre-existing hand-coded HTML survives as a fail-soft fallback that the renderer empties on success — so a fetch failure leaves visitors with a coherent page rather than an empty section. Card descriptions get a five-line clamp with a Read more / Lire la suite / Mehr anzeigen toggle that's only injected when the rendered text actually overflows; @media print drops the clamp.
This closes #249 and the drift class it tracked. The home-page event cards used to be hand-authored across three locales and could drift from the calendar feed; PR #248 had to paper over an "Applications now open" CTA that was already past its closing deadline. From v1.9.0 onwards, both lists share the same source of truth as the public /calendar.ics and /news.xml feeds. The Indico-write tooling continues to overwrite summary / start / end per indicoEventId; the renderer prefers cardTitle.{locale} over summary so localised home-page titles aren't affected.
News on the open web
/news.xml is a new RSS 2.0 feed exposing the home-page news cards to feed readers (Feedly, Inoreader, NetNewsWire, Reeder). scripts/build-news-rss.py generates it from data/news.json with <atom:link rel="self">, CDATA-wrapped descriptions, RFC 822 pubDate, and guid isPermaLink="false". A new news-drift CI workflow runs --check on every PR touching the data or the generator, mirroring the calendar-drift shape.
Every home page carries <link rel="alternate" type="application/rss+xml"> in <head> so feed readers auto-discover the feed without the visitor needing to know the URL; a visible Subscribe to NetSec news (RSS) affordance under the news block (mirroring the existing calendar-subscribe block) covers visitors who don't read <head>. Single-language EN by RSS convention — per-locale feeds (/news.fr.xml, /news.de.xml) are a deferred follow-up if reader demand surfaces.
Index of changes
Added
- Per-event
.icsdownloads at/calendar/<slug>.ics.scripts/build-calendar.pynow writes one.icsper event indata/events.json, in addition to the existing aggregate/calendar.icssubscribable feed. Slug derives from the event's UID (summer-school-2026@netsec-cost.eu→/calendar/summer-school-2026.ics); the script refuses non-conforming slugs (^[a-z0-9-]+$) at generation time so URLs stay predictable. The per-event files carry the same VTIMEZONE block as the aggregate but noREFRESH-INTERVAL/X-PUBLISHED-TTLsince they're one-shot import downloads, not subscribable feeds. Removing an event from JSON auto-deletes the matching/calendar/*.ics; the existingcalendar-driftCI workflow now watchescalendar/**too and fails the build if any output is stale. #257. - Home-page event cards now derive from
data/events.json(closes #249 renderer half). Newassets/js/home-events.jsreads the events JSON onDOMContentLoaded, picks the locale from<html lang>, and rebuilds the#events .event-listcards from structured data. Schema extends each event witheventType,featured,displayDate,cardTitle,cardDescription,meta[], andcta— all with{en, fr, de}blocks where appropriate. The pre-existing hand-coded HTML survives as a fail-soft fallback. Card descriptions get a five-line clamp with a Read more / Lire la suite / Mehr anzeigen toggle injected only when the rendered text overflows;@media printdrops the clamp. Each card carries an Add to calendar dropdown with four destinations: Google Calendar (prefilled template), Outlook web compose deep-link, Applewebcal://subscription, and direct.icsdownload from the per-event files. Outside-click + Escape dismiss the menu. The renderer preferscardTitle.{locale}oversummaryso the Indico sync (which overwritessummaryviaindicoEventId) doesn't bleed into the localised home-page titles. #259. - News RSS feed at
/news.xml+ structured news data layer. Newdata/news.jsonis the source of truth for both the home-page news cards and the public RSS feed. Schema mirrorsdata/events.jsonper item.scripts/build-news-rss.pygeneratesnews.xmlin RSS 2.0 format with<atom:link rel="self">, per-item CDATA-wrapped descriptions,guid isPermaLink="false", and RFC 822pubDate. Single-language EN per RSS convention; FR / DE feeds deferred. The matchingnews-drift.ymlCI workflow mirrorscalendar-drift.assets/js/home-news.jsreads the JSON onDOMContentLoaded, picks the locale, sorts newest-first, and rebuilds#news .news-list; hand-coded HTML survives as fail-soft fallback. Every home page now carries<link rel="alternate" type="application/rss+xml">in<head>for feed-reader auto-discovery. #261. - Visible Subscribe to NetSec news (RSS) affordance under the news block on the home page (EN / FR / DE), mirroring the existing Subscribe to NetSec events calendar affordance. Hint sentence names three popular readers (Feedly, Inoreader, NetNewsWire) so non-technical visitors recognise the use case. #263.
- Visual sitemap entries for
/news.xmland/calendar.icsinsitemap.html(+ FR + DE). The Home sub-tree now reads News & announcements · RSS feed and Events · calendar.ics. The machine-readablesitemap.xmlis unchanged: RSS feeds and.icsfiles aren't pages and don't belong in<urlset>. #264. - Directory freshness line on The Network.
/people.html(+ FR + DE) now shows a discreet Directory last updated line under the page lede, driven bydata/bios.json's top-levelgenerated_atstamp. The stamp only moves whenscripts/sync-bios.pyproduces a substantive change, so the date stays honest across weeks with no new submissions. Locale-aware date formatting (en-GB/fr-FR/de-DE) and a manually translated label;docs/bios-setup.mdnow documents the field. Closes #271.
Fixed
- Add-to-calendar URLs now resolve the event's real UTC offset year-round.
assets/js/home-events.jshard-coded+02:00when building the Google Calendar and Outlook compose URLs. That matches Stockholm summer time but lands an hour off for any event in winter (CET,+01:00) or in a different zone. The two URL builders now resolve the offset for each event's wall-clock time viaIntl.DateTimeFormat, lifting the zone fromdata/events.json(top-leveltzid, with an optional per-eventtzidoverride). The.icsdownloads were always correct (they carry a VTIMEZONE block), so only the inline URLs changed. Closes #260. - 404-page broken-star fragments now stream on a wind. The loose pieces of the emblem (the cluster of yellow circles where three stars are missing from the top of the ring) used to bob straight up and down on a quiet
err-illu-driftloop. They now blow left to right: each fragment enters from down-and-left, lifts up and across the canvas, then fades, as if a wind is carrying the broken pieces off the page. Per-fragment animation delays spread the cluster across the cycle so the motion never stalls, and a new--frag-peakcustom property (set inline on each circle, matched to itsopacityattribute) carries the resting opacity through the keyframes so the depth of the original cloud survives.prefers-reduced-motionfalls back to the static attribute opacity with no transform. - 404-page polish, sixth pass: the locale-row gap finally takes, map fills the card. The button-to-locale gap had been bumped four times (28 px through 80 px across PRs #289, #293, #295, #299) with no visible effect, because the rule never applied:
.err-langis a paragraph and the earlier.err-page p { margin: 0 0 24px }has higher specificity (0,1,1 against the bare class's 0,1,0), so it resetmargin-topto 0 every render. Qualifying the selector as.err-page .err-lang(0,2,0) wins the cascade, and a measured 48 px now sits the locale row clearly below the buttons. Separately the map cap went from 460 px to 520 px: the card's content box is about 488 px wide, so a cap above that letswidth: 100%fill it edge to edge and the continent reads at full width rather than slightly pinched. - 404-page polish, fifth pass: wider Europe map, more air below the buttons. The map was capped at 340 px, which read as pinched in the 560 px glass card next to the EISS reference (which renders the same 10:7 outline at 48 rem). Bumped the cap to 460 px so the continent nearly fills the card's content width. The gap between the action buttons and the locale row went from 56 px to 80 px, the third adjustment on this spacing across review rounds.
- 404-page polish, fourth pass: real Europe map backdrop, more of the star ring broken, wider locale-row gap. The third pass (PR #293) dropped the abstract continent shape because four overlapping ellipses read as a flying-saucer disc rather than as Europe. This pass imports the same Natural Earth 110m admin-0 Europe outline the EISS site uses behind its conference map (
src/_includes/europe-outline.njk, 36 country paths,viewBox 0 0 1000 700), rendered at the full SVG canvas as a faint backdrop (light fill in light mode, muted navy in dark mode via the site's.darkancestor convention). At full-canvas scale a faithful outline reads as Europe rather than as a cartoon, which the icon-scale attempt could not. The broken-emblem metaphor is stronger too: three stars are now missing from the top of the ring (11, 12 and 1 o'clock) instead of one, with a larger cloud of yellow fragments drifting up from the gap. Separately, the gap between the action buttons and the Also available in… locale row went from 40 px to 56 px so the locale row sits clearly as a meta-footer. - Press-kit logo cards stacked full-width and touched each other. The §2 Logos and emblems markup referenced a
.grid-2class from the start, but the rule was never defined in the page's<style>block, so the four brand-asset cards fell back to full-width block layout with no gap between them. Defined.grid-2as a responsive two-up grid (repeat(auto-fit, minmax(280px, 1fr))with a 20 px gap) that collapses to one column on narrow viewports. Applied identically across EN / FR / DE. - Add-to-calendar dropdown menu was occluded by the next event card. The menu was absolutely positioned with
z-index: 20inside an.event-cardthat carries.glass→backdrop-filter, which per W3C spec creates a new stacking context. The menu stayed inside the card's context and the next card's own context rendered above it. Fix: reparent the menu to<body>on first open and pin withposition: fixedagainsttrigger.getBoundingClientRect(). Same portal pattern as the ESSC member-preview popover. Outside-click + Escape dismiss still work; page scroll / window resize close the menu (matches existing popover dismissal convention). #262. - 404-page polish, third pass: drop the failed continent backdrop, bump locale-row spacing. The abstract European-continent shape behind the broken-EU-stars ring (PR #291) read at icon scale as a flying-saucer disc rather than as Europe — four overlapping ellipses turn out not to suggest a continent without recognisable peninsulas, and a faithful Europe outline at 260 px wide either dominates or reads as a cartoon. Dropped the backdrop entirely; the 11-of-12 EU stars with the broken top carry the metaphor cleanly on their own. Separately, the 28 px gap between the action buttons and the Also available in… locale row read tight again once the illustration above the 404 changed the page's vertical rhythm; bumped to 40 px so the locale row sits clearly as meta-footer rather than as a sibling of the buttons.
- 404-page polish, second pass: stray horizontal rules + on-theme illustration. Two stray lines from the previous polish PR were cleaned up — the bordered
.search-resultslist rendered its border even when empty (drawing a thin rule below the search bar before the visitor had typed); added:empty { display: none }so the box only appears once there's something to show. Theborder-topseparator on.err-langfrom PR #289 read as a stray rule against the glass card; the 28 px gap alone reads as enough separation, so the border is gone. New decorative illustration above the 404 heading: a 12-star EU emblem ring with the 12-o'clock star "broken" — its 11 siblings stay in their canonical positions, a small cloud of yellow fragments drifts up from the gap (subtly animated, honoured byprefers-reduced-motion), and a faint abstract land-mass blob sits behind in EU blue (#003399at 12% in light mode,#7eb4ffat 10% in dark mode). The metaphor: a piece of the Union's emblem has come loose — on-theme for both COST/EU identity and the "page got disconnected" idea, without trying to literally trace the European continent at icon scale (a recognisable outline would either dominate or read as a cartoon).aria-hidden="true"so screen readers skip the decoration; the 404 + Page not found text already convey the message verbally. - 404-page polish: site-map button visibility, language switcher spacing + behaviour. Three follow-ups to the earlier 404 search rewrite. (a) The Open the site map button uses
.btn-ghost, which is bordered with--glass-bordersite-wide — intentionally faint for floating-glass UI but unreadable against the err-page glass card in both themes. Overridden locally on.err-actions .btn-ghostto use the stronger--linecolour (andrgba(255,255,255,.22)in dark mode) so the button reads as a clear secondary call-to-action against either theme. (b) The Also available in English · Français · Deutsch row used to sit 18 px below the action buttons and read as part of the same cluster; bumped to a 28 px gap with a thinborder-top: 1px solid var(--line)separator and 18 px internal padding. (c) The locale links used to navigate to the localised home (/,/index.fr.html,/index.de.html), yanking the visitor off the 404 they'd just landed on; new behaviour saves the chosen locale tolocalStorageand reloads the same URL, so GitHub Pages re-serves404.htmlfor the unknown path and the page re-renders in the new locale without leaving the visitor's broken-link context. The existinghrefis preserved as a no-JS fallback. The 404 i18n loop also got a small refactor: a reusableapplyLang(lang)function, EN now applied symmetrically (FR → EN no longer leaves the page frozen on French HTML defaults), and the currently-active locale is marked witharia-current="page"and styled as a label rather than a link. - 404-page search was leaking internal Pagefind plumbing into the UI. Pagefind UI v1.4+ auto-renders every
meta.*key it finds as aTitleCase: valuechip below the result card, which exposed our bio-stub meta as visible chrome (Country: ch,Wgs: 2,3,Photo: /assets/images/people/…jpg,Kind: bio,Affiliation:,Position:,Role:,Keywords:) and had no dark-mode CSS so result titles read as muddy gold on dark navy. The default UI also rendered the cleared-input control as a floating white pill outside the search box's right edge. Two changes converge: (a)scripts/build-bio-search-stubs.pynow writes meta-bearing elements outside<main data-pagefind-body>as<span hidden data-pagefind-meta="…">, so Pagefind's body excerpt is drawn from the bio body text rather than a concatenation of role + position + affiliation + country + WGs + keywords paragraphs that produced snippets like "Member of the NetSec community directory. MC member · …" under the result title; (b)404.htmldrops the bundledpagefind-uifor an inline mini-search that calls Pagefind's low-levelpagefind.jsdirectly and renders results via the same.search-result-*/.search-bio-*classes that the Cmd-K overlay uses — bio hits get the avatar + flag + WG-chip card, page hits get the title + section + excerpt layout, and dark mode comes for free. All 39 bio stubs regenerated.
Changed
- Shortened the What's New banner headline to "The full ESSC 2026 programme is live." (EN / FR / DE). The previous wording carried the Stockholm dates as a trailing clause, which made the banner wrap on narrow viewports; the dates already sit on
/essc-2026.htmlbehind the CTA, so the banner can stay terse. - Documentation pack cover bump from v1.9.3 to v1.9.4 matching website v1.9.0. Cover-only bump per CLAUDE.md §11; section-level catch-up to website v1.9.0 (Section 02 repo layout adding
scripts/build-news-rss.py+ thecalendar/directory + the new renderers, Section 04 architecture for the runtime-render-from-JSON pattern) queued under #229 → v1.11.0. New Appendix C entry summarises the gap. #265. - Press kit refreshed to v1.1 (EN / FR / DE). §2 Logos and emblems now documents the designer's four PNG brand-asset variants (
netsec-lockup-primary,-white,-mono,netsec-mark) shipped in v1.8.0, with download links for each, replacing the stale launch-era "Pure CSS, no asset to download" copy that pointed at the gradient "NS" placeholder. §8 documentation-pack size bumped from 8 MB to 21 MB to match the live PDF. Footer stamp updated to "Press kit v1.1 · revised 28 May 2026 · supersedes v1.0". - Wiki Decisions log picks up two v1.9.0 entries: (a) render home-page event and news cards from JSON at runtime, keep hand-coded HTML as fail-soft fallback (#249); (b) the body-portal pattern chosen for the Add-to-calendar dropdown to escape the
.glassstacking-context trap (#262).
🤖 Authored with help from Claude Code.