🧠 Context
The webring embed widget fetches a small JSON file listing the ring members so it can compute prev/random/next. Today that file is a hand-written placeholder at public/ring.json (with a public/ring-README.txt explaining it's temporary). This ticket replaces it with a version generated at build time from the webring content collection, so the ring data is always derived from the real member entries — no hand-maintained list.
The generated file:
- Includes only members with
inRing === true (some members are listed in the directory but opt out of the ring).
- Contains just the fields the widget needs:
name and url.
- Is ordered by graduation year ascending, with
name as a stable tiebreak for members sharing a year (deterministic ordering matters — it defines the prev/next traversal).
- Is emitted at the same path,
/ring.json, with the same array-of-{name, url} shape as the placeholder — so anything already developed against the placeholder keeps working unchanged.
The graph visualization reads the webring collection directly at build; ring.json exists specifically as the external embed's data source. This ticket is independent of the embed work — that builds against whatever is served at /ring.json (placeholder or generated).
Files you'll touch:
src/pages/ring.json.ts (new — the static endpoint that generates the file)
public/ring.json (delete — replaced by the endpoint)
public/ring-README.txt (delete — no longer a placeholder to explain)
Don't touch:
- The
webring schema in src/content.config.ts.
webring.astro, RingGraph.astro, or other components.
🛠️ Implementation Plan
Note: Any code snippets may not work immediately, adjust as needed.
-
Add a static JSON endpoint. Create src/pages/ring.json.ts. In a static build, an Astro endpoint at this path emits a real /ring.json file. Pull the collection, filter to inRing, sort, and project to the needed fields:
import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';
export const GET: APIRoute = async () => {
const members = await getCollection('webring');
// Build `ring` from `members`:
// - .filter() to keep only inRing === true
// - .sort() by graduation year ascending, then name as a stable tiebreak
// (year difference, falling back to localeCompare on name)
// - .map() each down to just { name, url }
// This order defines prev/next traversal in the embed — keep it deterministic.
const ring = /* ... */;
return new Response(JSON.stringify(ring, null, 2), {
headers: { 'Content-Type': 'application/json' },
});
};
-
Remove the placeholders. Delete public/ring.json and public/ring-README.txt. (Two files can't serve the same /ring.json path — the old static file must go so the generated endpoint owns it.)
-
Verify. The generated file lands in the build output (dist/ring.json), not in public/ — public/ is an input folder, and the endpoint is served at the /ring.json URL (dynamically in pnpm dev, emitted to dist/ on pnpm build). Run pnpm build and confirm dist/ring.json exists, is valid JSON, contains only inRing: true members as { name, url }, and is ordered by grad year. Toggle a member's inRing to false in a content entry and rebuild to confirm they drop out. (If the site later sets a base path, the file is served under that base — that's the embed's concern, not this ticket's.)
✅ Acceptance Criteria
🧠 Context
The webring embed widget fetches a small JSON file listing the ring members so it can compute prev/random/next. Today that file is a hand-written placeholder at
public/ring.json(with apublic/ring-README.txtexplaining it's temporary). This ticket replaces it with a version generated at build time from thewebringcontent collection, so the ring data is always derived from the real member entries — no hand-maintained list.The generated file:
inRing === true(some members are listed in the directory but opt out of the ring).nameandurl.nameas a stable tiebreak for members sharing a year (deterministic ordering matters — it defines the prev/next traversal)./ring.json, with the same array-of-{name, url}shape as the placeholder — so anything already developed against the placeholder keeps working unchanged.The graph visualization reads the
webringcollection directly at build;ring.jsonexists specifically as the external embed's data source. This ticket is independent of the embed work — that builds against whatever is served at/ring.json(placeholder or generated).Files you'll touch:
src/pages/ring.json.ts(new — the static endpoint that generates the file)public/ring.json(delete — replaced by the endpoint)public/ring-README.txt(delete — no longer a placeholder to explain)Don't touch:
webringschema insrc/content.config.ts.webring.astro,RingGraph.astro, or other components.🛠️ Implementation Plan
Note: Any code snippets may not work immediately, adjust as needed.
Add a static JSON endpoint. Create
src/pages/ring.json.ts. In a static build, an Astro endpoint at this path emits a real/ring.jsonfile. Pull the collection, filter toinRing, sort, and project to the needed fields:Remove the placeholders. Delete
public/ring.jsonandpublic/ring-README.txt. (Two files can't serve the same/ring.jsonpath — the old static file must go so the generated endpoint owns it.)Verify. The generated file lands in the build output (
dist/ring.json), not inpublic/—public/is an input folder, and the endpoint is served at the/ring.jsonURL (dynamically inpnpm dev, emitted todist/onpnpm build). Runpnpm buildand confirmdist/ring.jsonexists, is valid JSON, contains onlyinRing: truemembers as{ name, url }, and is ordered by grad year. Toggle a member'sinRingtofalsein a content entry and rebuild to confirm they drop out. (If the site later sets abasepath, the file is served under that base — that's the embed's concern, not this ticket's.)✅ Acceptance Criteria
/ring.jsonis generated at build from thewebringcollection (no hand-maintained list).inRing === truemembers appear, each as{ name, url }./ring.json) and array shape match the old placeholder, so existing consumers keep working.public/ring.jsonandpublic/ring-README.txtare deleted.pnpm format:check,pnpm check, andpnpm buildall pass.