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 owns routing, layouts, metadata, and static content pages.
src/layouts/BaseLayout.astroprovides the document shell;MapLayout.astro,SiteLayout.astro, andDocsLayout.astroprovide the map, standard content, and Organizer Kit shells respectively. - Vue handles the interactive map UI as
client:only="vue"island components. Map-specific interactive features belong in Vue; static site and Organizer Kit pages belong in Astro and Markdown content collections.
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>.
The global Markdown pipeline runs rehype-table-wrapper and rehype-heading-anchors, which respectively wrap rendered tables in .table-wrapper and add permalink anchors to h2–h6. Their presentation styles live in the shared prose.css layer, scoped to both .prose and .docs-prose, because both plugins apply to all Markdown collections.
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 inMapLayout.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. - 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 |
src/components/NodePanel.vue |
Slide-in event detail panel with minimap, calendar links, share button |
src/components/LanguageSwitcher.vue |
Language selector dropdown in the top bar |
src/components/CopyMarkdownButton.astro |
Copies an Organizer Kit page as Markdown with accessible success/error feedback |
src/components/Header.astro |
Shared fixed site header and primary navigation |
src/components/Footer.astro |
Shared site footer, policy links, community links, and sponsors |
src/layouts/BaseLayout.astro |
Shared HTML document shell and metadata |
src/layouts/MapLayout.astro |
Map-page shell and Leaflet stylesheet links |
src/layouts/SiteLayout.astro |
Standard static content-page shell |
src/layouts/DocsLayout.astro |
Organizer Kit shell with sidebar, page TOC, and footer |
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/base.css |
Shared design tokens, reset, typography, focus, and skip-link styles |
src/styles/map.css |
Map layout, controls, popup styling, and Leaflet overrides |
src/styles/prose.css |
Standard static content-page presentation styles |
src/styles/docs/*.css |
Organizer Kit's modular Just-the-Docs-derived tokens, layout, navigation, and Markdown presentation styles |
src/lib/rehype-table-wrapper.mjs |
Markdown rehype plugin that wraps rendered tables for horizontal scrolling |
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
- The site is light-mode only — there is no dark mode, no
[data-theme]toggling, and no theme-related CSS. Do not reintroduce it without an explicit decision to do so.
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.
The browser-facing CARTO tile key is supplied at build time through
PUBLIC_CARTO_API_KEY. Configure it as a Netlify environment variable with
Builds scope; for local development it may be placed in pcd-website/.env.
Never commit the key. Without it, the site falls back to unkeyed tile URLs for
local development and may display CARTO's API-key watermark.