This file provides guidance to AI agents when working with code in this repository.
When making changes to the codebase, please also update this file as needed to reflect any new patterns, tools, or workflows that agents should be aware of. Especially when refactors or architectural changes are made, please update the "Architecture" and "Tech Stack" sections to reflect the new structure and technologies used in the project.
Static website for Processing Community Day (PCD) 2026 — a global map of events. Built with Astro 5 (static output) + Vue 3 + Leaflet. No backend, no database, no API calls at runtime.
The Astro project root is pcd-website/. All build commands run from there.
All from the pcd-website/ directory:
npm install
npm run dev # localhost:4321
npm run build # production build → dist/
npm run preview # preview production buildThere are currently no lint scripts configured.
See TEST.md for the full test inventory and coverage notes.
node --test .github/scripts/event-issue-helpers.test.mjs
node --test .github/scripts/process-new-event-issue.test.mjs
node --test .github/scripts/process-edit-event-issue.test.mjs
node --test .github/scripts/plus-code.test.mjs
# Requires npm run build from pcd-website/ first:
node --test .github/scripts/data-json.test.mjsNeed to run the tests end-to-end? ./scripts/run-tests.sh executes the helper, intake, and plus-code suites, builds the Astro site via npm --prefix pcd-website run build, and then runs data-json.test.mjs in sequence. Run this script from the repo root after installing dependencies so you get the full battery of checks in one shot.
No install needed — open-location-code is already available at pcd-website/node_modules/.
- Tests live alongside the code they test in
.github/scripts/. - Use
node:test+node:assert(built into Node — no test framework needed). - Mock
globalThis.fetchwithbeforeEach/afterEachfor any test that triggers a Nominatim call; always restore the original after each test. - When adding new functions to
.github/scripts/, extract pure/testable logic into a separate*.mjsmodule (as was done forplus-code.mjs) so it can be imported without triggering the main script's top-level side effects.
- Astro (
src/pages/index.astro) is the single entry point — a static HTML shell with<head>, font/CSS links, and BASE_URL injection. No new Astro pages should be added. - Vue handles all interactive UI as
client:only="vue"island components. New UI features go in Vue components, not Astro pages.
Event data lives in src/content/events/<event-id>/:
metadata.json— event fields (id, uid, name, location, dates, organizers, etc.)content.md— markdown body (frontmatter must includeid:anduid:)uid:values in frontmatter must always be quoted (uid: "abc1234") because unquoted hex strings like1e46977are parsed as scientific notation by YAML, destroying the value.
src/lib/nodes.ts loads all events at Astro build time using import.meta.glob() + getCollection('events'), validates plus codes with OpenLocationCode, decodes lat/lng, and returns a sorted Node[] array passed as props to <MapView>.
If a plus_code is invalid or too short, the build fails with a clear error — this is intentional.
"Confirmed" events in data.json: An event is included in the /data.json feed if it is present in loadNodes() and has no placeholder: true flag. There are currently no other event states (draft, hidden, etc.). If new states are added in future, the filter in src/pages/data.json.ts must be updated explicitly.
- Leaflet CSS is loaded via
<link>tags inindex.astro, NOT via JS imports — avoids SSR issues since MapView isclient:only="vue". open-location-codeexports{ OpenLocationCode }as a named export — usenew OpenLocationCode()(not static methods).leaflet.markerclustercauses a circular dependency warning, suppressed viarollupOptions.onwarninastro.config.mjs.- Deep linking:
?event=<id-or-uid>query param auto-opens the event detail panel. Both the slugidand the shortuidare accepted. - Map style preference persisted in
localStorage. - Event UIDs: Each event has a stable 7-char hex
uidstored in bothmetadata.jsonandcontent.mdfrontmatter. UIDs never change after creation. Three static URL formats are generated per event:/event/<slug>(redirects to canonical),/event/<slug>-<uid>(canonical, has OG tags, redirects into SPA), and/event/<uid>(short form, redirects to canonical). The canonical URL is what the share button copies.
| File | Role |
|---|---|
src/components/MapView.vue |
Leaflet map, marker clustering, keyboard shortcuts, tile layer switching |
src/components/NodePanel.vue |
Slide-in event detail panel with minimap, calendar links, share button |
src/components/NodeList.vue |
Alphabetical event list overlay with map style switcher + dark mode toggle |
src/components/LanguageSwitcher.vue |
Language selector dropdown in the top bar |
src/lib/analytics.ts |
trackEvent() Fathom helper + AnalyticsEvent type + event-name constants |
src/lib/nodes.ts |
Node interface + loadNodes() |
src/lib/format.ts |
formatDate(), formatDateRange(), calendarLinks(), etc. |
src/lib/popup.ts |
Leaflet popup HTML generation (makePopupContent()) |
src/styles/global.css |
Design tokens (CSS custom properties), IBM Plex Sans, Leaflet overrides |
src/pages/data.json.ts |
Static JSON feed of confirmed events, served at /data.json |
src/content.config.ts |
Astro content collection Zod schema for events |
src/config.ts |
Global static constants (contact email, etc.) |
src/i18n/index.ts |
Creates the vue-i18n instance and exports syncLocale() |
src/i18n/localeState.ts |
Reactive currentLocale ref, browser detection, localStorage persistence |
src/i18n/vuePlugin.ts |
Astro appEntrypoint — installs vue-i18n on every Vue island |
src/i18n/locales/en.json |
Source-of-truth translation file (all keys must exist here) |
src/i18n/locales/*.json |
Per-language translations (es, de, fr, pt, zh-TW, zh-CN, ja, ko) |
The site uses vue-i18n@11 with 9 supported locales: en, es, de, fr, pt, zh-TW, zh-CN, ja, ko.
vue-i18nis installed globally viaastro.config.mjs→vue({ appEntrypoint: '/src/i18n/vuePlugin' }).- Locale detection order: localStorage (
pcd-locale) →navigator.language→'en'. - The active locale is a reactive singleton (
currentLocaleref inlocaleState.ts) shared across all components.
- Always add the key to
en.jsonfirst. It is the source of truth and the fallback for all other locales. - Add the same key to every other locale file in
src/i18n/locales/. Missing keys fall back to English silently. - In Vue components, use
const { t, locale } = useI18n()and replace hardcoded text witht('key'). - In non-component TS files (e.g.
popup.ts), usei18n.global.t('key')imported fromsrc/i18n/index.ts. - Pass
locale(orlocale.valueas a string) toformatDateRange(),formatDate(), etc. for locale-aware date formatting.
Event data coming from content files — event_name, details_text, city, country, organization_name, organizer names, URLs — must never be wrapped in t(). Only static UI strings get translated.
Non-English locales use "Events" (not "Nodes") in list/dialog labels, since "Nodes" is a technical term that doesn't translate naturally.
Use src/config.ts for static, non-secret values that are referenced across multiple files or are likely to change. Import from it rather than hardcoding inline.
Store here:
- Contact emails (e.g.
PCD_EMAIL) - Stable URLs referenced in UI (e.g. a feedback form link)
- Project-wide constants (e.g. site name, org name)
Do not store here:
- Environment-specific or secret values — use
.envwithimport.meta.envfor those - Anything already defined in
astro.config.mjs(e.g. base path) - Component-local constants that aren't shared
- Always support both light and dark mode for any new or modified UI elements.
- Dark mode is toggled via
[data-theme="dark"]on<html>(set byNodeList.vue). - Dark mode uses CSS custom properties defined in
global.cssunder[data-theme="dark"]. - Vue scoped styles cannot target ancestor-based dark mode selectors — put those overrides in
global.css.
Must follow standard accessibility best practices (semantic HTML, ARIA attributes, keyboard navigation, focus management) for all interactive components (map, panels, buttons, etc.). WCAG 2.1 AA compliance is the goal.
New events are submitted via GitHub Issues using .github/ISSUE_TEMPLATE/01-new-event.yml. The workflow .github/workflows/new-event-intake.yml (process-new-event job) runs .github/scripts/process-new-event-issue.mjs to validate the issue and, if valid, opens a PR with generated metadata.json + content.md files. A stable uid is generated at intake and written into both files.
Organizers can edit existing events via .github/ISSUE_TEMPLATE/04-edit-event.yml. The same workflow (process-edit-event job) runs .github/scripts/process-edit-event-issue.mjs. The edit script: reads the existing event by event_id, preserves the immutable uid and intake block, preserves event_activities if all checkboxes are unchecked (GitHub issue forms cannot prefill checkboxes), and preserves content.md if full_description is blank.
Pure functions shared by both intake scripts live in .github/scripts/event-issue-helpers.mjs. This includes parseIssueSections, validation helpers, slugify, parseActivities, parseOrganizers, buildValidationComment, and generateUniqueUid.
Both scripts guard against running on the wrong template:
process-new-event-issue.mjsskips if the body contains### Event ID(unique to the edit template)process-edit-event-issue.mjsskips if the body does NOT contain### Event ID
Netlify, configured via netlify.toml. The site deploys to https://day.processing.org/ on push to main.