Skip to content

Commit 1ee5de6

Browse files
SableRafclaude
andcommitted
Add data.json structure, generation, and output validation
Creates a data.json file at build that compiles metadata about the events into one file Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5744b7b commit 1ee5de6

4 files changed

Lines changed: 139 additions & 0 deletions

File tree

.github/scripts/data-json.test.mjs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { test, describe } from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import { readFileSync } from 'node:fs';
4+
import { resolve } from 'node:path';
5+
6+
const dataJsonPath = resolve(import.meta.dirname, '../../pcd-website/dist/data.json');
7+
8+
let parsed;
9+
try {
10+
const raw = readFileSync(dataJsonPath, 'utf8');
11+
parsed = JSON.parse(raw);
12+
} catch (err) {
13+
throw new Error(
14+
`Failed to read or parse pcd-website/dist/data.json: ${err.message}\n` +
15+
`Run "npm run build" from pcd-website/ before running this test.`
16+
);
17+
}
18+
19+
describe('data.json structure', () => {
20+
test('top-level keys present', () => {
21+
assert.ok('schema_version' in parsed, 'missing schema_version');
22+
assert.ok('generated_at' in parsed, 'missing generated_at');
23+
assert.ok('event_count' in parsed, 'missing event_count');
24+
assert.ok('events' in parsed, 'missing events');
25+
});
26+
27+
test('event_count matches events.length', () => {
28+
assert.equal(parsed.event_count, parsed.events.length);
29+
});
30+
});
31+
32+
describe('data.json events', () => {
33+
test('no event contains primary_contact', () => {
34+
for (const event of parsed.events) {
35+
assert.ok(!('primary_contact' in event), `event ${event.id} contains primary_contact`);
36+
}
37+
});
38+
39+
test('no event contains placeholder', () => {
40+
for (const event of parsed.events) {
41+
assert.ok(!('placeholder' in event), `event ${event.id} contains placeholder`);
42+
}
43+
});
44+
45+
test('every canonical_url matches expected pattern', () => {
46+
for (const event of parsed.events) {
47+
const expected = `https://day.processing.org/event/${event.id}-${event.uid}/`;
48+
assert.equal(
49+
event.canonical_url,
50+
expected,
51+
`event ${event.id}: canonical_url mismatch`
52+
);
53+
}
54+
});
55+
56+
test('every event has finite lat and lng', () => {
57+
for (const event of parsed.events) {
58+
assert.ok(Number.isFinite(event.lat), `event ${event.id}: lat is not finite`);
59+
assert.ok(Number.isFinite(event.lng), `event ${event.id}: lng is not finite`);
60+
}
61+
});
62+
});

AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ node --test .github/scripts/event-issue-helpers.test.mjs
3434
node --test .github/scripts/process-new-event-issue.test.mjs
3535
node --test .github/scripts/process-edit-event-issue.test.mjs
3636
node --test .github/scripts/plus-code.test.mjs
37+
38+
# Requires npm run build from pcd-website/ first:
39+
node --test .github/scripts/data-json.test.mjs
3740
```
3841

3942
No install needed — `open-location-code` is already available at `pcd-website/node_modules/`.
@@ -62,6 +65,8 @@ Event data lives in `src/content/events/<event-id>/`:
6265

6366
**If a plus_code is invalid or too short, the build fails with a clear error — this is intentional.**
6467

68+
**"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.
69+
6570
### Key implementation details
6671

6772
- **Leaflet CSS** is loaded via `<link>` tags in `index.astro`, NOT via JS imports — avoids SSR issues since MapView is `client:only="vue"`.
@@ -83,6 +88,7 @@ Event data lives in `src/content/events/<event-id>/`:
8388
| `src/lib/format.ts` | `formatDate()`, `formatDateRange()`, `calendarLinks()`, etc. |
8489
| `src/lib/popup.ts` | Leaflet popup HTML generation (`makePopupContent()`) |
8590
| `src/styles/global.css` | Design tokens (CSS custom properties), IBM Plex Sans, Leaflet overrides |
91+
| `src/pages/data.json.ts` | Static JSON feed of confirmed events, served at /data.json |
8692
| `src/content.config.ts` | Astro content collection Zod schema for events |
8793
| `src/config.ts` | Global static constants (contact email, etc.) |
8894
| `src/i18n/index.ts` | Creates the `vue-i18n` instance and exports `syncLocale()` |

TEST.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,24 @@ These tests cover the shared pure functions extracted into `event-issue-helpers.
5656

5757
---
5858

59+
## data.json build output
60+
61+
**File:** `.github/scripts/data-json.test.mjs`
62+
**Run:** `node --test .github/scripts/data-json.test.mjs`
63+
**Requires:** `npm run build` must be run from `pcd-website/` first — this test reads `pcd-website/dist/data.json`.
64+
65+
| Case | Expected |
66+
|---|---|
67+
| File exists and is valid JSON | Passes without error |
68+
| Top-level keys | `schema_version`, `generated_at`, `event_count`, `events` all present |
69+
| `event_count` vs `events.length` | Equal |
70+
| No `primary_contact` in any event | Omitted (privacy) |
71+
| No `placeholder` in any event | Omitted (all feed entries are confirmed) |
72+
| `canonical_url` shape | Exactly `https://day.processing.org/event/${id}-${uid}/` |
73+
| `lat` and `lng` | Finite numbers on every event |
74+
75+
---
76+
5977
## Plus Code functions
6078

6179
**File:** `.github/scripts/plus-code.test.mjs`

pcd-website/src/pages/data.json.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type { APIRoute } from 'astro';
2+
import { loadNodes, canonicalEventId } from '../lib/nodes';
3+
4+
export const GET: APIRoute = async ({ site }) => {
5+
const nodes = await loadNodes();
6+
const siteUrl = site!.href.replace(/\/$/, '');
7+
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
8+
9+
const events = nodes
10+
.filter((node) => !node.placeholder)
11+
.map((node) => ({
12+
id: node.id,
13+
uid: node.uid,
14+
canonical_url: `${siteUrl}${base}/event/${canonicalEventId(node)}/`,
15+
event_name: node.event_name,
16+
city: node.city ?? null,
17+
country: node.country ?? null,
18+
location_name: node.location_name ?? null,
19+
address: node.address ?? null,
20+
location_tbd: node.location_tbd ?? false,
21+
plus_code: node.plus_code,
22+
lat: node.lat,
23+
lng: node.lng,
24+
event_date: node.event_date ?? null,
25+
event_end_date: node.event_end_date ?? null,
26+
event_start_time: node.event_start_time ?? null,
27+
event_end_time: node.event_end_time ?? null,
28+
date_tbd: node.date_tbd ?? false,
29+
time_tbd: node.time_tbd ?? false,
30+
online_event: node.online_event ?? false,
31+
event_url: node.event_url ?? null,
32+
event_page_url: node.event_page_url ?? null,
33+
event_short_description: node.event_short_description,
34+
details_text: node.details_text,
35+
event_activities: node.event_activities,
36+
organizers: node.organizers,
37+
organization_name: node.organization_name ?? null,
38+
organization_url: node.organization_url ?? null,
39+
organization_type: node.organization_type ?? null,
40+
forum_thread_url: node.forum_thread_url ?? null,
41+
}));
42+
43+
const payload = {
44+
schema_version: 1,
45+
generated_at: new Date().toISOString(),
46+
event_count: events.length,
47+
events,
48+
};
49+
50+
return new Response(JSON.stringify(payload, null, 2), {
51+
headers: { 'Content-Type': 'application/json' },
52+
});
53+
};

0 commit comments

Comments
 (0)