Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
583231a
Zine library initial implementation
SableRaf Aug 24, 2026
7baf28a
Rework Activity Guide organizer pages
SableRaf Aug 25, 2026
4528168
Reorganize activity guide docs with zine resources
SableRaf Aug 25, 2026
d9087cb
Update what-is-a-guide.md
SableRaf Aug 25, 2026
64b2d00
Update link template for zine submission
SableRaf Aug 25, 2026
339cc50
Refactor zine library to ordered card model
SableRaf Aug 25, 2026
1147cfa
Add topic-specific zine submit links
SableRaf Aug 25, 2026
9976ca2
Rename activity guide library to zine library
SableRaf Aug 27, 2026
e98b5b3
Split the zine making instructions to a new page
SableRaf Aug 27, 2026
ce175d6
Restructure organizer kit sections
SableRaf Aug 27, 2026
447a82e
Add dev CARTO key support to tile URLs for local dev
SableRaf Aug 27, 2026
441d716
Rename library to zine-library
SableRaf Aug 27, 2026
232b9b9
Update making-a-zine.md
SableRaf Aug 27, 2026
6b0a5e1
Hide TOC for single-section docs pages
SableRaf Aug 27, 2026
09ecc6f
Update what-is-a-guide.md
SableRaf Aug 27, 2026
8bc14f9
Add zine cover image metadata, rendering, and alt text
SableRaf Sep 1, 2026
a9019e8
Remove placehoder zines and adjust design for "submit a zine"
SableRaf Sep 1, 2026
c1c6944
Add zine submission intake via GitHub Issues instead of forum post
SableRaf Sep 1, 2026
e475d31
Add zine folding guide image and caption
SableRaf Sep 1, 2026
6320a31
Update making-a-zine.md
SableRaf Sep 1, 2026
2403ead
Update making-a-zine.md
SableRaf Sep 1, 2026
a320868
Remove extra steps on what to include
SableRaf Sep 1, 2026
1b02077
Simplify zine library cards
SableRaf Sep 1, 2026
bcd1f63
Rename activity guide components to zines
SableRaf Sep 1, 2026
ddb3ed3
Support tags and creator links for zines
SableRaf Sep 1, 2026
1971eb0
Redesign activity guide details
SableRaf Sep 1, 2026
4a0698a
Improve back navigation links
SableRaf Sep 1, 2026
459ab6a
Clean up zine library copy
SableRaf Sep 1, 2026
a4b1c2f
Add QR code icon asset
SableRaf Sep 1, 2026
2da0d34
Add docs share actions and QR menu
SableRaf Sep 1, 2026
08665dd
Merge branch 'main' into activity-guide-library
SableRaf Sep 1, 2026
3af34d9
Change permalink icon to link icon
SableRaf Sep 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions .github/ISSUE_TEMPLATE/05-new-zine.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
name: "📚 New Zine"
description: Submit an Activity Guide zine for the PCD Zine Library.
title: "New Zine: "
labels:
- new zine
- needs review
body:
- type: markdown
attributes:
value: |
<!-- new-zine-template -->
Everything in this issue, including the PDF links and notes, is public. Do not include private or sensitive information. We stage accepted submissions for maintainer review before publishing them.
- type: input
id: title
attributes:
label: Title
description: The public title of your activity guide.
validations:
required: true
- type: input
id: topic
attributes:
label: Topic
description: The subject or theme of the guide.
validations:
required: true
- type: input
id: tags
attributes:
label: Tags
description: Optional comma-separated tags that help describe the guide.
placeholder: beginner, p5.js, physical computing
- type: input
id: creators
attributes:
label: Creator(s)
description: The person or group to credit.
validations:
required: true
- type: input
id: creator_url
attributes:
label: Creator URL
description: Optional public website for the credited person or group.
placeholder: https://example.org/
- type: textarea
id: summary
attributes:
label: Short summary
description: A concise description for the library card.
validations:
required: true
- type: textarea
id: description
attributes:
label: Full description
description: Public description shown on the guide page. Markdown is supported.
validations:
required: true
- type: input
id: reader_order_pdf
attributes:
label: Reader-order PDF URL
description: A PDF whose reading order follows the content for accessible reading.
placeholder: https://example.org/my-guide-reader-order.pdf
validations:
required: true
- type: input
id: print_ready_pdf
attributes:
label: Print-ready PDF URL
description: A PDF prepared for printing, folding, or other physical distribution.
placeholder: https://example.org/my-guide-print-ready.pdf
validations:
required: true
- type: input
id: activity_format
attributes:
label: Activity format
description: Optional, for example workshop, discussion, or creative exercise.
- type: input
id: duration
attributes:
label: Duration
description: Optional, for example 45 minutes or 2 hours.
- type: textarea
id: materials
attributes:
label: Materials
description: Optional materials participants need.
- type: input
id: attribution
attributes:
label: Preferred attribution
description: Optional attribution wording.
- type: textarea
id: maintainer_notes
attributes:
label: Maintainer notes
description: Optional notes for maintainers. These notes are public.
- type: checkboxes
id: license_consent
attributes:
label: License consent
options:
- label: I own or have permission to license this material under CC BY-SA 4.0.
required: true
- type: checkboxes
id: link_consent
attributes:
label: Public, stable PDF links
description: Both links must work without login and must not be short-lived or expiring.
options:
- label: I confirm both PDF links are publicly accessible without login and are stable, non-expiring links.
required: true
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"id": "zine-integration-test-fixture",
"title": "Loops with Shapes",
"topic": "Loops",
"tags": ["beginner", "p5.js"],
"created_by": "Integration Test Author",
"attribution": "Integration Test Author, CC BY-SA 4.0",
"format": "workshop",
"duration": "2 hours",
"materials": "Laptop and p5.js editor",
"summary": "A compact guide to making patterns with repeated shapes.",
"cover": {
"src": "cover.png",
"alt": "Loops with Shapes zine cover"
},
"pdfs": [
{ "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"
}
135 changes: 135 additions & 0 deletions .github/scripts/process-new-zine-issue.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { isValidHttpUrl, parseIssueSections, required, slugify } from './event-issue-helpers.mjs';
import { ZINE_TEMPLATE_HEADING, hasCheckedConsent, makeDraftMarkdown, makeZinePrBody, zineValidationComment } from './zine-intake-helpers.mjs';

const WORKSPACE = process.cwd();
const RUNNER_TEMP = process.env.RUNNER_TEMP ?? path.join(WORKSPACE, '.tmp');
const OUTPUT_PATH = process.env.GITHUB_OUTPUT;
const payload = JSON.parse(await fs.readFile(process.env.GITHUB_EVENT_PATH, 'utf8'));
const issue = payload.issue;
const issueNumber = issue.number;
const issueBody = issue.body ?? '';

async function setOutput(key, value) {
if (OUTPUT_PATH) await fs.appendFile(OUTPUT_PATH, `${key}=${String(value)}\n`);
}

function issueUrl() {
if (issue.html_url) return issue.html_url;
const repository = payload.repository?.full_name ?? process.env.GITHUB_REPOSITORY;
return repository ? `${process.env.GITHUB_SERVER_URL ?? 'https://github.com'}/${repository}/issues/${issueNumber}` : `issue #${issueNumber}`;
}

async function folderExists(folder) {
try { await fs.access(folder); return true; } catch { return false; }
}

async function main() {
await fs.mkdir(RUNNER_TEMP, { recursive: true });
if (!issueBody.includes(ZINE_TEMPLATE_HEADING)) {
console.log('[process-new-zine-issue] template heading not found — skipping');
await setOutput('valid', 'skip');
return;
}

const fields = parseIssueSections(issueBody);
const errors = [];
const title = required(fields, 'Title', errors);
const topic = required(fields, 'Topic', errors);
const tags = [...new Set((fields.get('Tags') ?? '')
.split(',')
.map((tag) => tag.trim())
.filter(Boolean))];
const createdBy = required(fields, 'Creator(s)', errors);
const createdByUrl = fields.get('Creator URL')?.trim() ?? '';
if (createdByUrl && !isValidHttpUrl(createdByUrl)) {
errors.push({ field: 'Creator URL', found: createdByUrl, message: 'Enter a valid HTTP(S) URL.' });
}
const summary = required(fields, 'Short summary', errors);
const description = required(fields, 'Full description', errors);
const readerOrderUrl = fields.get('Reader-order PDF URL')?.trim() ?? '';
const printReadyUrl = fields.get('Print-ready PDF URL')?.trim() ?? '';
if (!readerOrderUrl) errors.push({ field: 'Reader-order PDF URL', message: 'This field is required.' });
else if (!isValidHttpUrl(readerOrderUrl)) errors.push({ field: 'Reader-order PDF URL', found: readerOrderUrl, message: 'Enter a valid HTTP(S) URL.' });
if (!printReadyUrl) errors.push({ field: 'Print-ready PDF URL', message: 'This field is required.' });
else if (!isValidHttpUrl(printReadyUrl)) errors.push({ field: 'Print-ready PDF URL', found: printReadyUrl, message: 'Enter a valid HTTP(S) URL.' });
if (!hasCheckedConsent(fields.get('License consent'))) errors.push({ field: 'License consent', message: 'Confirm that the material can be licensed under CC BY-SA 4.0.' });
if (!hasCheckedConsent(fields.get('Public, stable PDF links'))) errors.push({ field: 'Public, stable PDF links', message: 'Confirm that both links are public, stable, and non-expiring.' });

const slug = slugify(title);
if (!slug) errors.push({ field: 'Title', message: 'Use at least one letter or number so we can create a URL slug.' });
const publishedDir = path.join(WORKSPACE, 'pcd-website/src/content/zines', slug);
const draftDir = path.join(WORKSPACE, 'pcd-website/src/content/zines-drafts', slug);
const reservedDraftSlugs = new Set((process.env.ZINE_RESERVED_DRAFT_SLUGS ?? '').split(',').filter(Boolean));
if (slug && reservedDraftSlugs.has(slug)) {
errors.push({ field: 'Title', found: title, message: `The generated slug \`${slug}\` is already staged by another open zine review PR.` });
} else if (slug && await folderExists(publishedDir)) {
errors.push({ field: 'Title', found: title, message: `The generated slug \`${slug}\` already exists in published or draft zines.` });
} else if (slug && await folderExists(draftDir)) {
let existingIssueNumber;
try {
existingIssueNumber = JSON.parse(await fs.readFile(path.join(draftDir, 'submission.json'), 'utf8')).intake?.issue_number;
} catch {
// A malformed draft must not be overwritten by an unrelated submission.
}
if (existingIssueNumber !== issueNumber) {
errors.push({ field: 'Title', found: title, message: `The generated slug \`${slug}\` already exists in published or draft zines.` });
}
}

if (errors.length) {
const validationCommentPath = path.join(RUNNER_TEMP, `zine-validation-${issueNumber}.md`);
await fs.writeFile(validationCommentPath, zineValidationComment(errors));
await setOutput('valid', 'false');
await setOutput('validation_comment_path', validationCommentPath);
return;
}

const submission = {
id: slug,
title,
topic,
created_by: createdBy,
summary,
source_pdfs: [
{ role: 'reader-order', url: readerOrderUrl },
{ role: 'print-ready', url: printReadyUrl },
],
license: 'CC BY-SA 4.0',
source_issue_url: issueUrl(),
intake: {
issue_number: issueNumber,
submitted_by_github: issue.user?.login ?? '',
submitted_date: new Date().toISOString().slice(0, 10),
maintainer_notes: fields.get('Maintainer notes')?.trim() ?? '',
},
description,
};
if (tags.length) submission.tags = tags;
if (createdByUrl) submission.created_by_url = createdByUrl;
for (const [field, key] of [['Activity format', 'format'], ['Duration', 'duration'], ['Materials', 'materials'], ['Preferred attribution', 'attribution']]) {
const value = fields.get(field)?.trim();
if (value) submission[key] = value;
}
await fs.mkdir(draftDir, { recursive: true });
await fs.writeFile(path.join(draftDir, 'submission.json'), `${JSON.stringify(submission, null, 2)}\n`);
await fs.writeFile(path.join(draftDir, 'index.md'), makeDraftMarkdown(slug, description));
const prBodyPath = path.join(RUNNER_TEMP, `zine-pr-body-${issueNumber}.md`);
await fs.writeFile(prBodyPath, makeZinePrBody({ issueNumber, title, submitterLogin: issue.user?.login ?? '', submission }));
await setOutput('valid', 'true');
await setOutput('branch', `automation/new-zine-${issueNumber}`);
await setOutput('commit_message', `Stage ${title} zine from issue #${issueNumber}`);
await setOutput('pr_title', `Review zine: ${title}`);
await setOutput('pr_body_path', prBodyPath);
await setOutput('zine_title', title);
}

await main().catch(async (error) => {
console.error('[process-new-zine-issue] unhandled error:', error);
const validationCommentPath = path.join(RUNNER_TEMP, `zine-validation-${issueNumber}.md`);
await fs.writeFile(validationCommentPath, 'An unexpected error occurred while processing this zine submission. Please edit and save the issue to try again.');
await setOutput('valid', 'false');
await setOutput('validation_comment_path', validationCommentPath);
process.exitCode = 1;
});
Loading