From 583231af37a9837d90ebab006d71913cea664c23 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20de=20Courville?=
Date: Mon, 24 Aug 2026 22:26:41 +0200
Subject: [PATCH 01/15] Zine library initial implementation
---
.../guide-print.pdf | Bin 0 -> 77 bytes
.../guide-small.pdf | Bin 0 -> 77 bytes
.../zine-integration-test-fixture/index.md | 5 +
.../metadata.json | 18 +++
.github/scripts/zine-build.test.mjs | 60 ++++++++++
.github/scripts/zines.test.mjs | 76 ++++++++++++
AGENTS.md | 13 +-
TEST.md | 15 ++-
netlify.toml | 10 ++
.../src/components/ActivityGuideGrid.astro | 41 +++++++
.../ActivityGuideSubmitButton.astro | 10 ++
pcd-website/src/config.ts | 26 ++++
pcd-website/src/content.config.ts | 8 ++
.../activity-guides/contribute-a-guide.md | 26 +---
.../organizer-kit/activity-guides/library.md | 16 +++
.../what-are-activity-guides.md | 12 --
.../activity-guides/zine-library.md | 9 --
.../getting-started/minimum-viable-pcd.md | 2 +-
pcd-website/src/content/zines/README.md | 19 +++
pcd-website/src/lib/zine-metadata.d.ts | 27 +++++
pcd-website/src/lib/zine-metadata.js | 98 +++++++++++++++
pcd-website/src/lib/zines.ts | 112 ++++++++++++++++++
.../src/pages/activity-guide/[id].astro | 77 ++++++++++++
.../src/pages/organize/[...slug].astro | 11 +-
pcd-website/src/styles/docs/components.css | 99 ++++++++++++++++
scripts/run-tests.sh | 4 +
26 files changed, 746 insertions(+), 48 deletions(-)
create mode 100644 .github/scripts/fixtures/zines/zine-integration-test-fixture/guide-print.pdf
create mode 100644 .github/scripts/fixtures/zines/zine-integration-test-fixture/guide-small.pdf
create mode 100644 .github/scripts/fixtures/zines/zine-integration-test-fixture/index.md
create mode 100644 .github/scripts/fixtures/zines/zine-integration-test-fixture/metadata.json
create mode 100644 .github/scripts/zine-build.test.mjs
create mode 100644 .github/scripts/zines.test.mjs
create mode 100644 pcd-website/src/components/ActivityGuideGrid.astro
create mode 100644 pcd-website/src/components/ActivityGuideSubmitButton.astro
create mode 100644 pcd-website/src/content/organizer-kit/activity-guides/library.md
delete mode 100644 pcd-website/src/content/organizer-kit/activity-guides/what-are-activity-guides.md
delete mode 100644 pcd-website/src/content/organizer-kit/activity-guides/zine-library.md
create mode 100644 pcd-website/src/content/zines/README.md
create mode 100644 pcd-website/src/lib/zine-metadata.d.ts
create mode 100644 pcd-website/src/lib/zine-metadata.js
create mode 100644 pcd-website/src/lib/zines.ts
create mode 100644 pcd-website/src/pages/activity-guide/[id].astro
diff --git a/.github/scripts/fixtures/zines/zine-integration-test-fixture/guide-print.pdf b/.github/scripts/fixtures/zines/zine-integration-test-fixture/guide-print.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..a2ccecc911d0a6eae548ba5102052b3d0ef38d04
GIT binary patch
literal 77
zcmY!laB]+href="([^"]+)"[^>]*>${label}`));
+ assert.ok(match, `expected a link labelled "${label}"`);
+ return match[1];
+}
+
+test('a populated zine collection emits linked assets and replaces its topic placeholder', () => {
+ assert.ok(!existsSync(DEST), `${DEST} already exists — refusing to overwrite`);
+ let created = true;
+ try {
+ cpSync(FIXTURE, DEST, { recursive: true });
+ copyFileSync(join(WEBSITE, 'src/images/og-image.png'), join(DEST, 'cover.png'));
+ execFileSync('npm', ['run', 'build'], { cwd: WEBSITE, stdio: 'pipe' });
+
+ const pagePath = join(DIST, 'activity-guide', SLUG, 'index.html');
+ assert.ok(existsSync(pagePath), 'the zine page should be generated');
+ const page = readFileSync(pagePath, 'utf8');
+ assert.match(page, /Loops with Shapes/);
+ assert.match(page, /View the original submission/);
+
+ for (const label of ['Read on screen', 'Print and fold']) {
+ assert.ok(existsSync(emittedPath(hrefForLabel(page, label))), `${label} should resolve to an emitted PDF`);
+ }
+ const pageCover = page.match(/ ]+src="([^"]+)"/);
+ assert.ok(pageCover, 'the zine page should render a cover image');
+ assert.ok(existsSync(emittedPath(pageCover[1])), 'the zine cover should be emitted');
+
+ const library = readFileSync(join(DIST, 'organize/activity-guides/library/index.html'), 'utf8');
+ assert.match(library, new RegExp(`href="/activity-guide/${SLUG}/"`));
+ assert.match(library, /A compact guide to making patterns with repeated shapes\./);
+ assert.doesNotMatch(library, /Loops<\/strong>\s*Guide wanted<\/span>/);
+ const grid = library.match(/([\s\S]*?)<\/ul>/);
+ assert.ok(grid, 'the library should render its grid');
+ assert.equal((grid[1].match(//g) ?? []).length, 12, 'the grid should always have eleven topics plus submission');
+ const gridCover = grid[1].match(/ ]+src="([^"]+)"/);
+ assert.ok(gridCover, 'the zine card should render a cover image');
+ assert.ok(existsSync(emittedPath(gridCover[1])), 'the card cover should be emitted');
+ } finally {
+ if (created) rmSync(DEST, { recursive: true, force: true });
+ }
+});
diff --git a/.github/scripts/zines.test.mjs b/.github/scripts/zines.test.mjs
new file mode 100644
index 0000000..e5dcf79
--- /dev/null
+++ b/.github/scripts/zines.test.mjs
@@ -0,0 +1,76 @@
+import { describe, test } from 'node:test';
+import assert from 'node:assert/strict';
+import {
+ assertIdentity, assertUniqueIds, assertUniqueTopics, parseZineMetadata,
+ resolveZineAssets, zineMetadataSchema,
+} from '../../pcd-website/src/lib/zine-metadata.js';
+
+const valid = () => ({
+ id: 'loops-with-shapes', title: 'Loops with Shapes', topic: 'Loops',
+ created_by: 'Guide Author', summary: 'Make patterns with repeated shapes.',
+ cover: 'cover.png', pdfs: [{ file: 'guide.pdf', label: 'Read on screen' }],
+ license: 'CC BY-SA 4.0',
+});
+
+describe('zine metadata', () => {
+ test('accepts valid metadata', () => {
+ assert.deepEqual(parseZineMetadata(valid(), 'loops-with-shapes'), valid());
+ });
+
+ test('rejects non-object metadata cleanly', () => {
+ for (const input of [null, 'metadata', []]) {
+ assert.throws(() => parseZineMetadata(input, 'loops-with-shapes'), /Invalid metadata/);
+ }
+ });
+
+ test('validates required values, topics, ids, and strict object keys', () => {
+ const cases = [
+ [{ ...valid(), topic: 'Physics' }, /topic/],
+ [{ ...valid(), title: ' ' }, /title/],
+ [{ ...valid(), id: 'Not Kebab' }, /id/],
+ [{ ...valid(), unexpected: true }, /Unrecognized key/],
+ [{ ...valid(), cover: undefined }, /cover/],
+ [{ ...valid(), cover: 'cover.PNG' }, /cover/],
+ [{ ...valid(), pdfs: [] }, /pdfs/],
+ [{ ...valid(), pdfs: [{ file: 'guide.PDF', label: 'PDF' }] }, /pdfs/],
+ [{ ...valid(), pdfs: [{ file: 'guide.txt', label: 'Text' }] }, /pdfs/],
+ [{ ...valid(), pdfs: [{ file: 'guide.pdf' }] }, /pdfs/],
+ [{ ...valid(), pdfs: [{ file: 'guide.pdf', label: 'PDF', extra: true }] }, /Unrecognized key/],
+ [{ ...valid(), license: 'CC BY 4.0' }, /license/],
+ ];
+ for (const [input, message] of cases) {
+ assert.throws(() => parseZineMetadata(input, 'loops-with-shapes'), message);
+ }
+ });
+
+ test('rejects draft zines with the safe unpublished location', () => {
+ assert.throws(() => parseZineMetadata({ ...valid(), draft: true }, 'loops-with-shapes'), /zines-drafts/);
+ });
+
+ test('guards source URLs without allowing malformed values to escape', () => {
+ for (const source_url of ['https://example.com', 'http://example.com/path']) {
+ assert.equal(zineMetadataSchema.safeParse({ ...valid(), source_url }).success, true);
+ }
+ for (const source_url of ['javascript:alert(1)', 'data:text/html,test', 'not a url']) {
+ assert.doesNotThrow(() => zineMetadataSchema.safeParse({ ...valid(), source_url }));
+ assert.equal(zineMetadataSchema.safeParse({ ...valid(), source_url }).success, false);
+ }
+ });
+
+ test('checks asset existence', () => {
+ assert.deepEqual(resolveZineAssets('loops-with-shapes', valid(), ['cover.png', 'guide.pdf']), {
+ cover: 'cover.png', pdfs: ['guide.pdf'],
+ });
+ assert.throws(() => resolveZineAssets('loops-with-shapes', valid(), ['cover.png']), /guide.pdf/);
+ assert.throws(() => resolveZineAssets('loops-with-shapes', valid(), ['guide.pdf']), /cover.png/);
+ });
+
+ test('checks unique topics, unique ids, and three-way identity', () => {
+ const first = valid();
+ const sameTopic = { ...valid(), id: 'other-loops' };
+ assert.throws(() => assertUniqueTopics([first, sameTopic]), /both claim/);
+ assert.throws(() => assertUniqueIds([first, { ...valid() }]), /Duplicate zine id/);
+ assert.throws(() => assertIdentity({ slug: 'loops-with-shapes', frontmatterId: 'loops', metadataId: 'loops-with-shapes' }), /identity mismatch/);
+ assert.doesNotThrow(() => assertIdentity({ slug: 'loops-with-shapes', frontmatterId: 'loops-with-shapes', metadataId: 'loops-with-shapes' }));
+ });
+});
diff --git a/AGENTS.md b/AGENTS.md
index 2fc0719..4930583 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -34,12 +34,14 @@ 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
+node --test .github/scripts/zines.test.mjs
+node --test .github/scripts/zine-build.test.mjs
# Requires npm run build from pcd-website/ first:
node --test .github/scripts/data-json.test.mjs
```
-Need 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.
+Need to run the tests end-to-end? `./scripts/run-tests.sh` executes the helper, intake, plus-code, and zine metadata suites; runs the zine fixture build; 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/`.
@@ -66,6 +68,10 @@ Event data lives in `src/content/events//`:
`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 ``.
+Activity Guide zines live in `src/content/zines//`, with `metadata.json`, `index.md`, a cover image, and one or more PDFs together in the same folder. `src/lib/zines.ts` joins the Astro collection, metadata, and assets at build time; `src/lib/zine-metadata.js` owns the strict schema and pure validation. Unlike events, zines must use `index.md` (not `content.md`) so Astro's glob loader makes the entry id equal to the folder slug. A zine may claim only one of the fixed topic slots, and no two zines may claim the same topic; violations fail the build.
+
+Zine PDFs are emitted from `src/` assets using `?url&no-inline`, so even small downloads become real files in `dist/`. `src/content/zines/` contains publishable zines only: it deliberately has no `draft` field because eager asset imports would make draft files public. Keep unfinished zines in `src/content/zines-drafts/`. Review PDFs for selectable text, logical reading order, document title and language, tagged headings where possible, alt text, and at least one screen-reader-friendly reading-order version.
+
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.**
@@ -104,7 +110,10 @@ The global Markdown pipeline runs `rehype-table-wrapper` and `rehype-heading-anc
| `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/pages/activity-guide/[id].astro` | Standalone per-zine Activity Guide pages |
+| `src/lib/zines.ts` | Build-time zine loader and topic-slot mapping |
+| `src/lib/zine-metadata.js` | Zine schema and pure metadata/asset validation |
+| `src/content.config.ts` | Astro content collection Zod schemas for events, legal pages, Organizer Kit, and zines |
| `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 |
diff --git a/TEST.md b/TEST.md
index 84f0ef2..b7d5fe9 100644
--- a/TEST.md
+++ b/TEST.md
@@ -74,9 +74,22 @@ These tests cover the shared pure functions extracted into `event-issue-helpers.
---
+## Activity Guide zines
+
+**Files:** `.github/scripts/zines.test.mjs`, `.github/scripts/zine-build.test.mjs`
+**Run:** `node --test .github/scripts/zines.test.mjs` and `node --test .github/scripts/zine-build.test.mjs`
+**Requires:** The metadata suite uses the locally installed Astro dependency. The build suite owns a temporary fixture zine, builds the site, verifies emitted cover/PDF URLs and cleans up its fixture.
+
+| Suite | Cases |
+|---|---|
+| `zines.test.mjs` | Schema, URL safety, draft rejection, identity, topic/id uniqueness, and asset validation |
+| `zine-build.test.mjs` | Populated collection routes, labelled PDF downloads emitted as files, cover emission, source link, topic replacement, and stable 12-card grid |
+
+---
+
## Single-command test run
-Run `./scripts/run-tests.sh` from the repo root after installing dependencies (`pcd-website` already has `node_modules/` from `npm install`). The script executes the helper, intake, and plus-code suites, then builds the Astro site (`npm run build` inside `pcd-website/`) before running `data-json.test.mjs`. Use this single command whenever you want to verify the full test battery end to end.
+Run `./scripts/run-tests.sh` from the repo root after installing dependencies (`pcd-website` already has `node_modules/` from `npm install`). The script executes the helper, intake, plus-code, and zine metadata suites; runs the zine fixture build; then builds the Astro site (`npm run build` inside `pcd-website/`) before running `data-json.test.mjs`. Use this single command whenever you want to verify the full test battery end to end.
---
diff --git a/netlify.toml b/netlify.toml
index b83d423..3320a3f 100644
--- a/netlify.toml
+++ b/netlify.toml
@@ -16,6 +16,16 @@
to = "/og-image.png"
status = 301
+[[redirects]]
+ from = "/organize/activity-guides/zine-library/*"
+ to = "/organize/activity-guides/library/"
+ status = 301
+
+[[redirects]]
+ from = "/organize/activity-guides/what-are-activity-guides/*"
+ to = "/organize/activity-guides/library/"
+ status = 301
+
[[headers]]
for = "/data.json"
[headers.values]
diff --git a/pcd-website/src/components/ActivityGuideGrid.astro b/pcd-website/src/components/ActivityGuideGrid.astro
new file mode 100644
index 0000000..bafd673
--- /dev/null
+++ b/pcd-website/src/components/ActivityGuideGrid.astro
@@ -0,0 +1,41 @@
+---
+import { Image } from 'astro:assets';
+import { ACTIVITY_GUIDE_SUBMIT_URL } from '../config';
+import { ZINE_TOPICS } from '../lib/zine-metadata.js';
+import { loadZinesByTopic } from '../lib/zines';
+import ExternalLinkIcon from './ExternalLinkIcon.astro';
+
+const zinesByTopic = await loadZinesByTopic();
+---
+
+
diff --git a/pcd-website/src/components/ActivityGuideSubmitButton.astro b/pcd-website/src/components/ActivityGuideSubmitButton.astro
new file mode 100644
index 0000000..51e9454
--- /dev/null
+++ b/pcd-website/src/components/ActivityGuideSubmitButton.astro
@@ -0,0 +1,10 @@
+---
+import { ACTIVITY_GUIDE_SUBMIT_URL } from '../config';
+import ExternalLinkIcon from './ExternalLinkIcon.astro';
+---
+
+
+
+ Submit an activity guide
+
+
diff --git a/pcd-website/src/config.ts b/pcd-website/src/config.ts
index 49ac945..e17f1ce 100644
--- a/pcd-website/src/config.ts
+++ b/pcd-website/src/config.ts
@@ -16,6 +16,32 @@ export const PCD_FORUM_NEW_TOPIC_URL =
tags: "pcd",
}).toString();
+export const ACTIVITY_GUIDE_SUBMISSION_TEMPLATE = `**Activity Title:**
+**Created by:**
+
+**Activity Format:** [workshop, discussion, creative exercise, group project, etc]
+
+**Topic:** [what does the activity explore?]
+**About the Activity:** [In 1-2 sentences, explain what participants will do and why a PCD organizer might choose this activity]
+
+**Duration:** [1 hour / 2 hours / 3 hours]
+**Tools or Materials needed:**
+
+**Link to Activity:**
+
+**License:** I confirm that I own or have permission to license this material, and I agree to publish my original contribution under the [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/). I have identified any third-party material that is not covered by this license.
+
+**Preferred Attribution:**`;
+
+export const ACTIVITY_GUIDE_SUBMIT_URL =
+ "https://discourse.processing.org/new-topic?" +
+ new URLSearchParams({
+ title: 'Activity Guide Submission: [Title of your Activity]',
+ body: ACTIVITY_GUIDE_SUBMISSION_TEMPLATE,
+ category: 'community',
+ tags: 'pcd,zine',
+ }).toString();
+
export const PCD_DISCORD_URL = "https://discord.gg/q5NksnwGsY";
export interface SocialLink {
diff --git a/pcd-website/src/content.config.ts b/pcd-website/src/content.config.ts
index 7bacfe8..270bf91 100644
--- a/pcd-website/src/content.config.ts
+++ b/pcd-website/src/content.config.ts
@@ -36,8 +36,16 @@ const organizerKit = defineCollection({
}),
});
+const zines = defineCollection({
+ // One flat folder per zine. `index.md` makes the collection entry id the
+ // folder slug; `content.md` would instead produce `/content`.
+ loader: glob({ base: './src/content/zines', pattern: '*/index.md' }),
+ schema: z.object({ id: z.string() }),
+});
+
export const collections = {
events,
legal,
organizerKit,
+ zines,
};
diff --git a/pcd-website/src/content/organizer-kit/activity-guides/contribute-a-guide.md b/pcd-website/src/content/organizer-kit/activity-guides/contribute-a-guide.md
index 8c4077a..99e2702 100644
--- a/pcd-website/src/content/organizer-kit/activity-guides/contribute-a-guide.md
+++ b/pcd-website/src/content/organizer-kit/activity-guides/contribute-a-guide.md
@@ -3,12 +3,9 @@ title: Contribute an Activity Guide
section: Activity Guides
order: 2
description: Instructions for creating and submitting a zine to the PCD Activity Guide Library.
-draft: true
---
-## Contribute an Activity Guide
-
-Have an activity idea for one of the topics listed below? Create a self-contained zine for a 1-3 hour session and share it on the PCD Forum. There is no required visual format. Make it your own, but include enough information for someone unfamiliar with the activity to prepare for and facilitate it successfully. Fill out the template here once you’re ready to submit. By submitting to the Activity Guide Zine Library, you agree to publish your original contribution under the [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
+Have an activity idea for one of the [Activity Guide Library topics](/organize/activity-guides/library/)? Create a self-contained zine for a 1-3 hour session and share it on the PCD Forum. There is no required visual format. Make it your own, but include enough information for someone unfamiliar with the activity to prepare for and facilitate it successfully. When you're ready, use the submission link below. By submitting to the Activity Guide Library, you agree to publish your original contribution under the [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
## Making a Zine
@@ -16,23 +13,8 @@ Have an activity idea for one of the topics listed below? Create a self-containe
*Tools*
*Examples*
-## Template for Forum Submissions
-
-**Activity Guide Submission:** \[Title of your Activity\]
-
-**Activity Title:**
-**Created by:**
-
-**Activity Format:** \[workshop, discussion, creative exercise, group project, etc\]
-
-**Topic:** \[what does the activity explore?\]
-**About the Activity:** \[In 1-2 sentences, explain what participants will do and why a PCD organizer might choose this activity\]
-
-**Duration:** \[ 1 hour / 2 hours / 3 hours\]
-**Tools or Materials needed:**
-
-**Link to Activity:**
+Include a cover image and one or more labelled PDFs. Please supply at least one PDF whose reading order matches the content, rather than only a print-imposed layout. PDFs should use selectable text (not scans), have a logical reading order, a document title and language, tagged headings where the tool allows, and alt text for images.
-**License:** I confirm that I own or have permission to license this material, and I agree to publish my original contribution under the [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/). I have identified any third-party material that is not covered by this license.
+## Submit Your Guide
-**Preferred Attribution:**
+The submission link opens a pre-filled forum post for your guide.
diff --git a/pcd-website/src/content/organizer-kit/activity-guides/library.md b/pcd-website/src/content/organizer-kit/activity-guides/library.md
new file mode 100644
index 0000000..1c4bb80
--- /dev/null
+++ b/pcd-website/src/content/organizer-kit/activity-guides/library.md
@@ -0,0 +1,16 @@
+---
+title: Activity Guide Library
+section: Activity Guides
+order: 1
+description: Community-created zines you can use to facilitate a session at your PCD.
+---
+
+Activity Guides are community-created zines that you can use to facilitate a session at your Processing Community Day. They are designed to be taken "off the shelf," so you do not need to be the expert on the topic to use one.
+
+An Activity Guide may lead a hands-on workshop, creative exercise, discussion, collaborative experiment, or another kind of group or individual activity. Each zine is self-contained and designed for a 1-3 hour session.
+
+Browse the collection and choose a guide that fits your community's interest, the amount of time available, and your participants' experience levels. You can use a guide exactly as written or adapt it for your local context, as permitted by its [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
+
+## Guide Topics
+
+The collection includes Variables, Conditionals, Loops, Functions, Arrays, Objects, Coordinates, Color, Interaction, Animation, and Randomness. Topics without a guide are open for submissions.
diff --git a/pcd-website/src/content/organizer-kit/activity-guides/what-are-activity-guides.md b/pcd-website/src/content/organizer-kit/activity-guides/what-are-activity-guides.md
deleted file mode 100644
index a82200a..0000000
--- a/pcd-website/src/content/organizer-kit/activity-guides/what-are-activity-guides.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-title: What are Activity Guides?
-section: Activity Guides
-order: 1
-description: Community-created zines you can use to facilitate a session at your PCD.
----
-
-Activity Guides are community-created zines that you can use to facilitate a session at your Processing Community Day. They are designed to be taken “off the shelf,” so you do not need to be the expert on the topic to use one.
-
-An Activity Guide may lead a hands-on workshop, creative exercise, discussion, collaborative experiment, or another kind of group or individual activity. Each zine is self-contained and designed for a 1-3 hour session.
-
-*Coming soon, we will have a library of Activity Guides that you can browse, and contribute to. Please check back for updates.*
\ No newline at end of file
diff --git a/pcd-website/src/content/organizer-kit/activity-guides/zine-library.md b/pcd-website/src/content/organizer-kit/activity-guides/zine-library.md
deleted file mode 100644
index 25087c2..0000000
--- a/pcd-website/src/content/organizer-kit/activity-guides/zine-library.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-title: Activity Guide Library
-section: Activity Guides
-order: 3
-description: Community-created zines you can use to facilitate a session at your PCD.
-draft: true
----
-
-TBD
\ No newline at end of file
diff --git a/pcd-website/src/content/organizer-kit/getting-started/minimum-viable-pcd.md b/pcd-website/src/content/organizer-kit/getting-started/minimum-viable-pcd.md
index 1ca0c5d..7d45da6 100644
--- a/pcd-website/src/content/organizer-kit/getting-started/minimum-viable-pcd.md
+++ b/pcd-website/src/content/organizer-kit/getting-started/minimum-viable-pcd.md
@@ -26,4 +26,4 @@ If you're feeling overwhelmed, ask yourself:
* Which spaces are available and how many people will fit there?
* What can I organize with the time and energy I have?
-If you need inspiration for activities to run at your PCD, check out the [Activity Guides Library](/organize/activity-guides/zine-library/) which contains a collection of ready-to-run workshops and activities created by the community. You are also welcome to create your own activity guide and share it with the community. See [Contribute an Activity Guide](/organize/activity-guides/contribute-a-guide/) for more information.
\ No newline at end of file
+If you need inspiration for activities to run at your PCD, check out the [Activity Guides Library](/organize/activity-guides/library/) which contains a collection of ready-to-run workshops and activities created by the community. You are also welcome to create your own activity guide and share it with the community. See [Contribute an Activity Guide](/organize/activity-guides/contribute-a-guide/) for more information.
diff --git a/pcd-website/src/content/zines/README.md b/pcd-website/src/content/zines/README.md
new file mode 100644
index 0000000..f342d76
--- /dev/null
+++ b/pcd-website/src/content/zines/README.md
@@ -0,0 +1,19 @@
+# Activity Guide zines
+
+Each published guide lives in one flat directory:
+
+```
+src/content/zines//
+ metadata.json
+ index.md
+ cover.png
+ guide.pdf
+```
+
+`index.md` contains only `id` in its frontmatter plus the guide's long description. It is deliberately named `index.md`: Astro uses that filename to make the collection entry id equal the folder slug. Event content uses `content.md` because its loader joins through `metadata.id`; zines rely on the entry id, so `content.md` would incorrectly produce `/content`.
+
+`metadata.json` requires these fields: `id` (the folder slug, lowercase kebab-case), `title`, one of the fixed `topic` slots, `created_by`, `summary`, `cover`, a non-empty `pdfs` list (`{ "file", "label" }`), and `license` set to `CC BY-SA 4.0`. Optional fields are `attribution`, `format`, `duration`, `materials`, and an http(s) `source_url`. Covers must have lowercase `.png`, `.jpg`, `.jpeg`, or `.webp` extensions; PDFs must have lowercase `.pdf` extensions.
+
+Only publishable zines belong in `src/content/zines/`. This collection has no `draft` state because its eager asset imports would emit a draft's cover and PDFs to the public build. Keep unfinished work in `src/content/zines-drafts/`, outside the collection and its asset globs.
+
+Before publishing, review PDFs for accessible, selectable (not scanned) text; logical reading order; document title and language; tagged headings where the authoring tool allows; and alt text on images. Include at least one PDF whose reading order follows the content, not only a print-imposed layout.
diff --git a/pcd-website/src/lib/zine-metadata.d.ts b/pcd-website/src/lib/zine-metadata.d.ts
new file mode 100644
index 0000000..fbeca75
--- /dev/null
+++ b/pcd-website/src/lib/zine-metadata.d.ts
@@ -0,0 +1,27 @@
+export declare const ZINE_TOPICS: readonly [
+ 'Variables', 'Conditionals', 'Loops', 'Functions', 'Arrays', 'Objects',
+ 'Coordinates', 'Color', 'Interaction', 'Animation', 'Randomness',
+];
+export type ZineTopic = (typeof ZINE_TOPICS)[number];
+export type ZineLicense = 'CC BY-SA 4.0';
+export declare const LICENSE_URLS: Record;
+
+export interface ZinePdf { file: string; label: string }
+export interface ZineMetadata {
+ id: string; title: string; topic: ZineTopic;
+ created_by: string; attribution?: string;
+ format?: string; duration?: string; materials?: string;
+ summary: string; cover: string; pdfs: ZinePdf[];
+ license: ZineLicense; source_url?: string;
+}
+
+export declare const zineMetadataSchema: import('astro/zod').ZodType;
+export declare function parseZineMetadata(raw: unknown, slug: string): ZineMetadata;
+export declare function assertIdentity(ids: {
+ slug: string; frontmatterId: string; metadataId: string;
+}): void;
+export declare function assertUniqueTopics(zines: ZineMetadata[]): void;
+export declare function assertUniqueIds(zines: ZineMetadata[]): void;
+export declare function resolveZineAssets(
+ slug: string, metadata: ZineMetadata, availableFiles: string[],
+): { cover: string; pdfs: string[] };
diff --git a/pcd-website/src/lib/zine-metadata.js b/pcd-website/src/lib/zine-metadata.js
new file mode 100644
index 0000000..d9e126f
--- /dev/null
+++ b/pcd-website/src/lib/zine-metadata.js
@@ -0,0 +1,98 @@
+import { z } from 'astro/zod';
+
+export const ZINE_TOPICS = [
+ 'Variables', 'Conditionals', 'Loops', 'Functions', 'Arrays', 'Objects',
+ 'Coordinates', 'Color', 'Interaction', 'Animation', 'Randomness',
+];
+
+export const LICENSE_URLS = {
+ 'CC BY-SA 4.0': 'https://creativecommons.org/licenses/by-sa/4.0/',
+};
+
+function isHttpUrl(value) {
+ try {
+ return ['http:', 'https:'].includes(new URL(value).protocol);
+ } catch {
+ return false;
+ }
+}
+
+export const zineMetadataSchema = z.object({
+ id: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'must be lowercase kebab-case'),
+ title: z.string().trim().min(1),
+ topic: z.enum(ZINE_TOPICS),
+ created_by: z.string().trim().min(1),
+ attribution: z.string().trim().min(1).optional(),
+ format: z.string().trim().min(1).optional(),
+ duration: z.string().trim().min(1).optional(),
+ materials: z.string().trim().min(1).optional(),
+ summary: z.string().trim().min(1),
+ cover: z.string().regex(/\.(png|jpg|jpeg|webp)$/, 'cover must be a lowercase .png/.jpg/.jpeg/.webp'),
+ pdfs: z.array(z.object({
+ file: z.string().regex(/\.pdf$/, 'must be a lowercase .pdf'),
+ label: z.string().trim().min(1),
+ }).strict()).min(1, 'at least one PDF is required'),
+ license: z.literal('CC BY-SA 4.0'),
+ source_url: z.string().refine(isHttpUrl, 'must be an http(s) URL').optional(),
+}).strict();
+
+function formatIssues(error) {
+ return error.issues.map((issue) => `${issue.path.join('.') || 'metadata'}: ${issue.message}`).join('; ');
+}
+
+export function parseZineMetadata(raw, slug) {
+ if (raw !== null && typeof raw === 'object' && !Array.isArray(raw) && Object.hasOwn(raw, 'draft')) {
+ throw new Error(
+ `Zine "${slug}" sets \`draft\`, which this collection does not support. ` +
+ 'src/content/zines/ holds only publishable zines — its assets are emitted to dist/ whether or not a page links them. ' +
+ 'Move unfinished work to src/content/zines-drafts/ instead.',
+ );
+ }
+ const result = zineMetadataSchema.safeParse(raw);
+ if (!result.success) {
+ throw new Error(`Invalid metadata for zine "${slug}": ${formatIssues(result.error)}`);
+ }
+ if (result.data.id !== slug) {
+ throw new Error(`Zine "${slug}" has metadata id "${result.data.id}". Set metadata.id to "${slug}".`);
+ }
+ return result.data;
+}
+
+export function assertIdentity({ slug, frontmatterId, metadataId }) {
+ if (slug !== frontmatterId || slug !== metadataId || frontmatterId !== metadataId) {
+ throw new Error(
+ `Zine identity mismatch: folder slug "${slug}", index.md id "${frontmatterId}", metadata id "${metadataId}". ` +
+ 'All three values must match.',
+ );
+ }
+}
+
+export function assertUniqueTopics(zines) {
+ const topics = new Map();
+ for (const zine of zines) {
+ const prior = topics.get(zine.topic);
+ if (prior) throw new Error(`Zines "${prior.id}" and "${zine.id}" both claim the "${zine.topic}" topic. One canonical zine is allowed per topic.`);
+ topics.set(zine.topic, zine);
+ }
+}
+
+export function assertUniqueIds(zines) {
+ const ids = new Set();
+ for (const zine of zines) {
+ if (ids.has(zine.id)) throw new Error(`Duplicate zine id "${zine.id}". Every zine folder must have a unique metadata.id.`);
+ ids.add(zine.id);
+ }
+}
+
+export function resolveZineAssets(slug, metadata, availableFiles) {
+ const available = new Set(availableFiles);
+ const required = [metadata.cover, ...metadata.pdfs.map((pdf) => pdf.file)];
+ const missing = required.filter((file) => !available.has(file));
+ if (missing.length) {
+ throw new Error(
+ `Zine "${slug}" references missing asset(s): ${missing.join(', ')}. ` +
+ `Available files in src/content/zines/${slug}/: ${availableFiles.join(', ') || '(none)'}.`,
+ );
+ }
+ return { cover: metadata.cover, pdfs: metadata.pdfs.map((pdf) => pdf.file) };
+}
diff --git a/pcd-website/src/lib/zines.ts b/pcd-website/src/lib/zines.ts
new file mode 100644
index 0000000..2cd6c82
--- /dev/null
+++ b/pcd-website/src/lib/zines.ts
@@ -0,0 +1,112 @@
+import { getCollection, type CollectionEntry } from 'astro:content';
+import type { ImageMetadata } from 'astro';
+import {
+ assertIdentity, assertUniqueIds, assertUniqueTopics, parseZineMetadata,
+ resolveZineAssets, type ZineLicense, type ZineTopic,
+} from './zine-metadata.js';
+
+interface MetadataModule { default: unknown }
+
+export interface Zine {
+ id: string;
+ title: string;
+ topic: ZineTopic;
+ created_by: string;
+ attribution?: string;
+ format?: string;
+ duration?: string;
+ materials?: string;
+ summary: string;
+ cover: ImageMetadata;
+ pdfs: { url: string; label: string }[];
+ license: ZineLicense;
+ source_url?: string;
+ href: string;
+ entry: CollectionEntry<'zines'>;
+}
+
+function filename(path: string): string {
+ return path.slice(path.lastIndexOf('/') + 1);
+}
+
+function slugFromPath(path: string): string {
+ const match = path.match(/zines\/([^/]+)\//);
+ if (!match) throw new Error(`Could not determine zine folder from asset path "${path}".`);
+ return match[1];
+}
+
+function filesBySlug(modules: Record): Map> {
+ const output = new Map>();
+ for (const [path, value] of Object.entries(modules)) {
+ const slug = slugFromPath(path);
+ const files = output.get(slug) ?? new Map();
+ files.set(filename(path), value);
+ output.set(slug, files);
+ }
+ return output;
+}
+
+export async function loadZines(): Promise {
+ const metadataModules = import.meta.glob('../content/zines/*/metadata.json', { eager: true });
+ // Avoid asking Astro for an empty collection: its loader emits a misleading
+ // "collection does not exist" warning in the intended zero-zine state.
+ const indexFiles = import.meta.glob('../content/zines/*/index.md', { eager: true, query: '?raw', import: 'default' });
+ const covers = import.meta.glob('../content/zines/*/*.{png,jpg,jpeg,webp}', {
+ eager: true, import: 'default',
+ });
+ const pdfs = import.meta.glob('../content/zines/*/*.pdf', {
+ eager: true, import: 'default', query: '?url&no-inline',
+ });
+ if (!Object.keys(metadataModules).length && !Object.keys(indexFiles).length) return [];
+ const entries = await getCollection('zines');
+ const metadataBySlug = filesBySlug(metadataModules);
+ const coversBySlug = filesBySlug(covers);
+ const pdfsBySlug = filesBySlug(pdfs);
+ const entriesBySlug = new Map(entries.map((entry) => [entry.id, entry]));
+
+ for (const slug of metadataBySlug.keys()) {
+ if (!entriesBySlug.has(slug)) {
+ throw new Error(`Zine "${slug}" has metadata.json but no sibling index.md. Add src/content/zines/${slug}/index.md.`);
+ }
+ }
+ for (const slug of entriesBySlug.keys()) {
+ if (!metadataBySlug.has(slug)) {
+ throw new Error(`Zine "${slug}" has index.md but no sibling metadata.json. Add src/content/zines/${slug}/metadata.json.`);
+ }
+ }
+
+ const parsed = [...metadataBySlug.entries()].map(([slug, modules]) => {
+ const metadataModule = modules.get('metadata.json');
+ if (!metadataModule) throw new Error(`Zine "${slug}" is missing metadata.json.`);
+ const metadata = parseZineMetadata(metadataModule.default, slug);
+ const entry = entriesBySlug.get(slug)!;
+ assertIdentity({ slug, frontmatterId: entry.data.id, metadataId: metadata.id });
+ const availableFiles = [...(coversBySlug.get(slug)?.keys() ?? []), ...(pdfsBySlug.get(slug)?.keys() ?? [])];
+ resolveZineAssets(slug, metadata, availableFiles);
+ return { slug, metadata, entry };
+ });
+
+ assertUniqueTopics(parsed.map(({ metadata }) => metadata));
+ assertUniqueIds(parsed.map(({ metadata }) => metadata));
+
+ return parsed.map(({ slug, metadata, entry }) => {
+ const cover = coversBySlug.get(slug)?.get(metadata.cover);
+ if (!cover) throw new Error(`Zine "${slug}" cover "${metadata.cover}" could not be loaded.`);
+ const pdfFiles = pdfsBySlug.get(slug);
+ return {
+ ...metadata,
+ cover,
+ pdfs: metadata.pdfs.map((pdf) => {
+ const url = pdfFiles?.get(pdf.file);
+ if (!url) throw new Error(`Zine "${slug}" PDF "${pdf.file}" could not be loaded.`);
+ return { url, label: pdf.label };
+ }),
+ href: `/activity-guide/${metadata.id}/`,
+ entry,
+ };
+ }).sort((a, b) => a.title.localeCompare(b.title));
+}
+
+export async function loadZinesByTopic(): Promise> {
+ return new Map((await loadZines()).map((zine) => [zine.topic, zine]));
+}
diff --git a/pcd-website/src/pages/activity-guide/[id].astro b/pcd-website/src/pages/activity-guide/[id].astro
new file mode 100644
index 0000000..55344ea
--- /dev/null
+++ b/pcd-website/src/pages/activity-guide/[id].astro
@@ -0,0 +1,77 @@
+---
+import { Image } from 'astro:assets';
+import { render, type GetStaticPaths } from 'astro:content';
+import SiteLayout from '../../layouts/SiteLayout.astro';
+import ExternalLinkIcon from '../../components/ExternalLinkIcon.astro';
+import { LICENSE_URLS } from '../../lib/zine-metadata.js';
+import { loadZines, type Zine } from '../../lib/zines';
+
+export const getStaticPaths: GetStaticPaths = async () => {
+ const zines = await loadZines();
+ return zines.map((zine) => ({ params: { id: zine.id }, props: { zine } }));
+};
+
+const { zine } = Astro.props as { zine: Zine };
+const { Content } = await render(zine.entry);
+---
+
+
+
+ Activity Guide
+ {zine.title}
+
+
+
+
Topic {zine.topic}
+
Created by {zine.created_by}
+ {zine.format &&
Format {zine.format} }
+ {zine.duration &&
Duration {zine.duration} }
+ {zine.materials &&
Materials {zine.materials} }
+
+
+
+
+ Downloads
+
+
+ Licence and attribution
+ This guide is available under the {zine.license} License .
+ {zine.attribution && Preferred attribution: {zine.attribution}
}
+
+ {zine.source_url && (
+
+
+ View the original submission
+
+
+ )}
+
+ Back to the Activity Guide Library
+
+
+
+
diff --git a/pcd-website/src/pages/organize/[...slug].astro b/pcd-website/src/pages/organize/[...slug].astro
index 66ec508..f05cbab 100644
--- a/pcd-website/src/pages/organize/[...slug].astro
+++ b/pcd-website/src/pages/organize/[...slug].astro
@@ -3,6 +3,9 @@ import { getCollection, render } from 'astro:content';
import type { GetStaticPaths } from 'astro';
import DocsLayout from '../../layouts/DocsLayout.astro';
import { flattenKitNav, getKitNav } from '../../config/organizer-kit-nav';
+import { ACTIVITY_GUIDE_SUBMIT_URL } from '../../config';
+import ActivityGuideGrid from '../../components/ActivityGuideGrid.astro';
+import ActivityGuideSubmitButton from '../../components/ActivityGuideSubmitButton.astro';
export const getStaticPaths: GetStaticPaths = async () => {
const entries = await getCollection('organizerKit', (entry) => !entry.data.draft);
@@ -24,7 +27,11 @@ const prev = position > 0 ? ordered[position - 1] : undefined;
const next = position >= 0 && position < ordered.length - 1 ? ordered[position + 1] : undefined;
const editHref = `https://github.com/processing/processing-community-day/edit/main/pcd-website/src/content/organizer-kit/${entry.id}.md`;
-const markdown = `# ${entry.data.title}\n\n${entry.body?.trim() ?? ''}\n`;
+const SUBMIT_PAGES = new Set(['activity-guides/library', 'activity-guides/contribute-a-guide']);
+const markdownSuffix = SUBMIT_PAGES.has(entry.id)
+ ? `\n\n[Submit an activity guide](${ACTIVITY_GUIDE_SUBMIT_URL})\n`
+ : '';
+const markdown = `# ${entry.data.title}\n\n${entry.body?.trim() ?? ''}\n${markdownSuffix}`;
---
+ {entry.id === 'activity-guides/library' && }
+ {entry.id === 'activity-guides/contribute-a-guide' && }
↑ Back to top
diff --git a/pcd-website/src/styles/docs/components.css b/pcd-website/src/styles/docs/components.css
index da63a6e..524d801 100644
--- a/pcd-website/src/styles/docs/components.css
+++ b/pcd-website/src/styles/docs/components.css
@@ -230,3 +230,102 @@
.docs-prose .docs-page-credit a:hover {
text-decoration-color: var(--color-link-underline-hover);
}
+
+.docs-prose .guide-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: var(--sp-4);
+ margin: var(--sp-6) 0;
+ padding: 0;
+ list-style: none;
+}
+.docs-prose .guide-grid > li::before { content: none; }
+.docs-prose .guide-grid > li {
+ display: flex;
+ min-width: 0;
+ margin: 0;
+}
+.docs-prose .guide-grid .guide-card { width: 100%; }
+.guide-card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 5.5rem;
+ overflow: hidden;
+ color: var(--color-text);
+ text-align: center;
+ text-decoration: none;
+ border-radius: var(--border-radius);
+}
+.guide-card--empty {
+ gap: var(--sp-1);
+ padding: var(--sp-4);
+ color: var(--color-text-subtle);
+ border: 1px dashed var(--color-border);
+}
+.guide-card--empty span,
+.guide-card__topic {
+ font-size: var(--step-2);
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+}
+.guide-card--zine {
+ align-items: stretch;
+ border: 1px solid var(--color-border);
+}
+.guide-card--zine:hover {
+ border-color: var(--color-primary);
+ background: var(--docs-sidebar-color);
+}
+.guide-card--zine:focus-visible,
+.guide-card--add:focus-visible {
+ outline: 3px solid var(--color-focus);
+ outline-offset: 3px;
+}
+.guide-card--zine img {
+ display: block;
+ width: 100%;
+ aspect-ratio: 4 / 3;
+ object-fit: cover;
+}
+.guide-card__body {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ align-items: center;
+ gap: var(--sp-1);
+ padding: var(--sp-3);
+}
+.guide-card__summary {
+ color: var(--color-text-muted);
+ font-size: var(--step-2);
+}
+.guide-card--add {
+ flex-direction: row;
+ gap: var(--sp-2);
+ padding: var(--sp-4);
+ color: var(--color-link);
+ font-weight: 600;
+ border: 1px dashed var(--color-border);
+}
+.guide-card--add:hover {
+ border-color: var(--color-primary);
+ background: var(--docs-sidebar-color);
+}
+.guide-card__plus {
+ font-size: var(--step-7);
+ font-weight: 300;
+ line-height: 1;
+}
+.guide-card--add svg { flex: 0 0 auto; }
+.activity-guide-submit { margin-top: var(--sp-5); }
+.activity-guide-submit .btn { display: inline-flex; align-items: center; gap: var(--sp-2); }
+
+@media (min-width: 30rem) {
+ .docs-prose .guide-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+}
+@media (min-width: 66.5rem) {
+ .docs-prose .guide-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
+}
diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh
index a83207d..5631a17 100755
--- a/scripts/run-tests.sh
+++ b/scripts/run-tests.sh
@@ -10,6 +10,7 @@ tests=(
".github/scripts/process-new-event-issue.test.mjs"
".github/scripts/process-edit-event-issue.test.mjs"
".github/scripts/plus-code.test.mjs"
+ ".github/scripts/zines.test.mjs"
)
for test in "${tests[@]}"; do
@@ -17,6 +18,9 @@ for test in "${tests[@]}"; do
node --test "$test"
done
+printf '\n=== .github/scripts/zine-build.test.mjs ===\n'
+node --test ".github/scripts/zine-build.test.mjs"
+
printf '\n=== Build data.json dependencies ===\n'
npm --prefix "${root_dir}/pcd-website" run build
From 7baf28a2b6b6a15af4d386d910c6fa4b7c7fb2b5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20de=20Courville?=
Date: Tue, 25 Aug 2026 13:17:16 +0200
Subject: [PATCH 02/15] Rework Activity Guide organizer pages
Replace the old "Contribute an Activity Guide" page with a new "What are Activity Guides?" page, including overview and draft contribution guidance. Update the Activity Guide Library page to add a dedicated submission section and licensing note so contribution info is surfaced there as well.
---
.../activity-guides/contribute-a-guide.md | 20 -------------
.../organizer-kit/activity-guides/library.md | 8 +++++-
.../activity-guides/what-is-a-guide.md | 28 +++++++++++++++++++
3 files changed, 35 insertions(+), 21 deletions(-)
delete mode 100644 pcd-website/src/content/organizer-kit/activity-guides/contribute-a-guide.md
create mode 100644 pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
diff --git a/pcd-website/src/content/organizer-kit/activity-guides/contribute-a-guide.md b/pcd-website/src/content/organizer-kit/activity-guides/contribute-a-guide.md
deleted file mode 100644
index 99e2702..0000000
--- a/pcd-website/src/content/organizer-kit/activity-guides/contribute-a-guide.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-title: Contribute an Activity Guide
-section: Activity Guides
-order: 2
-description: Instructions for creating and submitting a zine to the PCD Activity Guide Library.
----
-
-Have an activity idea for one of the [Activity Guide Library topics](/organize/activity-guides/library/)? Create a self-contained zine for a 1-3 hour session and share it on the PCD Forum. There is no required visual format. Make it your own, but include enough information for someone unfamiliar with the activity to prepare for and facilitate it successfully. When you're ready, use the submission link below. By submitting to the Activity Guide Library, you agree to publish your original contribution under the [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
-
-## Making a Zine
-
-*Resources*
-*Tools*
-*Examples*
-
-Include a cover image and one or more labelled PDFs. Please supply at least one PDF whose reading order matches the content, rather than only a print-imposed layout. PDFs should use selectable text (not scans), have a logical reading order, a document title and language, tagged headings where the tool allows, and alt text for images.
-
-## Submit Your Guide
-
-The submission link opens a pre-filled forum post for your guide.
diff --git a/pcd-website/src/content/organizer-kit/activity-guides/library.md b/pcd-website/src/content/organizer-kit/activity-guides/library.md
index 1c4bb80..643cbc4 100644
--- a/pcd-website/src/content/organizer-kit/activity-guides/library.md
+++ b/pcd-website/src/content/organizer-kit/activity-guides/library.md
@@ -11,6 +11,12 @@ An Activity Guide may lead a hands-on workshop, creative exercise, discussion, c
Browse the collection and choose a guide that fits your community's interest, the amount of time available, and your participants' experience levels. You can use a guide exactly as written or adapt it for your local context, as permitted by its [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
+## Submit Your Guide
+
+You can also contribute your own Activity Guide to the library. There is no required visual format. Make it your own, but include enough information for someone unfamiliar with the activity to prepare for and facilitate it successfully.
+
+By submitting to the Activity Guide Library, you agree to publish your original contribution under the [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
+
## Guide Topics
-The collection includes Variables, Conditionals, Loops, Functions, Arrays, Objects, Coordinates, Color, Interaction, Animation, and Randomness. Topics without a guide are open for submissions.
+The collection includes Variables, Conditionals, Loops, Functions, Arrays, Objects, Coordinates, Color, Interaction, Animation, and Randomness. Topics without a guide are open for submissions.
\ No newline at end of file
diff --git a/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md b/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
new file mode 100644
index 0000000..71fe31f
--- /dev/null
+++ b/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
@@ -0,0 +1,28 @@
+---
+title: What are Activity Guides?
+section: Activity Guides
+order: 2
+description: Instructions for creating and submitting a zine to the PCD Activity Guide Library.
+---
+
+Activity Guides are community-created zines that you can use to facilitate a session at your Processing Community Day. They are designed to be taken “off the shelf,” so you do not need to be the expert on the topic to use one.
+
+An Activity Guide may lead a hands-on workshop, creative exercise, discussion, collaborative experiment, or another kind of group or individual activity. Each zine is self-contained and designed for a 1-3 hour session.
+
+Coming soon, we will have a library of Activity Guides that you can browse, and contribute to. Please check back for updates.
+
+There is no required visual format. Make it your own, but include enough information for someone unfamiliar with the activity to prepare for and facilitate it successfully.
+
+When you're ready, use the submission link below. By submitting to the Activity Guide Library, you agree to publish your original contribution under the [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
+
+## Making a Zine
+
+TBD
+
+*Resources*
+*Tools*
+*Examples*
+
+## Submit Your Guide
+
+The submission link opens a pre-filled forum post for your guide.
From 45281680f502dea363856af7ff2238ef948ae6ed Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20de=20Courville?=
Date: Tue, 25 Aug 2026 15:16:08 +0200
Subject: [PATCH 03/15] Reorganize activity guide docs with zine resources
---
.../organizer-kit/activity-guides/library.md | 16 +------
.../activity-guides/what-is-a-guide.md | 47 ++++++++++++++-----
2 files changed, 37 insertions(+), 26 deletions(-)
diff --git a/pcd-website/src/content/organizer-kit/activity-guides/library.md b/pcd-website/src/content/organizer-kit/activity-guides/library.md
index 643cbc4..73178a8 100644
--- a/pcd-website/src/content/organizer-kit/activity-guides/library.md
+++ b/pcd-website/src/content/organizer-kit/activity-guides/library.md
@@ -5,18 +5,4 @@ order: 1
description: Community-created zines you can use to facilitate a session at your PCD.
---
-Activity Guides are community-created zines that you can use to facilitate a session at your Processing Community Day. They are designed to be taken "off the shelf," so you do not need to be the expert on the topic to use one.
-
-An Activity Guide may lead a hands-on workshop, creative exercise, discussion, collaborative experiment, or another kind of group or individual activity. Each zine is self-contained and designed for a 1-3 hour session.
-
-Browse the collection and choose a guide that fits your community's interest, the amount of time available, and your participants' experience levels. You can use a guide exactly as written or adapt it for your local context, as permitted by its [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
-
-## Submit Your Guide
-
-You can also contribute your own Activity Guide to the library. There is no required visual format. Make it your own, but include enough information for someone unfamiliar with the activity to prepare for and facilitate it successfully.
-
-By submitting to the Activity Guide Library, you agree to publish your original contribution under the [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
-
-## Guide Topics
-
-The collection includes Variables, Conditionals, Loops, Functions, Arrays, Objects, Coordinates, Color, Interaction, Animation, and Randomness. Topics without a guide are open for submissions.
\ No newline at end of file
+Browse the collection and choose a guide that fits your needs. You can use a guide as is or adapt it as needed.
diff --git a/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md b/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
index 71fe31f..73d8c86 100644
--- a/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
+++ b/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
@@ -5,24 +5,49 @@ order: 2
description: Instructions for creating and submitting a zine to the PCD Activity Guide Library.
---
-Activity Guides are community-created zines that you can use to facilitate a session at your Processing Community Day. They are designed to be taken “off the shelf,” so you do not need to be the expert on the topic to use one.
+Activity Guides are community-created zines (see ["What is a Zine?"](#what-is-a-zine)) that you can use to facilitate a session at your Processing Community Day. They are designed to be taken “off the shelf,” so you do not need to be an expert on the topic to use one.
-An Activity Guide may lead a hands-on workshop, creative exercise, discussion, collaborative experiment, or another kind of group or individual activity. Each zine is self-contained and designed for a 1-3 hour session.
+## How to Design an Activity Guide
-Coming soon, we will have a library of Activity Guides that you can browse, and contribute to. Please check back for updates.
+An Activity Guide is a practical, self-contained resource that helps someone facilitate an activity during PCD without too much preparation.
-There is no required visual format. Make it your own, but include enough information for someone unfamiliar with the activity to prepare for and facilitate it successfully.
+An Activity Guide can be used for a hands-on workshop, creative exercise, discussion, collaborative experiment, or another group or individual activity. Each guide should be designed for a session lasting between one and three hours.
-When you're ready, use the submission link below. By submitting to the Activity Guide Library, you agree to publish your original contribution under the [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
+Your guide should include:
-## Making a Zine
+- the purpose of the activity (what participants will learn or experience)
+- who the activity is for (for example, beginners, intermediate, or advanced participants)
+- the recommended duration. (1-3 hours)
+- any tools, materials, or preparation needed
+- clear, step-by-step instructions (for a tutorial, workshop, or exercise)
+- prompts or questions for participants (for a discussion or collaborative activity)
-TBD
+There is no required visual format. Make it your own, but include enough information for someone unfamiliar with the activity or subject to prepare for and facilitate it successfully.
-*Resources*
-*Tools*
-*Examples*
+## What is a Zine?
+
+A [zine](https://en.wikipedia.org/wiki/Zine) (pronounced “zeen,” short for magazine or fanzine) is a small, independent publication usually created by one person or a small group. It usually explores a specific or unconventional subject and is often made by hand, photocopied, or published online. Usually, a zine is a small booklet or pamphlet, often with a limited number of pages. Many zines can be printed and folded from a single sheet of paper.
+
+## Zine making resources
+
+This [wikiHow article](https://www.wikihow.com/Make-a-Zine) is an illustrated step-by-step guide to making a zine. It's a great starting point for creating your first zine.
+
+This YouTube video, [How to Make a Zine](https://www.youtube.com/watch?v=ab4O9SWNl9g) by Austin Kleon, is a short and fun introduction to zine-making.
+
+The US Library of Congress has a [Zine Making Guide (PDF)](https://guides.loc.gov/ld.php?content_id=67687837) and a [list of zine making resources](https://guides.loc.gov/zines/external-websites).
+
+### Tools
+
+- [p5.(gen)zine](https://github.com/munusshih/p5.genzine) by Munus Shih and Iley Cao is an open-sourced and friendly p5.js library for zine-making.
+
+- [The Electric Zine Maker](https://alienmelon.itch.io/electric-zine-maker) by alienmelon is a printshop and art tool for easily making and printing zines.
+
+- [Zine Arranger](https://nashhigh.itch.io/zinearranger) by Nash Hight, arranges multi-page PDF files into a printable zine layout.
+
+### Examples
+
+- [Tiny Tech Zines](https://tinytechzines.bigcartel.com/)
## Submit Your Guide
-The submission link opens a pre-filled forum post for your guide.
+Go to the [Activity Guide Library](/organize/activity-guides/library/) to submit your zine and explore the full collection. By submitting an Activity Guide, you agree to publish your original contribution under the [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
\ No newline at end of file
From d9087cb5ad83897cffeb49c53d07a7a2a31ce123 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20de=20Courville?=
Date: Tue, 25 Aug 2026 15:58:19 +0200
Subject: [PATCH 04/15] Update what-is-a-guide.md
---
.../activity-guides/what-is-a-guide.md | 32 ++++++++++++-------
1 file changed, 20 insertions(+), 12 deletions(-)
diff --git a/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md b/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
index 73d8c86..0186676 100644
--- a/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
+++ b/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
@@ -2,27 +2,35 @@
title: What are Activity Guides?
section: Activity Guides
order: 2
-description: Instructions for creating and submitting a zine to the PCD Activity Guide Library.
+description: Instructions for creating and submitting an activity guide to the PCD Activity Guide Library.
---
-Activity Guides are community-created zines (see ["What is a Zine?"](#what-is-a-zine)) that you can use to facilitate a session at your Processing Community Day. They are designed to be taken “off the shelf,” so you do not need to be an expert on the topic to use one.
+Activity Guides are community-created zines (see ["What is a Zine?"](#what-is-a-zine)) that you can use to facilitate or take part in an activity at your Processing Community Day. They are designed to be taken “off the shelf,” so you do not need to be an expert on the topic to use one.
-## How to Design an Activity Guide
+## How to Create an Activity Guide
-An Activity Guide is a practical, self-contained resource that helps someone facilitate an activity during PCD without too much preparation.
+An Activity Guide should be practical, self-contained, and easy to follow. It might contain instructions for a hands-on workshop, creative exercise, discussion, collaborative experiment, or another kind of group or individual activity.
-An Activity Guide can be used for a hands-on workshop, creative exercise, discussion, collaborative experiment, or another group or individual activity. Each guide should be designed for a session lasting between one and three hours.
+Each guide should be designed for a session lasting between one and three hours. The goal is to make it easy for someone to pick up the guide and use it successfully, either as a facilitator or as a participant.
Your guide should include:
-- the purpose of the activity (what participants will learn or experience)
-- who the activity is for (for example, beginners, intermediate, or advanced participants)
-- the recommended duration. (1-3 hours)
+- the purpose of the activity and what participants will learn or experience
+- who the activity is for (beginners, intermediate, or advanced participants)
+- the recommended duration (between one and three hours)
- any tools, materials, or preparation needed
-- clear, step-by-step instructions (for a tutorial, workshop, or exercise)
-- prompts or questions for participants (for a discussion or collaborative activity)
+- additional resources for further exploration
-There is no required visual format. Make it your own, but include enough information for someone unfamiliar with the activity or subject to prepare for and facilitate it successfully.
+For a tutorial-style activity, you may also want to include:
+- step-by-step instructions
+- example code or templates (can be links too)
+- suggested exercises or challenges
+
+For a discussion or collaborative activity, you may also want to include:
+- a list of discussion questions or prompts
+- suggested group exercises or collaborative tasks
+
+There is no required visual format. Make it your own, but include enough information for someone unfamiliar with the activity or subject to facilitate it or complete it independently.
## What is a Zine?
@@ -48,6 +56,6 @@ The US Library of Congress has a [Zine Making Guide (PDF)](https://guides.loc.go
- [Tiny Tech Zines](https://tinytechzines.bigcartel.com/)
-## Submit Your Guide
+## Submit Your Zine
Go to the [Activity Guide Library](/organize/activity-guides/library/) to submit your zine and explore the full collection. By submitting an Activity Guide, you agree to publish your original contribution under the [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
\ No newline at end of file
From 64b2d00e0043c44961cbb6640723059ebb06a15a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20de=20Courville?=
Date: Tue, 25 Aug 2026 16:49:53 +0200
Subject: [PATCH 05/15] Update link template for zine submission
---
pcd-website/src/config.ts | 26 ++++++++++++++++----------
1 file changed, 16 insertions(+), 10 deletions(-)
diff --git a/pcd-website/src/config.ts b/pcd-website/src/config.ts
index e17f1ce..13c8815 100644
--- a/pcd-website/src/config.ts
+++ b/pcd-website/src/config.ts
@@ -16,22 +16,28 @@ export const PCD_FORUM_NEW_TOPIC_URL =
tags: "pcd",
}).toString();
-export const ACTIVITY_GUIDE_SUBMISSION_TEMPLATE = `**Activity Title:**
-**Created by:**
+export const ACTIVITY_GUIDE_SUBMISSION_TEMPLATE = `*This post uses the submission template for the Processing Community Day [Activity Guide Library](https://day.processing.org/organize/activity-guides/library/), a collection of activities for PCD events.*
-**Activity Format:** [workshop, discussion, creative exercise, group project, etc]
+---
-**Topic:** [what does the activity explore?]
-**About the Activity:** [In 1-2 sentences, explain what participants will do and why a PCD organizer might choose this activity]
+**Title:**
+**Author(s):**
-**Duration:** [1 hour / 2 hours / 3 hours]
-**Tools or Materials needed:**
+**Activity Format:**
+**About the Activity:**
-**Link to Activity:**
+**Number of Pages:**
+**Duration:**
+
+
+**PDF (individual pages):**
+**PDF (printable version):**
+
+**License:** I confirm that I own or have permission to license this material, and I agree to publish it under the [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
+
+**Tags:** `
-**License:** I confirm that I own or have permission to license this material, and I agree to publish my original contribution under the [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/). I have identified any third-party material that is not covered by this license.
-**Preferred Attribution:**`;
export const ACTIVITY_GUIDE_SUBMIT_URL =
"https://discourse.processing.org/new-topic?" +
From 339cc50cd449cdfed5ee796cb6f632d042d54d77 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20de=20Courville?=
Date: Tue, 25 Aug 2026 19:09:12 +0200
Subject: [PATCH 06/15] Refactor zine library to ordered card model
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Reworks Activity Guide zines to load from file-backed cards sorted by required `order` frontmatter, with explicit placeholder entries (`placeholder: true`) that render “Guide wanted” cards but do not generate detail pages. Published zines now support optional covers, optional license blocks, local or external PDF downloads, and required human-readable `file_size` metadata.
Introduces reusable `BackButton` and `ZineDownloads` components, updates grid/detail rendering and styles for cover fallbacks, adds new zine/placeholder content entries (including Zine Making Kit), and updates schema/validation logic by removing fixed topic-slot uniqueness in favor of id/identity checks. Tests and docs were expanded to cover ordering, placeholder behavior, external downloads, and emitted asset expectations.
---
.../zine-integration-test-fixture/index.md | 1 +
.../metadata.json | 4 +-
.github/scripts/zine-build.test.mjs | 36 +++++--
.github/scripts/zines.test.mjs | 40 +++++---
AGENTS.md | 4 +-
TEST.md | 4 +-
.../src/components/ActivityGuideGrid.astro | 33 ++++---
pcd-website/src/components/BackButton.astro | 30 ++++++
.../src/components/ZineDownloads.astro | 94 +++++++++++++++++++
pcd-website/src/content.config.ts | 10 +-
pcd-website/src/content/zines/README.md | 14 +--
.../src/content/zines/animation/index.md | 6 ++
pcd-website/src/content/zines/arrays/index.md | 6 ++
pcd-website/src/content/zines/color/index.md | 6 ++
.../src/content/zines/conditionals/index.md | 6 ++
.../src/content/zines/coordinates/index.md | 6 ++
.../src/content/zines/functions/index.md | 6 ++
.../src/content/zines/interaction/index.md | 6 ++
pcd-website/src/content/zines/loops/index.md | 6 ++
.../src/content/zines/objects/index.md | 6 ++
.../src/content/zines/randomness/index.md | 6 ++
.../src/content/zines/variables/index.md | 6 ++
.../content/zines/zine-making-kit/index.md | 6 ++
.../zines/zine-making-kit/metadata.json | 16 ++++
pcd-website/src/lib/zine-metadata.d.ts | 18 ++--
pcd-website/src/lib/zine-metadata.js | 42 ++++-----
pcd-website/src/lib/zines.ts | 71 +++++++++++---
.../src/pages/activity-guide/[id].astro | 37 ++++++--
pcd-website/src/styles/docs/components.css | 14 ++-
29 files changed, 431 insertions(+), 109 deletions(-)
create mode 100644 pcd-website/src/components/BackButton.astro
create mode 100644 pcd-website/src/components/ZineDownloads.astro
create mode 100644 pcd-website/src/content/zines/animation/index.md
create mode 100644 pcd-website/src/content/zines/arrays/index.md
create mode 100644 pcd-website/src/content/zines/color/index.md
create mode 100644 pcd-website/src/content/zines/conditionals/index.md
create mode 100644 pcd-website/src/content/zines/coordinates/index.md
create mode 100644 pcd-website/src/content/zines/functions/index.md
create mode 100644 pcd-website/src/content/zines/interaction/index.md
create mode 100644 pcd-website/src/content/zines/loops/index.md
create mode 100644 pcd-website/src/content/zines/objects/index.md
create mode 100644 pcd-website/src/content/zines/randomness/index.md
create mode 100644 pcd-website/src/content/zines/variables/index.md
create mode 100644 pcd-website/src/content/zines/zine-making-kit/index.md
create mode 100644 pcd-website/src/content/zines/zine-making-kit/metadata.json
diff --git a/.github/scripts/fixtures/zines/zine-integration-test-fixture/index.md b/.github/scripts/fixtures/zines/zine-integration-test-fixture/index.md
index 6929c18..3f893e5 100644
--- a/.github/scripts/fixtures/zines/zine-integration-test-fixture/index.md
+++ b/.github/scripts/fixtures/zines/zine-integration-test-fixture/index.md
@@ -1,5 +1,6 @@
---
id: zine-integration-test-fixture
+order: 13
---
Use a loop to draw a playful field of shapes. Try changing the colours, sizes, and spacing together.
diff --git a/.github/scripts/fixtures/zines/zine-integration-test-fixture/metadata.json b/.github/scripts/fixtures/zines/zine-integration-test-fixture/metadata.json
index bb8e61a..9f92906 100644
--- a/.github/scripts/fixtures/zines/zine-integration-test-fixture/metadata.json
+++ b/.github/scripts/fixtures/zines/zine-integration-test-fixture/metadata.json
@@ -10,8 +10,8 @@
"summary": "A compact guide to making patterns with repeated shapes.",
"cover": "cover.png",
"pdfs": [
- { "file": "guide-small.pdf", "label": "Read on screen" },
- { "file": "guide-print.pdf", "label": "Print and fold" }
+ { "file": "guide-small.pdf", "label": "Read on screen", "file_size": "96 B" },
+ { "file": "guide-print.pdf", "label": "Print and fold", "file_size": "96 B" }
],
"license": "CC BY-SA 4.0",
"source_url": "https://discourse.processing.org/t/activity-guide-integration-test/1"
diff --git a/.github/scripts/zine-build.test.mjs b/.github/scripts/zine-build.test.mjs
index 75d3e72..efbd93c 100644
--- a/.github/scripts/zine-build.test.mjs
+++ b/.github/scripts/zine-build.test.mjs
@@ -17,13 +17,13 @@ function emittedPath(href) {
return join(DIST, new URL(href, 'https://day.processing.org').pathname.replace(/^\//, ''));
}
-function hrefForLabel(html, label) {
- const match = html.match(new RegExp(`]+href="([^"]+)"[^>]*>${label} `));
- assert.ok(match, `expected a link labelled "${label}"`);
+function hrefForFilename(html, filename) {
+ const match = html.match(new RegExp(`]+href="([^"]+)"[^>]+download="${filename}"`));
+ assert.ok(match, `expected a download for "${filename}"`);
return match[1];
}
-test('a populated zine collection emits linked assets and replaces its topic placeholder', () => {
+test('a populated zine collection emits linked assets and renders entries in frontmatter order', () => {
assert.ok(!existsSync(DEST), `${DEST} already exists — refusing to overwrite`);
let created = true;
try {
@@ -37,20 +37,40 @@ test('a populated zine collection emits linked assets and replaces its topic pla
assert.match(page, /Loops with Shapes/);
assert.match(page, /View the original submission/);
- for (const label of ['Read on screen', 'Print and fold']) {
- assert.ok(existsSync(emittedPath(hrefForLabel(page, label))), `${label} should resolve to an emitted PDF`);
+ for (const filename of ['guide-small.pdf', 'guide-print.pdf']) {
+ assert.ok(existsSync(emittedPath(hrefForFilename(page, filename))), `${filename} should resolve to an emitted PDF`);
}
+ assert.match(page, /download-list__size[^>]*>96 B);
const pageCover = page.match(/ ]+src="([^"]+)"/);
assert.ok(pageCover, 'the zine page should render a cover image');
assert.ok(existsSync(emittedPath(pageCover[1])), 'the zine cover should be emitted');
+ const noCoverPage = readFileSync(join(DIST, 'activity-guide/zine-making-kit/index.html'), 'utf8');
+ assert.match(noCoverPage, /activity-guide__cover--placeholder[^>]*>Zine Making Kit);
+ assert.equal(
+ hrefForFilename(noCoverPage, 'B230_zinemakingactivity.pdf'),
+ 'https://guides.loc.gov/ld.php?content_id=67687837',
+ );
+ assert.match(noCoverPage, /download-list__size[^>]*>519 kB);
+
const library = readFileSync(join(DIST, 'organize/activity-guides/library/index.html'), 'utf8');
+ assert.match(library, /\s*\s*]*>Zine Making Kit);
assert.match(library, new RegExp(`href="/activity-guide/${SLUG}/"`));
assert.match(library, /A compact guide to making patterns with repeated shapes\./);
- assert.doesNotMatch(library, /Loops<\/strong>\s*Guide wanted<\/span>/);
+ assert.ok(
+ library.indexOf('/activity-guide/zine-making-kit/') < library.indexOf('Variables '),
+ 'order 1 should render before order 2',
+ );
+ assert.ok(
+ library.indexOf('Randomness ') < library.indexOf(`/activity-guide/${SLUG}/`),
+ 'order 12 should render before order 13',
+ );
+ assert.equal((library.match(/Guide wanted/g) ?? []).length, 11, 'each placeholder file should render a wanted card');
+ assert.equal(existsSync(join(DIST, 'activity-guide/variables/index.html')), false, 'placeholders should not get detail pages');
const grid = library.match(/([\s\S]*?)<\/ul>/);
assert.ok(grid, 'the library should render its grid');
- assert.equal((grid[1].match(//g) ?? []).length, 12, 'the grid should always have eleven topics plus submission');
+ assert.equal((grid[1].match(/ /g) ?? []).length, 14, 'the grid should contain all file-backed entries plus submission');
const gridCover = grid[1].match(/ ]+src="([^"]+)"/);
assert.ok(gridCover, 'the zine card should render a cover image');
assert.ok(existsSync(emittedPath(gridCover[1])), 'the card cover should be emitted');
diff --git a/.github/scripts/zines.test.mjs b/.github/scripts/zines.test.mjs
index e5dcf79..db83c82 100644
--- a/.github/scripts/zines.test.mjs
+++ b/.github/scripts/zines.test.mjs
@@ -1,20 +1,30 @@
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import {
- assertIdentity, assertUniqueIds, assertUniqueTopics, parseZineMetadata,
+ assertIdentity, assertUniqueIds, parseZineMetadata,
resolveZineAssets, zineMetadataSchema,
} from '../../pcd-website/src/lib/zine-metadata.js';
const valid = () => ({
id: 'loops-with-shapes', title: 'Loops with Shapes', topic: 'Loops',
created_by: 'Guide Author', summary: 'Make patterns with repeated shapes.',
- cover: 'cover.png', pdfs: [{ file: 'guide.pdf', label: 'Read on screen' }],
+ cover: 'cover.png', pdfs: [{ file: 'guide.pdf', label: 'Read on screen', file_size: '24 kB' }],
license: 'CC BY-SA 4.0',
});
describe('zine metadata', () => {
test('accepts valid metadata', () => {
assert.deepEqual(parseZineMetadata(valid(), 'loops-with-shapes'), valid());
+ const withoutCover = { ...valid(), cover: undefined };
+ assert.deepEqual(parseZineMetadata(withoutCover, 'loops-with-shapes'), withoutCover);
+ const externalPdf = {
+ ...valid(), cover: undefined, license: undefined,
+ pdfs: [{
+ url: 'https://example.com/guide.pdf', label: 'Download guide',
+ filename: 'guide.pdf', file_size: '24 kB',
+ }],
+ };
+ assert.deepEqual(parseZineMetadata(externalPdf, 'loops-with-shapes'), externalPdf);
});
test('rejects non-object metadata cleanly', () => {
@@ -25,17 +35,20 @@ describe('zine metadata', () => {
test('validates required values, topics, ids, and strict object keys', () => {
const cases = [
- [{ ...valid(), topic: 'Physics' }, /topic/],
+ [{ ...valid(), topic: ' ' }, /topic/],
[{ ...valid(), title: ' ' }, /title/],
[{ ...valid(), id: 'Not Kebab' }, /id/],
[{ ...valid(), unexpected: true }, /Unrecognized key/],
- [{ ...valid(), cover: undefined }, /cover/],
[{ ...valid(), cover: 'cover.PNG' }, /cover/],
[{ ...valid(), pdfs: [] }, /pdfs/],
- [{ ...valid(), pdfs: [{ file: 'guide.PDF', label: 'PDF' }] }, /pdfs/],
- [{ ...valid(), pdfs: [{ file: 'guide.txt', label: 'Text' }] }, /pdfs/],
- [{ ...valid(), pdfs: [{ file: 'guide.pdf' }] }, /pdfs/],
- [{ ...valid(), pdfs: [{ file: 'guide.pdf', label: 'PDF', extra: true }] }, /Unrecognized key/],
+ [{ ...valid(), pdfs: [{ file: 'guide.PDF', label: 'PDF', file_size: '24 kB' }] }, /pdfs/],
+ [{ ...valid(), pdfs: [{ file: 'guide.txt', label: 'Text', file_size: '24 kB' }] }, /pdfs/],
+ [{ ...valid(), pdfs: [{ file: 'guide.pdf', file_size: '24 kB' }] }, /pdfs/],
+ [{ ...valid(), pdfs: [{ file: 'guide.pdf', label: 'PDF' }] }, /pdfs/],
+ [{ ...valid(), pdfs: [{ file: 'guide.pdf', label: 'PDF', file_size: '24 kB', extra: true }] }, /Unrecognized key/],
+ [{ ...valid(), pdfs: [{
+ url: 'javascript:alert(1)', label: 'PDF', filename: 'guide.pdf', file_size: '24 kB',
+ }] }, /pdfs/],
[{ ...valid(), license: 'CC BY 4.0' }, /license/],
];
for (const [input, message] of cases) {
@@ -61,14 +74,19 @@ describe('zine metadata', () => {
assert.deepEqual(resolveZineAssets('loops-with-shapes', valid(), ['cover.png', 'guide.pdf']), {
cover: 'cover.png', pdfs: ['guide.pdf'],
});
+ assert.deepEqual(resolveZineAssets('loops-with-shapes', {
+ ...valid(), cover: undefined,
+ pdfs: [{
+ url: 'https://example.com/guide.pdf', label: 'Download guide',
+ filename: 'guide.pdf', file_size: '24 kB',
+ }],
+ }, []), { cover: undefined, pdfs: [] });
assert.throws(() => resolveZineAssets('loops-with-shapes', valid(), ['cover.png']), /guide.pdf/);
assert.throws(() => resolveZineAssets('loops-with-shapes', valid(), ['guide.pdf']), /cover.png/);
});
- test('checks unique topics, unique ids, and three-way identity', () => {
+ test('checks unique ids and three-way identity', () => {
const first = valid();
- const sameTopic = { ...valid(), id: 'other-loops' };
- assert.throws(() => assertUniqueTopics([first, sameTopic]), /both claim/);
assert.throws(() => assertUniqueIds([first, { ...valid() }]), /Duplicate zine id/);
assert.throws(() => assertIdentity({ slug: 'loops-with-shapes', frontmatterId: 'loops', metadataId: 'loops-with-shapes' }), /identity mismatch/);
assert.doesNotThrow(() => assertIdentity({ slug: 'loops-with-shapes', frontmatterId: 'loops-with-shapes', metadataId: 'loops-with-shapes' }));
diff --git a/AGENTS.md b/AGENTS.md
index 4930583..e8b9d87 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -68,7 +68,7 @@ Event data lives in `src/content/events//`:
`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 ``.
-Activity Guide zines live in `src/content/zines//`, with `metadata.json`, `index.md`, a cover image, and one or more PDFs together in the same folder. `src/lib/zines.ts` joins the Astro collection, metadata, and assets at build time; `src/lib/zine-metadata.js` owns the strict schema and pure validation. Unlike events, zines must use `index.md` (not `content.md`) so Astro's glob loader makes the entry id equal to the folder slug. A zine may claim only one of the fixed topic slots, and no two zines may claim the same topic; violations fail the build.
+Activity Guide cards live in `src/content/zines//` and the library grid is built dynamically from every `*/index.md`, sorted by its required numeric `order` frontmatter. Published zines set `placeholder: false` implicitly and pair `index.md` with `metadata.json`, an optional cover image, and one or more PDF downloads. Downloads may be local sibling PDF assets or external http(s) URLs; metadata supplies the human-readable file size (and the filename for external files) used by the shared download rows. Placeholder topics contain only `index.md` with `title`, `order`, and `placeholder: true`; they render “Guide wanted” cards and do not generate detail pages. `src/lib/zines.ts` joins the Astro collection, metadata, and assets at build time; `src/lib/zine-metadata.js` owns the strict published-zine schema and pure validation. Zines must use `index.md` (not `content.md`) so Astro's glob loader makes the entry id equal to the folder slug. A published zine without a cover renders a grey title fallback in the library and on its detail page.
Zine PDFs are emitted from `src/` assets using `?url&no-inline`, so even small downloads become real files in `dist/`. `src/content/zines/` contains publishable zines only: it deliberately has no `draft` field because eager asset imports would make draft files public. Keep unfinished zines in `src/content/zines-drafts/`. Review PDFs for selectable text, logical reading order, document title and language, tagged headings where possible, alt text, and at least one screen-reader-friendly reading-order version.
@@ -93,7 +93,9 @@ The global Markdown pipeline runs `rehype-table-wrapper` and `rehype-heading-anc
| `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/BackButton.astro` | Reusable button-style link for navigating from a detail page back to its parent listing |
| `src/components/CopyMarkdownButton.astro` | Copies an Organizer Kit page as Markdown with accessible success/error feedback |
+| `src/components/ZineDownloads.astro` | Renders zine download rows with a button, filename, and human-readable file size |
| `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 |
diff --git a/TEST.md b/TEST.md
index b7d5fe9..7ebd486 100644
--- a/TEST.md
+++ b/TEST.md
@@ -82,8 +82,8 @@ These tests cover the shared pure functions extracted into `event-issue-helpers.
| Suite | Cases |
|---|---|
-| `zines.test.mjs` | Schema, URL safety, draft rejection, identity, topic/id uniqueness, and asset validation |
-| `zine-build.test.mjs` | Populated collection routes, labelled PDF downloads emitted as files, cover emission, source link, topic replacement, and stable 12-card grid |
+| `zines.test.mjs` | Schema, URL safety, draft rejection, identity/id uniqueness, optional covers, external downloads, and asset validation |
+| `zine-build.test.mjs` | Published and placeholder cards loaded from files, frontmatter ordering, placeholder route exclusion, download metadata, cover/fallback rendering, emitted local assets, and source links |
---
diff --git a/pcd-website/src/components/ActivityGuideGrid.astro b/pcd-website/src/components/ActivityGuideGrid.astro
index bafd673..025b306 100644
--- a/pcd-website/src/components/ActivityGuideGrid.astro
+++ b/pcd-website/src/components/ActivityGuideGrid.astro
@@ -1,33 +1,32 @@
---
import { Image } from 'astro:assets';
import { ACTIVITY_GUIDE_SUBMIT_URL } from '../config';
-import { ZINE_TOPICS } from '../lib/zine-metadata.js';
-import { loadZinesByTopic } from '../lib/zines';
+import { loadZineCards } from '../lib/zines';
import ExternalLinkIcon from './ExternalLinkIcon.astro';
-const zinesByTopic = await loadZinesByTopic();
+const cards = await loadZineCards();
---
- {ZINE_TOPICS.map((topic) => {
- const zine = zinesByTopic.get(topic);
- return zine ? (
+ {cards.map((card) => {
+ return card.placeholder ? (
-
-
-
- {zine.title}
- {topic}
- {zine.summary}
-
-
+
+ {card.title}
+ Guide wanted
+
) : (
-
- {topic}
- Guide wanted
+
+ {card.cover
+ ?
+ : {card.title} }
+
+ {card.title}
+ {card.summary}
+
);
})}
diff --git a/pcd-website/src/components/BackButton.astro b/pcd-website/src/components/BackButton.astro
new file mode 100644
index 0000000..ae71ac0
--- /dev/null
+++ b/pcd-website/src/components/BackButton.astro
@@ -0,0 +1,30 @@
+---
+interface Props {
+ href: string;
+ label?: string;
+}
+
+const { href, label = 'Back' } = Astro.props;
+---
+
+
+ <
+ {label}
+
+
+
diff --git a/pcd-website/src/components/ZineDownloads.astro b/pcd-website/src/components/ZineDownloads.astro
new file mode 100644
index 0000000..9e437a3
--- /dev/null
+++ b/pcd-website/src/components/ZineDownloads.astro
@@ -0,0 +1,94 @@
+---
+interface Download {
+ url: string;
+ label: string;
+ filename: string;
+ fileSize: string;
+}
+
+interface Props {
+ downloads: Download[];
+}
+
+const { downloads } = Astro.props;
+---
+
+
+
+ {downloads.map((download) => (
+
+ Download
+
+ {download.filename}
+ {download.fileSize}
+
+
+ ))}
+
+
+
+
diff --git a/pcd-website/src/content.config.ts b/pcd-website/src/content.config.ts
index 270bf91..0f32d9a 100644
--- a/pcd-website/src/content.config.ts
+++ b/pcd-website/src/content.config.ts
@@ -40,7 +40,15 @@ const zines = defineCollection({
// One flat folder per zine. `index.md` makes the collection entry id the
// folder slug; `content.md` would instead produce `/content`.
loader: glob({ base: './src/content/zines', pattern: '*/index.md' }),
- schema: z.object({ id: z.string() }),
+ schema: z.object({
+ id: z.string(),
+ order: z.number().int().nonnegative(),
+ placeholder: z.boolean().default(false),
+ title: z.string().trim().min(1).optional(),
+ }).refine((data) => !data.placeholder || Boolean(data.title), {
+ message: 'Placeholder zines require a title',
+ path: ['title'],
+ }),
});
export const collections = {
diff --git a/pcd-website/src/content/zines/README.md b/pcd-website/src/content/zines/README.md
index f342d76..4ffce2b 100644
--- a/pcd-website/src/content/zines/README.md
+++ b/pcd-website/src/content/zines/README.md
@@ -1,19 +1,21 @@
# Activity Guide zines
-Each published guide lives in one flat directory:
+Each library card lives in one flat directory. A published guide uses:
```
src/content/zines//
metadata.json
index.md
- cover.png
- guide.pdf
+ cover.png # optional
+ guide.pdf # optional when metadata uses an external PDF URL
```
-`index.md` contains only `id` in its frontmatter plus the guide's long description. It is deliberately named `index.md`: Astro uses that filename to make the collection entry id equal the folder slug. Event content uses `content.md` because its loader joins through `metadata.id`; zines rely on the entry id, so `content.md` would incorrectly produce `/content`.
+A topic without a guide uses only `index.md`.
-`metadata.json` requires these fields: `id` (the folder slug, lowercase kebab-case), `title`, one of the fixed `topic` slots, `created_by`, `summary`, `cover`, a non-empty `pdfs` list (`{ "file", "label" }`), and `license` set to `CC BY-SA 4.0`. Optional fields are `attribution`, `format`, `duration`, `materials`, and an http(s) `source_url`. Covers must have lowercase `.png`, `.jpg`, `.jpeg`, or `.webp` extensions; PDFs must have lowercase `.pdf` extensions.
+The library grid is built dynamically from every `*/index.md` in this directory. Each file requires `id` and a numeric `order` in frontmatter; cards are sorted by `order`, then title. A placeholder also sets `title` and `placeholder: true`, has no `metadata.json`, and renders a “Guide wanted” card without generating a detail page. Published guides omit `placeholder` (it defaults to `false`) and can use the Markdown body as their long description. The file is deliberately named `index.md`: Astro uses that filename to make the collection entry id equal to the folder slug.
-Only publishable zines belong in `src/content/zines/`. This collection has no `draft` state because its eager asset imports would emit a draft's cover and PDFs to the public build. Keep unfinished work in `src/content/zines-drafts/`, outside the collection and its asset globs.
+For published guides, `metadata.json` requires these fields: `id` (the folder slug, lowercase kebab-case), `title`, `topic`, `created_by`, `summary`, and a non-empty `pdfs` list. Each local PDF uses `{ "file", "label", "file_size" }`; each external http(s) download uses `{ "url", "label", "filename", "file_size" }`. The filename and human-readable size appear beside the Download button. Optional fields are `cover`, `license` (currently `CC BY-SA 4.0`), `attribution`, `format`, `duration`, `materials`, and an http(s) `source_url`. Covers must have lowercase `.png`, `.jpg`, `.jpeg`, or `.webp` extensions; PDF filenames must use lowercase `.pdf` extensions. A guide without a cover uses a grey title fallback on its library card and detail page.
+
+Only published zines and asset-free placeholder cards belong in `src/content/zines/`. This collection has no `draft` state because its eager asset imports would emit a draft's cover and PDFs to the public build. Keep unfinished work in `src/content/zines-drafts/`, outside the collection and its asset globs.
Before publishing, review PDFs for accessible, selectable (not scanned) text; logical reading order; document title and language; tagged headings where the authoring tool allows; and alt text on images. Include at least one PDF whose reading order follows the content, not only a print-imposed layout.
diff --git a/pcd-website/src/content/zines/animation/index.md b/pcd-website/src/content/zines/animation/index.md
new file mode 100644
index 0000000..5873c26
--- /dev/null
+++ b/pcd-website/src/content/zines/animation/index.md
@@ -0,0 +1,6 @@
+---
+id: animation
+title: Animation
+order: 11
+placeholder: true
+---
diff --git a/pcd-website/src/content/zines/arrays/index.md b/pcd-website/src/content/zines/arrays/index.md
new file mode 100644
index 0000000..20bce5f
--- /dev/null
+++ b/pcd-website/src/content/zines/arrays/index.md
@@ -0,0 +1,6 @@
+---
+id: arrays
+title: Arrays
+order: 6
+placeholder: true
+---
diff --git a/pcd-website/src/content/zines/color/index.md b/pcd-website/src/content/zines/color/index.md
new file mode 100644
index 0000000..2a507d6
--- /dev/null
+++ b/pcd-website/src/content/zines/color/index.md
@@ -0,0 +1,6 @@
+---
+id: color
+title: Color
+order: 9
+placeholder: true
+---
diff --git a/pcd-website/src/content/zines/conditionals/index.md b/pcd-website/src/content/zines/conditionals/index.md
new file mode 100644
index 0000000..8c81dbb
--- /dev/null
+++ b/pcd-website/src/content/zines/conditionals/index.md
@@ -0,0 +1,6 @@
+---
+id: conditionals
+title: Conditionals
+order: 3
+placeholder: true
+---
diff --git a/pcd-website/src/content/zines/coordinates/index.md b/pcd-website/src/content/zines/coordinates/index.md
new file mode 100644
index 0000000..536e18c
--- /dev/null
+++ b/pcd-website/src/content/zines/coordinates/index.md
@@ -0,0 +1,6 @@
+---
+id: coordinates
+title: Coordinates
+order: 8
+placeholder: true
+---
diff --git a/pcd-website/src/content/zines/functions/index.md b/pcd-website/src/content/zines/functions/index.md
new file mode 100644
index 0000000..bc41bc7
--- /dev/null
+++ b/pcd-website/src/content/zines/functions/index.md
@@ -0,0 +1,6 @@
+---
+id: functions
+title: Functions
+order: 5
+placeholder: true
+---
diff --git a/pcd-website/src/content/zines/interaction/index.md b/pcd-website/src/content/zines/interaction/index.md
new file mode 100644
index 0000000..422d1cd
--- /dev/null
+++ b/pcd-website/src/content/zines/interaction/index.md
@@ -0,0 +1,6 @@
+---
+id: interaction
+title: Interaction
+order: 10
+placeholder: true
+---
diff --git a/pcd-website/src/content/zines/loops/index.md b/pcd-website/src/content/zines/loops/index.md
new file mode 100644
index 0000000..682798e
--- /dev/null
+++ b/pcd-website/src/content/zines/loops/index.md
@@ -0,0 +1,6 @@
+---
+id: loops
+title: Loops
+order: 4
+placeholder: true
+---
diff --git a/pcd-website/src/content/zines/objects/index.md b/pcd-website/src/content/zines/objects/index.md
new file mode 100644
index 0000000..575d923
--- /dev/null
+++ b/pcd-website/src/content/zines/objects/index.md
@@ -0,0 +1,6 @@
+---
+id: objects
+title: Objects
+order: 7
+placeholder: true
+---
diff --git a/pcd-website/src/content/zines/randomness/index.md b/pcd-website/src/content/zines/randomness/index.md
new file mode 100644
index 0000000..bae2fbe
--- /dev/null
+++ b/pcd-website/src/content/zines/randomness/index.md
@@ -0,0 +1,6 @@
+---
+id: randomness
+title: Randomness
+order: 12
+placeholder: true
+---
diff --git a/pcd-website/src/content/zines/variables/index.md b/pcd-website/src/content/zines/variables/index.md
new file mode 100644
index 0000000..fb89f43
--- /dev/null
+++ b/pcd-website/src/content/zines/variables/index.md
@@ -0,0 +1,6 @@
+---
+id: variables
+title: Variables
+order: 2
+placeholder: true
+---
diff --git a/pcd-website/src/content/zines/zine-making-kit/index.md b/pcd-website/src/content/zines/zine-making-kit/index.md
new file mode 100644
index 0000000..e376146
--- /dev/null
+++ b/pcd-website/src/content/zines/zine-making-kit/index.md
@@ -0,0 +1,6 @@
+---
+id: zine-making-kit
+order: 1
+---
+
+Print and fold your own zine with this Zine Making Activity Kit produced by the US Library of Congress for the National Book Festival. Includes a zine diagram, an example zine, and instructions for making a zine.
diff --git a/pcd-website/src/content/zines/zine-making-kit/metadata.json b/pcd-website/src/content/zines/zine-making-kit/metadata.json
new file mode 100644
index 0000000..70ad87c
--- /dev/null
+++ b/pcd-website/src/content/zines/zine-making-kit/metadata.json
@@ -0,0 +1,16 @@
+{
+ "id": "zine-making-kit",
+ "title": "Zine Making Kit",
+ "topic": "Zine Making",
+ "created_by": "Library of Congress",
+ "format": "Individual pages",
+ "summary": "A guide to making your own zine.",
+ "pdfs": [
+ {
+ "url": "https://guides.loc.gov/ld.php?content_id=67687837",
+ "label": "Zine Making Kit",
+ "filename": "B230_zinemakingactivity.pdf",
+ "file_size": "519 kB"
+ }
+ ]
+}
diff --git a/pcd-website/src/lib/zine-metadata.d.ts b/pcd-website/src/lib/zine-metadata.d.ts
index fbeca75..a3b2e91 100644
--- a/pcd-website/src/lib/zine-metadata.d.ts
+++ b/pcd-website/src/lib/zine-metadata.d.ts
@@ -1,18 +1,15 @@
-export declare const ZINE_TOPICS: readonly [
- 'Variables', 'Conditionals', 'Loops', 'Functions', 'Arrays', 'Objects',
- 'Coordinates', 'Color', 'Interaction', 'Animation', 'Randomness',
-];
-export type ZineTopic = (typeof ZINE_TOPICS)[number];
export type ZineLicense = 'CC BY-SA 4.0';
export declare const LICENSE_URLS: Record;
-export interface ZinePdf { file: string; label: string }
+export type ZinePdf =
+ | { file: string; label: string; file_size: string }
+ | { url: string; label: string; filename: string; file_size: string };
export interface ZineMetadata {
- id: string; title: string; topic: ZineTopic;
+ id: string; title: string; topic: string;
created_by: string; attribution?: string;
format?: string; duration?: string; materials?: string;
- summary: string; cover: string; pdfs: ZinePdf[];
- license: ZineLicense; source_url?: string;
+ summary: string; cover?: string; pdfs: ZinePdf[];
+ license?: ZineLicense; source_url?: string;
}
export declare const zineMetadataSchema: import('astro/zod').ZodType;
@@ -20,8 +17,7 @@ export declare function parseZineMetadata(raw: unknown, slug: string): ZineMetad
export declare function assertIdentity(ids: {
slug: string; frontmatterId: string; metadataId: string;
}): void;
-export declare function assertUniqueTopics(zines: ZineMetadata[]): void;
export declare function assertUniqueIds(zines: ZineMetadata[]): void;
export declare function resolveZineAssets(
slug: string, metadata: ZineMetadata, availableFiles: string[],
-): { cover: string; pdfs: string[] };
+): { cover: string | undefined; pdfs: string[] };
diff --git a/pcd-website/src/lib/zine-metadata.js b/pcd-website/src/lib/zine-metadata.js
index d9e126f..bcb5241 100644
--- a/pcd-website/src/lib/zine-metadata.js
+++ b/pcd-website/src/lib/zine-metadata.js
@@ -1,10 +1,5 @@
import { z } from 'astro/zod';
-export const ZINE_TOPICS = [
- 'Variables', 'Conditionals', 'Loops', 'Functions', 'Arrays', 'Objects',
- 'Coordinates', 'Color', 'Interaction', 'Animation', 'Randomness',
-];
-
export const LICENSE_URLS = {
'CC BY-SA 4.0': 'https://creativecommons.org/licenses/by-sa/4.0/',
};
@@ -20,19 +15,28 @@ function isHttpUrl(value) {
export const zineMetadataSchema = z.object({
id: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'must be lowercase kebab-case'),
title: z.string().trim().min(1),
- topic: z.enum(ZINE_TOPICS),
+ topic: z.string().trim().min(1),
created_by: z.string().trim().min(1),
attribution: z.string().trim().min(1).optional(),
format: z.string().trim().min(1).optional(),
duration: z.string().trim().min(1).optional(),
materials: z.string().trim().min(1).optional(),
summary: z.string().trim().min(1),
- cover: z.string().regex(/\.(png|jpg|jpeg|webp)$/, 'cover must be a lowercase .png/.jpg/.jpeg/.webp'),
- pdfs: z.array(z.object({
- file: z.string().regex(/\.pdf$/, 'must be a lowercase .pdf'),
- label: z.string().trim().min(1),
- }).strict()).min(1, 'at least one PDF is required'),
- license: z.literal('CC BY-SA 4.0'),
+ cover: z.string().regex(/\.(png|jpg|jpeg|webp)$/, 'cover must be a lowercase .png/.jpg/.jpeg/.webp').optional(),
+ pdfs: z.array(z.union([
+ z.object({
+ file: z.string().regex(/\.pdf$/, 'must be a lowercase .pdf'),
+ label: z.string().trim().min(1),
+ file_size: z.string().trim().min(1),
+ }).strict(),
+ z.object({
+ url: z.string().refine(isHttpUrl, 'must be an http(s) URL'),
+ label: z.string().trim().min(1),
+ filename: z.string().regex(/\.pdf$/, 'must be a lowercase .pdf'),
+ file_size: z.string().trim().min(1),
+ }).strict(),
+ ])).min(1, 'at least one PDF is required'),
+ license: z.literal('CC BY-SA 4.0').optional(),
source_url: z.string().refine(isHttpUrl, 'must be an http(s) URL').optional(),
}).strict();
@@ -67,15 +71,6 @@ export function assertIdentity({ slug, frontmatterId, metadataId }) {
}
}
-export function assertUniqueTopics(zines) {
- const topics = new Map();
- for (const zine of zines) {
- const prior = topics.get(zine.topic);
- if (prior) throw new Error(`Zines "${prior.id}" and "${zine.id}" both claim the "${zine.topic}" topic. One canonical zine is allowed per topic.`);
- topics.set(zine.topic, zine);
- }
-}
-
export function assertUniqueIds(zines) {
const ids = new Set();
for (const zine of zines) {
@@ -86,7 +81,8 @@ export function assertUniqueIds(zines) {
export function resolveZineAssets(slug, metadata, availableFiles) {
const available = new Set(availableFiles);
- const required = [metadata.cover, ...metadata.pdfs.map((pdf) => pdf.file)];
+ const localPdfs = metadata.pdfs.flatMap((pdf) => 'file' in pdf ? [pdf.file] : []);
+ const required = [...(metadata.cover ? [metadata.cover] : []), ...localPdfs];
const missing = required.filter((file) => !available.has(file));
if (missing.length) {
throw new Error(
@@ -94,5 +90,5 @@ export function resolveZineAssets(slug, metadata, availableFiles) {
`Available files in src/content/zines/${slug}/: ${availableFiles.join(', ') || '(none)'}.`,
);
}
- return { cover: metadata.cover, pdfs: metadata.pdfs.map((pdf) => pdf.file) };
+ return { cover: metadata.cover, pdfs: localPdfs };
}
diff --git a/pcd-website/src/lib/zines.ts b/pcd-website/src/lib/zines.ts
index 2cd6c82..a3cce01 100644
--- a/pcd-website/src/lib/zines.ts
+++ b/pcd-website/src/lib/zines.ts
@@ -1,30 +1,41 @@
import { getCollection, type CollectionEntry } from 'astro:content';
import type { ImageMetadata } from 'astro';
import {
- assertIdentity, assertUniqueIds, assertUniqueTopics, parseZineMetadata,
- resolveZineAssets, type ZineLicense, type ZineTopic,
+ assertIdentity, assertUniqueIds, parseZineMetadata,
+ resolveZineAssets, type ZineLicense,
} from './zine-metadata.js';
interface MetadataModule { default: unknown }
export interface Zine {
id: string;
+ order: number;
+ placeholder: false;
title: string;
- topic: ZineTopic;
+ topic: string;
created_by: string;
attribution?: string;
format?: string;
duration?: string;
materials?: string;
summary: string;
- cover: ImageMetadata;
- pdfs: { url: string; label: string }[];
- license: ZineLicense;
+ cover?: ImageMetadata;
+ pdfs: { url: string; label: string; filename: string; fileSize: string }[];
+ license?: ZineLicense;
source_url?: string;
href: string;
entry: CollectionEntry<'zines'>;
}
+export interface ZinePlaceholder {
+ id: string;
+ order: number;
+ placeholder: true;
+ title: string;
+}
+
+export type ZineCard = Zine | ZinePlaceholder;
+
function filename(path: string): string {
return path.slice(path.lastIndexOf('/') + 1);
}
@@ -59,15 +70,26 @@ export async function loadZines(): Promise {
});
if (!Object.keys(metadataModules).length && !Object.keys(indexFiles).length) return [];
const entries = await getCollection('zines');
+ const publishedEntries = entries.filter((entry) => !entry.data.placeholder);
const metadataBySlug = filesBySlug(metadataModules);
const coversBySlug = filesBySlug(covers);
const pdfsBySlug = filesBySlug(pdfs);
- const entriesBySlug = new Map(entries.map((entry) => [entry.id, entry]));
+ const allEntriesBySlug = new Map(entries.map((entry) => [entry.id, entry]));
+ const entriesBySlug = new Map(publishedEntries.map((entry) => [entry.id, entry]));
for (const slug of metadataBySlug.keys()) {
- if (!entriesBySlug.has(slug)) {
+ const entry = allEntriesBySlug.get(slug);
+ if (!entry) {
throw new Error(`Zine "${slug}" has metadata.json but no sibling index.md. Add src/content/zines/${slug}/index.md.`);
}
+ if (entry.data.placeholder) {
+ throw new Error(`Placeholder zine "${slug}" must not include metadata.json.`);
+ }
+ }
+ for (const entry of entries) {
+ if (entry.data.id !== entry.id) {
+ throw new Error(`Zine folder "${entry.id}" has frontmatter id "${entry.data.id}". Set id to "${entry.id}".`);
+ }
}
for (const slug of entriesBySlug.keys()) {
if (!metadataBySlug.has(slug)) {
@@ -86,27 +108,46 @@ export async function loadZines(): Promise {
return { slug, metadata, entry };
});
- assertUniqueTopics(parsed.map(({ metadata }) => metadata));
assertUniqueIds(parsed.map(({ metadata }) => metadata));
return parsed.map(({ slug, metadata, entry }) => {
- const cover = coversBySlug.get(slug)?.get(metadata.cover);
- if (!cover) throw new Error(`Zine "${slug}" cover "${metadata.cover}" could not be loaded.`);
+ const cover = metadata.cover ? coversBySlug.get(slug)?.get(metadata.cover) : undefined;
+ if (metadata.cover && !cover) throw new Error(`Zine "${slug}" cover "${metadata.cover}" could not be loaded.`);
const pdfFiles = pdfsBySlug.get(slug);
return {
...metadata,
+ order: entry.data.order,
+ placeholder: false as const,
cover,
pdfs: metadata.pdfs.map((pdf) => {
+ if ('url' in pdf) return {
+ url: pdf.url, label: pdf.label, filename: pdf.filename, fileSize: pdf.file_size,
+ };
const url = pdfFiles?.get(pdf.file);
if (!url) throw new Error(`Zine "${slug}" PDF "${pdf.file}" could not be loaded.`);
- return { url, label: pdf.label };
+ return { url, label: pdf.label, filename: pdf.file, fileSize: pdf.file_size };
}),
href: `/activity-guide/${metadata.id}/`,
entry,
};
- }).sort((a, b) => a.title.localeCompare(b.title));
+ }).sort((a, b) => a.order - b.order || a.title.localeCompare(b.title));
}
-export async function loadZinesByTopic(): Promise> {
- return new Map((await loadZines()).map((zine) => [zine.topic, zine]));
+export async function loadZineCards(): Promise {
+ const [entries, zines] = await Promise.all([getCollection('zines'), loadZines()]);
+ const zinesById = new Map(zines.map((zine) => [zine.id, zine]));
+
+ return entries.map((entry): ZineCard => {
+ if (entry.data.placeholder) {
+ return {
+ id: entry.id,
+ order: entry.data.order,
+ placeholder: true,
+ title: entry.data.title!,
+ };
+ }
+ const zine = zinesById.get(entry.id);
+ if (!zine) throw new Error(`Published zine "${entry.id}" could not be loaded.`);
+ return zine;
+ }).sort((a, b) => a.order - b.order || a.title.localeCompare(b.title));
}
diff --git a/pcd-website/src/pages/activity-guide/[id].astro b/pcd-website/src/pages/activity-guide/[id].astro
index 55344ea..5756673 100644
--- a/pcd-website/src/pages/activity-guide/[id].astro
+++ b/pcd-website/src/pages/activity-guide/[id].astro
@@ -2,7 +2,9 @@
import { Image } from 'astro:assets';
import { render, type GetStaticPaths } from 'astro:content';
import SiteLayout from '../../layouts/SiteLayout.astro';
+import BackButton from '../../components/BackButton.astro';
import ExternalLinkIcon from '../../components/ExternalLinkIcon.astro';
+import ZineDownloads from '../../components/ZineDownloads.astro';
import { LICENSE_URLS } from '../../lib/zine-metadata.js';
import { loadZines, type Zine } from '../../lib/zines';
@@ -17,9 +19,12 @@ const { Content } = await render(zine.entry);
+
Activity Guide
{zine.title}
-
+ {zine.cover
+ ?
+ : {zine.title}
}
Topic {zine.topic}
@@ -31,14 +36,15 @@ const { Content } = await render(zine.entry);
- Downloads
-
+
- Licence and attribution
- This guide is available under the {zine.license} License .
- {zine.attribution && Preferred attribution: {zine.attribution}
}
+ {(zine.license || zine.attribution) && (
+ <>
+ Licence and attribution
+ {zine.license && This guide is available under the {zine.license} License .
}
+ {zine.attribution && Preferred attribution: {zine.attribution}
}
+ >
+ )}
{zine.source_url && (
@@ -47,8 +53,6 @@ const { Content } = await render(zine.entry);
)}
-
- Back to the Activity Guide Library
@@ -61,6 +65,19 @@ const { Content } = await render(zine.entry);
border-radius: var(--border-radius);
}
+ .activity-guide__cover--placeholder {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ aspect-ratio: 4 / 3;
+ padding: var(--spacing-lg);
+ color: var(--color-text-subtle);
+ font-size: clamp(1.5rem, 5vw, 3rem);
+ font-weight: 600;
+ text-align: center;
+ background: var(--color-border);
+ }
+
.activity-guide__metadata {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
diff --git a/pcd-website/src/styles/docs/components.css b/pcd-website/src/styles/docs/components.css
index 524d801..26159b4 100644
--- a/pcd-website/src/styles/docs/components.css
+++ b/pcd-website/src/styles/docs/components.css
@@ -264,8 +264,7 @@
color: var(--color-text-subtle);
border: 1px dashed var(--color-border);
}
-.guide-card--empty span,
-.guide-card__topic {
+.guide-card--empty span {
font-size: var(--step-2);
font-weight: 600;
text-transform: uppercase;
@@ -290,6 +289,17 @@
aspect-ratio: 4 / 3;
object-fit: cover;
}
+.guide-card__cover-placeholder {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ aspect-ratio: 4 / 3;
+ padding: var(--sp-4);
+ color: var(--color-text-subtle);
+ font-weight: 600;
+ background: var(--color-border);
+}
.guide-card__body {
display: flex;
flex: 1;
From 1147cfa5290d8899548ebf790b5b3cb1960bb814 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20de=20Courville?=
Date: Tue, 25 Aug 2026 19:56:58 +0200
Subject: [PATCH 07/15] Add topic-specific zine submit links
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Placeholder Activity Guide cards now show a “Submit a zine” button that opens the Discourse new-topic form with the topic prefilled in both the title and submission template body. This replaces the old “Guide wanted” placeholder text, updates card styling/layout, and refreshes Organizer Kit and zine documentation copy to match. Tests were updated to assert the new button text and prefilled URL behavior.
---
.github/scripts/zine-build.test.mjs | 12 +++++++++-
AGENTS.md | 2 +-
.../src/components/ActivityGuideGrid.astro | 17 ++++++++++----
pcd-website/src/config.ts | 23 ++++++++++++-------
.../organizer-kit/activity-guides/library.md | 2 +-
.../activity-guides/what-is-a-guide.md | 2 +-
pcd-website/src/content/zines/README.md | 2 +-
pcd-website/src/styles/docs/components.css | 19 +++++++++++----
8 files changed, 56 insertions(+), 23 deletions(-)
diff --git a/.github/scripts/zine-build.test.mjs b/.github/scripts/zine-build.test.mjs
index efbd93c..873fc71 100644
--- a/.github/scripts/zine-build.test.mjs
+++ b/.github/scripts/zine-build.test.mjs
@@ -66,7 +66,17 @@ test('a populated zine collection emits linked assets and renders entries in fro
library.indexOf('Randomness ') < library.indexOf(`/activity-guide/${SLUG}/`),
'order 12 should render before order 13',
);
- assert.equal((library.match(/Guide wanted/g) ?? []).length, 11, 'each placeholder file should render a wanted card');
+ assert.equal((library.match(/Submit a zine/g) ?? []).length, 11, 'each placeholder should render a submission button');
+ assert.doesNotMatch(library, /Guide wanted/);
+ const variablesLink = library.match(/Variables<\/strong>[\s\S]*?]+href="([^"]+)"/);
+ assert.ok(variablesLink, 'the Variables placeholder should have a submission link');
+ const variablesUrl = new URL(variablesLink[1].replaceAll('&', '&'));
+ assert.equal(variablesUrl.searchParams.get('title'), 'Activity Guide Submission: Variables');
+ assert.match(
+ variablesUrl.searchParams.get('body') ?? '',
+ /\*\*Title:\*\* Variables/,
+ 'a placeholder submission should pre-populate its topic in the submission body',
+ );
assert.equal(existsSync(join(DIST, 'activity-guide/variables/index.html')), false, 'placeholders should not get detail pages');
const grid = library.match(/([\s\S]*?)<\/ul>/);
assert.ok(grid, 'the library should render its grid');
diff --git a/AGENTS.md b/AGENTS.md
index e8b9d87..16f895e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -68,7 +68,7 @@ Event data lives in `src/content/events//`:
`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 ``.
-Activity Guide cards live in `src/content/zines//` and the library grid is built dynamically from every `*/index.md`, sorted by its required numeric `order` frontmatter. Published zines set `placeholder: false` implicitly and pair `index.md` with `metadata.json`, an optional cover image, and one or more PDF downloads. Downloads may be local sibling PDF assets or external http(s) URLs; metadata supplies the human-readable file size (and the filename for external files) used by the shared download rows. Placeholder topics contain only `index.md` with `title`, `order`, and `placeholder: true`; they render “Guide wanted” cards and do not generate detail pages. `src/lib/zines.ts` joins the Astro collection, metadata, and assets at build time; `src/lib/zine-metadata.js` owns the strict published-zine schema and pure validation. Zines must use `index.md` (not `content.md`) so Astro's glob loader makes the entry id equal to the folder slug. A published zine without a cover renders a grey title fallback in the library and on its detail page.
+Activity Guide cards live in `src/content/zines//` and the library grid is built dynamically from every `*/index.md`, sorted by its required numeric `order` frontmatter. Published zines set `placeholder: false` implicitly and pair `index.md` with `metadata.json`, an optional cover image, and one or more PDF downloads. Downloads may be local sibling PDF assets or external http(s) URLs; metadata supplies the human-readable file size (and the filename for external files) used by the shared download rows. Placeholder topics contain only `index.md` with `title`, `order`, and `placeholder: true`; they render cards with a topic-prefilled “Submit a zine” link and do not generate detail pages. `src/lib/zines.ts` joins the Astro collection, metadata, and assets at build time; `src/lib/zine-metadata.js` owns the strict published-zine schema and pure validation. Zines must use `index.md` (not `content.md`) so Astro's glob loader makes the entry id equal to the folder slug. A published zine without a cover renders a grey title fallback in the library and on its detail page.
Zine PDFs are emitted from `src/` assets using `?url&no-inline`, so even small downloads become real files in `dist/`. `src/content/zines/` contains publishable zines only: it deliberately has no `draft` field because eager asset imports would make draft files public. Keep unfinished zines in `src/content/zines-drafts/`. Review PDFs for selectable text, logical reading order, document title and language, tagged headings where possible, alt text, and at least one screen-reader-friendly reading-order version.
diff --git a/pcd-website/src/components/ActivityGuideGrid.astro b/pcd-website/src/components/ActivityGuideGrid.astro
index 025b306..70957fd 100644
--- a/pcd-website/src/components/ActivityGuideGrid.astro
+++ b/pcd-website/src/components/ActivityGuideGrid.astro
@@ -1,6 +1,6 @@
---
import { Image } from 'astro:assets';
-import { ACTIVITY_GUIDE_SUBMIT_URL } from '../config';
+import { ACTIVITY_GUIDE_SUBMIT_URL, activityGuideSubmitUrl } from '../config';
import { loadZineCards } from '../lib/zines';
import ExternalLinkIcon from './ExternalLinkIcon.astro';
@@ -11,10 +11,17 @@ const cards = await loadZineCards();
{cards.map((card) => {
return card.placeholder ? (
-
- {card.title}
- Guide wanted
-
+
) : (
diff --git a/pcd-website/src/config.ts b/pcd-website/src/config.ts
index 13c8815..14be5cc 100644
--- a/pcd-website/src/config.ts
+++ b/pcd-website/src/config.ts
@@ -39,14 +39,21 @@ export const ACTIVITY_GUIDE_SUBMISSION_TEMPLATE = `*This post uses the submissio
-export const ACTIVITY_GUIDE_SUBMIT_URL =
- "https://discourse.processing.org/new-topic?" +
- new URLSearchParams({
- title: 'Activity Guide Submission: [Title of your Activity]',
- body: ACTIVITY_GUIDE_SUBMISSION_TEMPLATE,
- category: 'community',
- tags: 'pcd,zine',
- }).toString();
+export function activityGuideSubmitUrl(topic?: string): string {
+ return "https://discourse.processing.org/new-topic?" +
+ new URLSearchParams({
+ title: topic
+ ? `Activity Guide Submission: ${topic}`
+ : 'Activity Guide Submission: [Title of your Activity]',
+ body: topic
+ ? ACTIVITY_GUIDE_SUBMISSION_TEMPLATE.replace('**Title:**', `**Title:** ${topic}`)
+ : ACTIVITY_GUIDE_SUBMISSION_TEMPLATE,
+ category: 'community',
+ tags: 'pcd,zine',
+ }).toString();
+}
+
+export const ACTIVITY_GUIDE_SUBMIT_URL = activityGuideSubmitUrl();
export const PCD_DISCORD_URL = "https://discord.gg/q5NksnwGsY";
diff --git a/pcd-website/src/content/organizer-kit/activity-guides/library.md b/pcd-website/src/content/organizer-kit/activity-guides/library.md
index 73178a8..81db9e2 100644
--- a/pcd-website/src/content/organizer-kit/activity-guides/library.md
+++ b/pcd-website/src/content/organizer-kit/activity-guides/library.md
@@ -5,4 +5,4 @@ order: 1
description: Community-created zines you can use to facilitate a session at your PCD.
---
-Browse the collection and choose a guide that fits your needs. You can use a guide as is or adapt it as needed.
+Submissions are welcome! You can create and submit an activity guide on any of the listed topics, or come up with your own. See [How to Create an Activity Guide](/organize/activity-guides/what-is-a-guide/#how-to-create-an-activity-guide) for instructions.
\ No newline at end of file
diff --git a/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md b/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
index 0186676..bf6ae3f 100644
--- a/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
+++ b/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
@@ -23,7 +23,7 @@ Your guide should include:
For a tutorial-style activity, you may also want to include:
- step-by-step instructions
-- example code or templates (can be links too)
+- example sketches or short code snippets
- suggested exercises or challenges
For a discussion or collaborative activity, you may also want to include:
diff --git a/pcd-website/src/content/zines/README.md b/pcd-website/src/content/zines/README.md
index 4ffce2b..7a13ee9 100644
--- a/pcd-website/src/content/zines/README.md
+++ b/pcd-website/src/content/zines/README.md
@@ -12,7 +12,7 @@ src/content/zines//
A topic without a guide uses only `index.md`.
-The library grid is built dynamically from every `*/index.md` in this directory. Each file requires `id` and a numeric `order` in frontmatter; cards are sorted by `order`, then title. A placeholder also sets `title` and `placeholder: true`, has no `metadata.json`, and renders a “Guide wanted” card without generating a detail page. Published guides omit `placeholder` (it defaults to `false`) and can use the Markdown body as their long description. The file is deliberately named `index.md`: Astro uses that filename to make the collection entry id equal to the folder slug.
+The library grid is built dynamically from every `*/index.md` in this directory. Each file requires `id` and a numeric `order` in frontmatter; cards are sorted by `order`, then title. A placeholder also sets `title` and `placeholder: true`, has no `metadata.json`, and renders a topic-specific “Submit a zine” link without generating a detail page. Published guides omit `placeholder` (it defaults to `false`) and can use the Markdown body as their long description. The file is deliberately named `index.md`: Astro uses that filename to make the collection entry id equal to the folder slug.
For published guides, `metadata.json` requires these fields: `id` (the folder slug, lowercase kebab-case), `title`, `topic`, `created_by`, `summary`, and a non-empty `pdfs` list. Each local PDF uses `{ "file", "label", "file_size" }`; each external http(s) download uses `{ "url", "label", "filename", "file_size" }`. The filename and human-readable size appear beside the Download button. Optional fields are `cover`, `license` (currently `CC BY-SA 4.0`), `attribution`, `format`, `duration`, `materials`, and an http(s) `source_url`. Covers must have lowercase `.png`, `.jpg`, `.jpeg`, or `.webp` extensions; PDF filenames must use lowercase `.pdf` extensions. A guide without a cover uses a grey title fallback on its library card and detail page.
diff --git a/pcd-website/src/styles/docs/components.css b/pcd-website/src/styles/docs/components.css
index 26159b4..3f9b4d1 100644
--- a/pcd-website/src/styles/docs/components.css
+++ b/pcd-website/src/styles/docs/components.css
@@ -251,6 +251,7 @@
flex-direction: column;
align-items: center;
justify-content: center;
+ aspect-ratio: 210 / 297;
min-height: 5.5rem;
overflow: hidden;
color: var(--color-text);
@@ -259,16 +260,24 @@
border-radius: var(--border-radius);
}
.guide-card--empty {
- gap: var(--sp-1);
+ justify-content: flex-start;
+ gap: var(--sp-3);
padding: var(--sp-4);
color: var(--color-text-subtle);
border: 1px dashed var(--color-border);
}
-.guide-card--empty span {
+.guide-card__empty-title {
+ display: flex;
+ flex: 1;
+ align-items: center;
+ justify-content: center;
+}
+.guide-card__submit {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--sp-1);
font-size: var(--step-2);
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.06em;
+ white-space: nowrap;
}
.guide-card--zine {
align-items: stretch;
From 9976ca21001eabb6021e6bf409c67fc573e57565 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20de=20Courville?=
Date: Thu, 27 Aug 2026 18:39:17 +0200
Subject: [PATCH 08/15] Rename activity guide library to zine library
Updates the Organizer Kit copy and navigation labels to use "Zine Library" instead of "Activity Guide Library," and reorders the two related docs so the overview page comes first. Also refreshes the submission template text to match the new naming.
---
pcd-website/src/config.ts | 2 +-
.../src/content/organizer-kit/activity-guides/library.md | 6 +++---
.../organizer-kit/activity-guides/what-is-a-guide.md | 8 ++++----
3 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/pcd-website/src/config.ts b/pcd-website/src/config.ts
index 14be5cc..919dc0e 100644
--- a/pcd-website/src/config.ts
+++ b/pcd-website/src/config.ts
@@ -16,7 +16,7 @@ export const PCD_FORUM_NEW_TOPIC_URL =
tags: "pcd",
}).toString();
-export const ACTIVITY_GUIDE_SUBMISSION_TEMPLATE = `*This post uses the submission template for the Processing Community Day [Activity Guide Library](https://day.processing.org/organize/activity-guides/library/), a collection of activities for PCD events.*
+export const ACTIVITY_GUIDE_SUBMISSION_TEMPLATE = `*This post uses the submission template for the Processing Community Day [Zine Library](https://day.processing.org/organize/activity-guides/library/), a collection of activities for PCD events.*
---
diff --git a/pcd-website/src/content/organizer-kit/activity-guides/library.md b/pcd-website/src/content/organizer-kit/activity-guides/library.md
index 81db9e2..040c62a 100644
--- a/pcd-website/src/content/organizer-kit/activity-guides/library.md
+++ b/pcd-website/src/content/organizer-kit/activity-guides/library.md
@@ -1,8 +1,8 @@
---
-title: Activity Guide Library
+title: Zine Library
section: Activity Guides
-order: 1
+order: 2
description: Community-created zines you can use to facilitate a session at your PCD.
---
-Submissions are welcome! You can create and submit an activity guide on any of the listed topics, or come up with your own. See [How to Create an Activity Guide](/organize/activity-guides/what-is-a-guide/#how-to-create-an-activity-guide) for instructions.
\ No newline at end of file
+Submissions are welcome! You can create and submit an activity guide on any of the listed topics, or come up with your own. See [How to Create an Activity Guide](/organize/activity-guides/what-is-a-guide/#how-to-create-an-activity-guide) for instructions.
diff --git a/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md b/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
index bf6ae3f..54bca24 100644
--- a/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
+++ b/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
@@ -1,8 +1,8 @@
---
-title: What are Activity Guides?
+title: About Activity Guides
section: Activity Guides
-order: 2
-description: Instructions for creating and submitting an activity guide to the PCD Activity Guide Library.
+order: 1
+description: Instructions for creating and submitting an activity guide to the PCD Zine Library.
---
Activity Guides are community-created zines (see ["What is a Zine?"](#what-is-a-zine)) that you can use to facilitate or take part in an activity at your Processing Community Day. They are designed to be taken “off the shelf,” so you do not need to be an expert on the topic to use one.
@@ -58,4 +58,4 @@ The US Library of Congress has a [Zine Making Guide (PDF)](https://guides.loc.go
## Submit Your Zine
-Go to the [Activity Guide Library](/organize/activity-guides/library/) to submit your zine and explore the full collection. By submitting an Activity Guide, you agree to publish your original contribution under the [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
\ No newline at end of file
+Go to the [Zine Library](/organize/activity-guides/library/) to submit your zine and explore the full collection. By submitting an Activity Guide, you agree to publish your original contribution under the [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
From e98b5b36387ddd59f66e937eacbc391f1b366597 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20de=20Courville?=
Date: Thu, 27 Aug 2026 18:42:17 +0200
Subject: [PATCH 09/15] Split the zine making instructions to a new page
---
.../activity-guides/making-a-zine.md | 34 +++++++++++++++++++
.../activity-guides/what-is-a-guide.md | 34 ++-----------------
2 files changed, 37 insertions(+), 31 deletions(-)
create mode 100644 pcd-website/src/content/organizer-kit/activity-guides/making-a-zine.md
diff --git a/pcd-website/src/content/organizer-kit/activity-guides/making-a-zine.md b/pcd-website/src/content/organizer-kit/activity-guides/making-a-zine.md
new file mode 100644
index 0000000..0a8e3dd
--- /dev/null
+++ b/pcd-website/src/content/organizer-kit/activity-guides/making-a-zine.md
@@ -0,0 +1,34 @@
+---
+title: Making a Zine
+section: Activity Guides
+order: 3
+description: An introduction to zines, resources for making one, and information about submitting your zine.
+---
+
+## What is a Zine?
+
+A [zine](https://en.wikipedia.org/wiki/Zine) (pronounced “zeen,” short for magazine or fanzine) is a small, independent publication usually created by one person or a small group. It usually explores a specific or unconventional subject and is often made by hand, photocopied, or published online. Usually, a zine is a small booklet or pamphlet, often with a limited number of pages. Many zines can be printed and folded from a single sheet of paper.
+
+## Zine making resources
+
+This [wikiHow article](https://www.wikihow.com/Make-a-Zine) is an illustrated step-by-step guide to making a zine. It's a great starting point for creating your first zine.
+
+This YouTube video, [How to Make a Zine](https://www.youtube.com/watch?v=ab4O9SWNl9g) by Austin Kleon, is a short and fun introduction to zine-making.
+
+The US Library of Congress has a [Zine Making Guide (PDF)](https://guides.loc.gov/ld.php?content_id=67687837) and a [list of zine making resources](https://guides.loc.gov/zines/external-websites).
+
+### Tools
+
+- [p5.(gen)zine](https://github.com/munusshih/p5.genzine) by Munus Shih and Iley Cao is an open-sourced and friendly p5.js library for zine-making.
+
+- [The Electric Zine Maker](https://alienmelon.itch.io/electric-zine-maker) by alienmelon is a printshop and art tool for easily making and printing zines.
+
+- [Zine Arranger](https://nashhigh.itch.io/zinearranger) by Nash Hight, arranges multi-page PDF files into a printable zine layout.
+
+### Examples
+
+- [Tiny Tech Zines](https://tinytechzines.bigcartel.com/)
+
+## Submit Your Zine
+
+Go to the [Zine Library](/organize/activity-guides/library/) to submit your zine and explore the full collection. By submitting an Activity Guide, you agree to publish your original contribution under the [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
diff --git a/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md b/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
index 54bca24..38d55d4 100644
--- a/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
+++ b/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
@@ -2,12 +2,12 @@
title: About Activity Guides
section: Activity Guides
order: 1
-description: Instructions for creating and submitting an activity guide to the PCD Zine Library.
+description: Instructions for creating an activity guide for the PCD Zine Library.
---
-Activity Guides are community-created zines (see ["What is a Zine?"](#what-is-a-zine)) that you can use to facilitate or take part in an activity at your Processing Community Day. They are designed to be taken “off the shelf,” so you do not need to be an expert on the topic to use one.
+Activity Guides are community-created zines (see ["What is a Zine?"](/organize/activity-guides/making-a-zine/#what-is-a-zine)) that you can use to facilitate or take part in an activity at your Processing Community Day. They are designed to be taken “off the shelf,” so you do not need to be an expert on the topic to use one.
-## How to Create an Activity Guide
+## What should an Activity Guide include?
An Activity Guide should be practical, self-contained, and easy to follow. It might contain instructions for a hands-on workshop, creative exercise, discussion, collaborative experiment, or another kind of group or individual activity.
@@ -31,31 +31,3 @@ For a discussion or collaborative activity, you may also want to include:
- suggested group exercises or collaborative tasks
There is no required visual format. Make it your own, but include enough information for someone unfamiliar with the activity or subject to facilitate it or complete it independently.
-
-## What is a Zine?
-
-A [zine](https://en.wikipedia.org/wiki/Zine) (pronounced “zeen,” short for magazine or fanzine) is a small, independent publication usually created by one person or a small group. It usually explores a specific or unconventional subject and is often made by hand, photocopied, or published online. Usually, a zine is a small booklet or pamphlet, often with a limited number of pages. Many zines can be printed and folded from a single sheet of paper.
-
-## Zine making resources
-
-This [wikiHow article](https://www.wikihow.com/Make-a-Zine) is an illustrated step-by-step guide to making a zine. It's a great starting point for creating your first zine.
-
-This YouTube video, [How to Make a Zine](https://www.youtube.com/watch?v=ab4O9SWNl9g) by Austin Kleon, is a short and fun introduction to zine-making.
-
-The US Library of Congress has a [Zine Making Guide (PDF)](https://guides.loc.gov/ld.php?content_id=67687837) and a [list of zine making resources](https://guides.loc.gov/zines/external-websites).
-
-### Tools
-
-- [p5.(gen)zine](https://github.com/munusshih/p5.genzine) by Munus Shih and Iley Cao is an open-sourced and friendly p5.js library for zine-making.
-
-- [The Electric Zine Maker](https://alienmelon.itch.io/electric-zine-maker) by alienmelon is a printshop and art tool for easily making and printing zines.
-
-- [Zine Arranger](https://nashhigh.itch.io/zinearranger) by Nash Hight, arranges multi-page PDF files into a printable zine layout.
-
-### Examples
-
-- [Tiny Tech Zines](https://tinytechzines.bigcartel.com/)
-
-## Submit Your Zine
-
-Go to the [Zine Library](/organize/activity-guides/library/) to submit your zine and explore the full collection. By submitting an Activity Guide, you agree to publish your original contribution under the [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
From ce175d625cf7eacfbcb00117aa74a9c1b9a8454c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20de=20Courville?=
Date: Thu, 27 Aug 2026 19:29:52 +0200
Subject: [PATCH 10/15] Restructure organizer kit sections
Rename "resources" to "event resource", add "additional guides", and reorder sections
---
pcd-website/src/config/organizer-kit-nav.ts | 2 +-
.../organizer-kit/activity-guides/additional-guides.md | 8 ++++++++
.../content/organizer-kit/resources/about-the-forum.md | 2 +-
.../content/organizer-kit/resources/about-the-pcd-map.md | 2 +-
.../src/content/organizer-kit/resources/accessibility.md | 2 +-
.../content/organizer-kit/resources/event-checklist.md | 2 +-
.../src/content/organizer-kit/resources/info-sessions.md | 2 +-
.../organizer-kit/resources/photography-and-video.md | 2 +-
.../content/organizer-kit/resources/venue-checklist.md | 2 +-
9 files changed, 16 insertions(+), 8 deletions(-)
create mode 100644 pcd-website/src/content/organizer-kit/activity-guides/additional-guides.md
diff --git a/pcd-website/src/config/organizer-kit-nav.ts b/pcd-website/src/config/organizer-kit-nav.ts
index 831d018..f6f9827 100644
--- a/pcd-website/src/config/organizer-kit-nav.ts
+++ b/pcd-website/src/config/organizer-kit-nav.ts
@@ -27,9 +27,9 @@ export const KIT_BASE = '/organize';
const TOP_LEVEL: ReadonlyArray = [
'Getting Started',
'Organizing Your Event',
+ 'Event Resources',
'Activity Guides',
{ page: 'peer-support-sessions' },
- 'Resources',
{ page: 'code-of-conduct' },
{ page: 'faq' },
{ page: 'about-processing-foundation' },
diff --git a/pcd-website/src/content/organizer-kit/activity-guides/additional-guides.md b/pcd-website/src/content/organizer-kit/activity-guides/additional-guides.md
new file mode 100644
index 0000000..e893461
--- /dev/null
+++ b/pcd-website/src/content/organizer-kit/activity-guides/additional-guides.md
@@ -0,0 +1,8 @@
+---
+title: Additional Guides
+section: Activity Guides
+order: 4
+description: Additional guides for Processing Community Day activities.
+---
+
+TBD
diff --git a/pcd-website/src/content/organizer-kit/resources/about-the-forum.md b/pcd-website/src/content/organizer-kit/resources/about-the-forum.md
index 1b517fd..626788a 100644
--- a/pcd-website/src/content/organizer-kit/resources/about-the-forum.md
+++ b/pcd-website/src/content/organizer-kit/resources/about-the-forum.md
@@ -1,6 +1,6 @@
---
title: About the Forum
-section: Resources
+section: Event Resources
order: 3
description: Introducing yourself and coordinating in the open on the Processing forum.
draft: true
diff --git a/pcd-website/src/content/organizer-kit/resources/about-the-pcd-map.md b/pcd-website/src/content/organizer-kit/resources/about-the-pcd-map.md
index a7b1c95..30d199d 100644
--- a/pcd-website/src/content/organizer-kit/resources/about-the-pcd-map.md
+++ b/pcd-website/src/content/organizer-kit/resources/about-the-pcd-map.md
@@ -1,6 +1,6 @@
---
title: About the PCD Map
-section: Resources
+section: Event Resources
order: 4
description: Getting your event onto day.processing.org, and editing it later.
draft: true
diff --git a/pcd-website/src/content/organizer-kit/resources/accessibility.md b/pcd-website/src/content/organizer-kit/resources/accessibility.md
index 7602eaf..7b84251 100644
--- a/pcd-website/src/content/organizer-kit/resources/accessibility.md
+++ b/pcd-website/src/content/organizer-kit/resources/accessibility.md
@@ -1,6 +1,6 @@
---
title: Accessibility
-section: Resources
+section: Event Resources
order: 5
description: Making your PCD welcoming and usable for disabled participants.
---
diff --git a/pcd-website/src/content/organizer-kit/resources/event-checklist.md b/pcd-website/src/content/organizer-kit/resources/event-checklist.md
index 33253f8..7ce05e7 100644
--- a/pcd-website/src/content/organizer-kit/resources/event-checklist.md
+++ b/pcd-website/src/content/organizer-kit/resources/event-checklist.md
@@ -1,6 +1,6 @@
---
title: Event Checklist
-section: Resources
+section: Event Resources
order: 1
description: Setup, communications, logistics, accessibility, and day-of checklists.
---
diff --git a/pcd-website/src/content/organizer-kit/resources/info-sessions.md b/pcd-website/src/content/organizer-kit/resources/info-sessions.md
index 3601298..9d2989b 100644
--- a/pcd-website/src/content/organizer-kit/resources/info-sessions.md
+++ b/pcd-website/src/content/organizer-kit/resources/info-sessions.md
@@ -1,6 +1,6 @@
---
title: Info Sessions
-section: Resources
+section: Event Resources
order: 7
description: Recordings of past info sessions for interested organizers.
---
diff --git a/pcd-website/src/content/organizer-kit/resources/photography-and-video.md b/pcd-website/src/content/organizer-kit/resources/photography-and-video.md
index df90a03..e2a90fe 100644
--- a/pcd-website/src/content/organizer-kit/resources/photography-and-video.md
+++ b/pcd-website/src/content/organizer-kit/resources/photography-and-video.md
@@ -1,6 +1,6 @@
---
title: Taking Photos
-section: Resources
+section: Event Resources
order: 6
description: Consent, tips, and who should be behind the camera.
---
diff --git a/pcd-website/src/content/organizer-kit/resources/venue-checklist.md b/pcd-website/src/content/organizer-kit/resources/venue-checklist.md
index 4a151ba..07086ed 100644
--- a/pcd-website/src/content/organizer-kit/resources/venue-checklist.md
+++ b/pcd-website/src/content/organizer-kit/resources/venue-checklist.md
@@ -1,6 +1,6 @@
---
title: Venue Checklist
-section: Resources
+section: Event Resources
order: 2
description: Questions to ask about a space before you commit to it.
---
From 447a82e75ba0d4c391bab255bf0404a47e0b7bad Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20de=20Courville?=
Date: Thu, 27 Aug 2026 20:00:05 +0200
Subject: [PATCH 11/15] Add dev CARTO key support to tile URLs for local dev
Add support for using .env for local dev carto.com API key. Introduces `src/lib/carto.ts` with a `cartoTileUrl()` helper that appends `PUBLIC_CARTO_API_KEY` (`import.meta.env.DEV`). MapView and NodePanel now use this helper for all CARTO basemap layers, so local development can use keyed tile requests without affecting production URLs. AGENTS.md was updated to document the new helper and the local `.env` behavior.
---
AGENTS.md | 3 +++
pcd-website/src/components/MapView.vue | 5 +++--
pcd-website/src/components/NodePanel.vue | 3 ++-
pcd-website/src/lib/carto.ts | 11 +++++++++++
4 files changed, 19 insertions(+), 3 deletions(-)
create mode 100644 pcd-website/src/lib/carto.ts
diff --git a/AGENTS.md b/AGENTS.md
index 16f895e..20ab920 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -103,6 +103,7 @@ The global Markdown pipeline runs `rehype-table-wrapper` and `rehype-heading-anc
| `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/carto.ts` | Adds the optional local-development CARTO API key to basemap tile URLs |
| `src/lib/nodes.ts` | `Node` interface + `loadNodes()` |
| `src/lib/format.ts` | `formatDate()`, `formatDateRange()`, `calendarLinks()`, etc. |
| `src/lib/popup.ts` | Leaflet popup HTML generation (`makePopupContent()`) |
@@ -163,6 +164,8 @@ Use `src/config.ts` for static, non-secret values that are referenced across mul
- Anything already defined in `astro.config.mjs` (e.g. base path)
- Component-local constants that aren't shared
+For local map development, `pcd-website/.env` may define `PUBLIC_CARTO_API_KEY`. During `npm run dev`, all CARTO raster tile URLs append it as the `key` query parameter; production builds do not embed it.
+
## UI / Styling Rules
- 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.
diff --git a/pcd-website/src/components/MapView.vue b/pcd-website/src/components/MapView.vue
index 2d18242..98a24bf 100644
--- a/pcd-website/src/components/MapView.vue
+++ b/pcd-website/src/components/MapView.vue
@@ -9,6 +9,7 @@ import InfoModal from './InfoModal.vue';
import SubmitModal from './SubmitModal.vue';
import { currentLocale } from '../i18n/localeState';
import { trackEvent, SUBMIT_EVENT_BUTTON_CLICK } from '../lib/analytics';
+import { cartoTileUrl } from '../lib/carto';
import { i18n } from '../i18n/index';
const props = defineProps<{
@@ -66,11 +67,11 @@ const CARTO_ATTR = '© Ope
const LIGHT_LAYERS: TileLayerConfig[] = [
{
- url: 'https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png',
+ url: cartoTileUrl('https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png'),
options: { attribution: CARTO_ATTR, subdomains: 'abcd', maxZoom: 20, detectRetina: true },
},
{
- url: 'https://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}{r}.png',
+ url: cartoTileUrl('https://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}{r}.png'),
options: { attribution: CARTO_ATTR, subdomains: 'abcd', maxZoom: 20, detectRetina: true, tileSize: 512, zoomOffset: -1 },
},
];
diff --git a/pcd-website/src/components/NodePanel.vue b/pcd-website/src/components/NodePanel.vue
index 53dab32..af7e03c 100644
--- a/pcd-website/src/components/NodePanel.vue
+++ b/pcd-website/src/components/NodePanel.vue
@@ -5,6 +5,7 @@ import { createFocusTrap, type FocusTrap } from 'focus-trap';
import { Icon } from '@iconify/vue';
import type { Node } from '../lib/nodes';
import { formatDateRange, formatTimeRange, calendarLinks, onlinePlatformName } from '../lib/format';
+import { cartoTileUrl } from '../lib/carto';
import { getOsmUrl } from '../lib/popup';
import { GITHUB_EDIT_EVENT_URL, GITHUB_CONTENT_ISSUE_URL } from '../config';
const props = defineProps<{
@@ -108,7 +109,7 @@ async function initMinimap(node: Node) {
tap: false,
} as L.MapOptions & { tap: boolean });
- L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', {
+ L.tileLayer(cartoTileUrl('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png'), {
subdomains: 'abcd',
maxZoom: 20,
}).addTo(minimap);
diff --git a/pcd-website/src/lib/carto.ts b/pcd-website/src/lib/carto.ts
new file mode 100644
index 0000000..c95e985
--- /dev/null
+++ b/pcd-website/src/lib/carto.ts
@@ -0,0 +1,11 @@
+const localDevApiKey = import.meta.env.DEV
+ ? import.meta.env.PUBLIC_CARTO_API_KEY?.trim()
+ : undefined;
+
+/** Add the optional local-development API key to a CARTO basemap URL. */
+export function cartoTileUrl(url: string): string {
+ if (!localDevApiKey) return url;
+
+ const separator = url.includes('?') ? '&' : '?';
+ return `${url}${separator}key=${encodeURIComponent(localDevApiKey)}`;
+}
From 441d71643d387c136f75d6384ed0527dfbd8064e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20de=20Courville?=
Date: Thu, 27 Aug 2026 20:09:59 +0200
Subject: [PATCH 12/15] Rename library to zine-library
---
.github/scripts/zine-build.test.mjs | 2 +-
AGENTS.md | 2 +-
netlify.toml | 6 +++---
pcd-website/src/config.ts | 2 +-
.../content/organizer-kit/activity-guides/making-a-zine.md | 2 +-
.../organizer-kit/activity-guides/what-is-a-guide.md | 6 ++++--
.../activity-guides/{library.md => zine-library.md} | 0
.../organizer-kit/getting-started/minimum-viable-pcd.md | 2 +-
pcd-website/src/pages/activity-guide/[id].astro | 2 +-
pcd-website/src/pages/organize/[...slug].astro | 4 ++--
10 files changed, 15 insertions(+), 13 deletions(-)
rename pcd-website/src/content/organizer-kit/activity-guides/{library.md => zine-library.md} (100%)
diff --git a/.github/scripts/zine-build.test.mjs b/.github/scripts/zine-build.test.mjs
index 873fc71..826f1ff 100644
--- a/.github/scripts/zine-build.test.mjs
+++ b/.github/scripts/zine-build.test.mjs
@@ -53,7 +53,7 @@ test('a populated zine collection emits linked assets and renders entries in fro
);
assert.match(noCoverPage, /download-list__size[^>]*>519 kB);
- const library = readFileSync(join(DIST, 'organize/activity-guides/library/index.html'), 'utf8');
+ const library = readFileSync(join(DIST, 'organize/activity-guides/zine-library/index.html'), 'utf8');
assert.match(library, /\s*\s*]*>Zine Making Kit);
assert.match(library, new RegExp(`href="/activity-guide/${SLUG}/"`));
diff --git a/AGENTS.md b/AGENTS.md
index 20ab920..1805698 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -68,7 +68,7 @@ Event data lives in `src/content/events//`:
`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 ``.
-Activity Guide cards live in `src/content/zines//` and the library grid is built dynamically from every `*/index.md`, sorted by its required numeric `order` frontmatter. Published zines set `placeholder: false` implicitly and pair `index.md` with `metadata.json`, an optional cover image, and one or more PDF downloads. Downloads may be local sibling PDF assets or external http(s) URLs; metadata supplies the human-readable file size (and the filename for external files) used by the shared download rows. Placeholder topics contain only `index.md` with `title`, `order`, and `placeholder: true`; they render cards with a topic-prefilled “Submit a zine” link and do not generate detail pages. `src/lib/zines.ts` joins the Astro collection, metadata, and assets at build time; `src/lib/zine-metadata.js` owns the strict published-zine schema and pure validation. Zines must use `index.md` (not `content.md`) so Astro's glob loader makes the entry id equal to the folder slug. A published zine without a cover renders a grey title fallback in the library and on its detail page.
+Activity Guide cards live in `src/content/zines//` and the library grid at `/organize/activity-guides/zine-library/` is built dynamically from every `*/index.md`, sorted by its required numeric `order` frontmatter. Published zines set `placeholder: false` implicitly and pair `index.md` with `metadata.json`, an optional cover image, and one or more PDF downloads. Downloads may be local sibling PDF assets or external http(s) URLs; metadata supplies the human-readable file size (and the filename for external files) used by the shared download rows. Placeholder topics contain only `index.md` with `title`, `order`, and `placeholder: true`; they render cards with a topic-prefilled “Submit a zine” link and do not generate detail pages. `src/lib/zines.ts` joins the Astro collection, metadata, and assets at build time; `src/lib/zine-metadata.js` owns the strict published-zine schema and pure validation. Zines must use `index.md` (not `content.md`) so Astro's glob loader makes the entry id equal to the folder slug. A published zine without a cover renders a grey title fallback in the library and on its detail page.
Zine PDFs are emitted from `src/` assets using `?url&no-inline`, so even small downloads become real files in `dist/`. `src/content/zines/` contains publishable zines only: it deliberately has no `draft` field because eager asset imports would make draft files public. Keep unfinished zines in `src/content/zines-drafts/`. Review PDFs for selectable text, logical reading order, document title and language, tagged headings where possible, alt text, and at least one screen-reader-friendly reading-order version.
diff --git a/netlify.toml b/netlify.toml
index 3320a3f..92750e6 100644
--- a/netlify.toml
+++ b/netlify.toml
@@ -17,13 +17,13 @@
status = 301
[[redirects]]
- from = "/organize/activity-guides/zine-library/*"
- to = "/organize/activity-guides/library/"
+ from = "/organize/activity-guides/library/*"
+ to = "/organize/activity-guides/zine-library/"
status = 301
[[redirects]]
from = "/organize/activity-guides/what-are-activity-guides/*"
- to = "/organize/activity-guides/library/"
+ to = "/organize/activity-guides/zine-library/"
status = 301
[[headers]]
diff --git a/pcd-website/src/config.ts b/pcd-website/src/config.ts
index 919dc0e..5f03270 100644
--- a/pcd-website/src/config.ts
+++ b/pcd-website/src/config.ts
@@ -16,7 +16,7 @@ export const PCD_FORUM_NEW_TOPIC_URL =
tags: "pcd",
}).toString();
-export const ACTIVITY_GUIDE_SUBMISSION_TEMPLATE = `*This post uses the submission template for the Processing Community Day [Zine Library](https://day.processing.org/organize/activity-guides/library/), a collection of activities for PCD events.*
+export const ACTIVITY_GUIDE_SUBMISSION_TEMPLATE = `*This post uses the submission template for the Processing Community Day [Zine Library](https://day.processing.org/organize/activity-guides/zine-library/), a collection of activities for PCD events.*
---
diff --git a/pcd-website/src/content/organizer-kit/activity-guides/making-a-zine.md b/pcd-website/src/content/organizer-kit/activity-guides/making-a-zine.md
index 0a8e3dd..dbdc7e5 100644
--- a/pcd-website/src/content/organizer-kit/activity-guides/making-a-zine.md
+++ b/pcd-website/src/content/organizer-kit/activity-guides/making-a-zine.md
@@ -31,4 +31,4 @@ The US Library of Congress has a [Zine Making Guide (PDF)](https://guides.loc.go
## Submit Your Zine
-Go to the [Zine Library](/organize/activity-guides/library/) to submit your zine and explore the full collection. By submitting an Activity Guide, you agree to publish your original contribution under the [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
+Go to the [Zine Library](/organize/activity-guides/zine-library/) to submit your zine and explore the full collection. By submitting an Activity Guide, you agree to publish your original contribution under the [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
diff --git a/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md b/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
index 38d55d4..e47a0e0 100644
--- a/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
+++ b/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
@@ -5,7 +5,9 @@ order: 1
description: Instructions for creating an activity guide for the PCD Zine Library.
---
-Activity Guides are community-created zines (see ["What is a Zine?"](/organize/activity-guides/making-a-zine/#what-is-a-zine)) that you can use to facilitate or take part in an activity at your Processing Community Day. They are designed to be taken “off the shelf,” so you do not need to be an expert on the topic to use one.
+Activity Guides are community-created resources that you can use to run or take part in a session at your Processing Community Day. An Activity Guide can be an online video, a workshop template, a project tutorial, a series of discussion questions, or any other kind of activity that can be completed in a group or individually. They are designed to be taken “off the shelf,” so you do not need to be an expert on the topic to use one.
+
+Some Activity Guides are published as zines and included in our community [zine library](/organize/activity-guides/zine-library/). We encourage you to print them out and use them at your Processing Community Day. If you would like to create a zine, see [Making a Zine](/organize/activity-guides/making-a-zine/).
## What should an Activity Guide include?
@@ -30,4 +32,4 @@ For a discussion or collaborative activity, you may also want to include:
- a list of discussion questions or prompts
- suggested group exercises or collaborative tasks
-There is no required visual format. Make it your own, but include enough information for someone unfamiliar with the activity or subject to facilitate it or complete it independently.
+There is no required format. Make it your own, but include enough information for someone unfamiliar with the activity or subject to facilitate it or complete it independently.
diff --git a/pcd-website/src/content/organizer-kit/activity-guides/library.md b/pcd-website/src/content/organizer-kit/activity-guides/zine-library.md
similarity index 100%
rename from pcd-website/src/content/organizer-kit/activity-guides/library.md
rename to pcd-website/src/content/organizer-kit/activity-guides/zine-library.md
diff --git a/pcd-website/src/content/organizer-kit/getting-started/minimum-viable-pcd.md b/pcd-website/src/content/organizer-kit/getting-started/minimum-viable-pcd.md
index 7d45da6..7a601d7 100644
--- a/pcd-website/src/content/organizer-kit/getting-started/minimum-viable-pcd.md
+++ b/pcd-website/src/content/organizer-kit/getting-started/minimum-viable-pcd.md
@@ -26,4 +26,4 @@ If you're feeling overwhelmed, ask yourself:
* Which spaces are available and how many people will fit there?
* What can I organize with the time and energy I have?
-If you need inspiration for activities to run at your PCD, check out the [Activity Guides Library](/organize/activity-guides/library/) which contains a collection of ready-to-run workshops and activities created by the community. You are also welcome to create your own activity guide and share it with the community. See [Contribute an Activity Guide](/organize/activity-guides/contribute-a-guide/) for more information.
+If you need inspiration for activities to run at your PCD, check out the [Activity Guides Library](/organize/activity-guides/zine-library/) which contains a collection of ready-to-run workshops and activities created by the community. You are also welcome to create your own activity guide and share it with the community. See [Contribute an Activity Guide](/organize/activity-guides/contribute-a-guide/) for more information.
diff --git a/pcd-website/src/pages/activity-guide/[id].astro b/pcd-website/src/pages/activity-guide/[id].astro
index 5756673..0a96f9f 100644
--- a/pcd-website/src/pages/activity-guide/[id].astro
+++ b/pcd-website/src/pages/activity-guide/[id].astro
@@ -19,7 +19,7 @@ const { Content } = await render(zine.entry);
-
+
Activity Guide
{zine.title}
{zine.cover
diff --git a/pcd-website/src/pages/organize/[...slug].astro b/pcd-website/src/pages/organize/[...slug].astro
index f05cbab..0495d0a 100644
--- a/pcd-website/src/pages/organize/[...slug].astro
+++ b/pcd-website/src/pages/organize/[...slug].astro
@@ -27,7 +27,7 @@ const prev = position > 0 ? ordered[position - 1] : undefined;
const next = position >= 0 && position < ordered.length - 1 ? ordered[position + 1] : undefined;
const editHref = `https://github.com/processing/processing-community-day/edit/main/pcd-website/src/content/organizer-kit/${entry.id}.md`;
-const SUBMIT_PAGES = new Set(['activity-guides/library', 'activity-guides/contribute-a-guide']);
+const SUBMIT_PAGES = new Set(['activity-guides/zine-library', 'activity-guides/contribute-a-guide']);
const markdownSuffix = SUBMIT_PAGES.has(entry.id)
? `\n\n[Submit an activity guide](${ACTIVITY_GUIDE_SUBMIT_URL})\n`
: '';
@@ -42,7 +42,7 @@ const markdown = `# ${entry.data.title}\n\n${entry.body?.trim() ?? ''}\n${markdo
markdown={markdown}
>
- {entry.id === 'activity-guides/library' && }
+ {entry.id === 'activity-guides/zine-library' && }
{entry.id === 'activity-guides/contribute-a-guide' && }
↑ Back to top
From 232b9b915c87e12181bd0ac7da0152dd214d3a6f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20de=20Courville?=
Date: Thu, 27 Aug 2026 20:14:19 +0200
Subject: [PATCH 13/15] Update making-a-zine.md
---
.../src/content/organizer-kit/activity-guides/making-a-zine.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pcd-website/src/content/organizer-kit/activity-guides/making-a-zine.md b/pcd-website/src/content/organizer-kit/activity-guides/making-a-zine.md
index dbdc7e5..1d18f73 100644
--- a/pcd-website/src/content/organizer-kit/activity-guides/making-a-zine.md
+++ b/pcd-website/src/content/organizer-kit/activity-guides/making-a-zine.md
@@ -31,4 +31,4 @@ The US Library of Congress has a [Zine Making Guide (PDF)](https://guides.loc.go
## Submit Your Zine
-Go to the [Zine Library](/organize/activity-guides/zine-library/) to submit your zine and explore the full collection. By submitting an Activity Guide, you agree to publish your original contribution under the [CC BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
+Go to the [Zine Library](/organize/activity-guides/zine-library/) to submit your zine and explore the full collection.
\ No newline at end of file
From 6b0a5e1c8530d34b17eb5468c0d5edbeaa89ada2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20de=20Courville?=
Date: Thu, 27 Aug 2026 20:48:57 +0200
Subject: [PATCH 14/15] Hide TOC for single-section docs pages
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Only render the docs page table of contents when there is more than one h2 heading. This avoids showing an unnecessary “On this page” block on pages with a single section.
---
pcd-website/src/components/DocsTableOfContents.astro | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pcd-website/src/components/DocsTableOfContents.astro b/pcd-website/src/components/DocsTableOfContents.astro
index 923db9f..f414092 100644
--- a/pcd-website/src/components/DocsTableOfContents.astro
+++ b/pcd-website/src/components/DocsTableOfContents.astro
@@ -10,7 +10,7 @@ const { headings } = Astro.props;
const anchors = headings.filter((heading) => heading.depth === 2);
---
-{anchors.length > 0 && (
+{anchors.length > 1 && (
On this page
From 09ecc6f866704ff1b3e74890dc438c8e6039c139 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20de=20Courville?=
Date: Thu, 27 Aug 2026 20:54:30 +0200
Subject: [PATCH 15/15] Update what-is-a-guide.md
---
.../content/organizer-kit/activity-guides/what-is-a-guide.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md b/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
index e47a0e0..532d1dd 100644
--- a/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
+++ b/pcd-website/src/content/organizer-kit/activity-guides/what-is-a-guide.md
@@ -7,7 +7,7 @@ description: Instructions for creating an activity guide for the PCD Zine Librar
Activity Guides are community-created resources that you can use to run or take part in a session at your Processing Community Day. An Activity Guide can be an online video, a workshop template, a project tutorial, a series of discussion questions, or any other kind of activity that can be completed in a group or individually. They are designed to be taken “off the shelf,” so you do not need to be an expert on the topic to use one.
-Some Activity Guides are published as zines and included in our community [zine library](/organize/activity-guides/zine-library/). We encourage you to print them out and use them at your Processing Community Day. If you would like to create a zine, see [Making a Zine](/organize/activity-guides/making-a-zine/).
+Some Activity Guides are published as zines and included in our community [zine library](/organize/activity-guides/zine-library/). We encourage you to print them out and use them at your Processing Community Day. And if you would like to contribute a zine, see [Making a Zine](/organize/activity-guides/making-a-zine/).
## What should an Activity Guide include?