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}

    + + + + + + +

    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]+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]*>519 kB\s*
  • \s*]*>Zine Making KitLoops<\/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(/