From 4916cc691a23fa8b586fcba50bb1bd6e441e52c3 Mon Sep 17 00:00:00 2001 From: Richard Littauer Date: Sat, 23 May 2026 16:51:41 +1200 Subject: [PATCH 1/2] Add per-author pages via mkdocs hook (refs #89) Introduce a central authors.yml at the repo root and a small mkdocs hook (.config/hooks/authors.py) that, on every build: - Loads authors.yml (slug -> {name, orcid, affiliation}). - Walks pattern frontmatter for `authors:` (a list of slugs) and builds a reverse slug -> [patterns] map; fails the build with a PluginError if a pattern references a slug missing from authors.yml. - Emits one generated File per author at authors/.md plus an authors/index.md listing all authors alphabetically. - In on_page_markdown, replaces the bullet list under `## Contributors & Acknowledgement` with chips linking to the author pages, preserving any prose that follows (e.g. AI-use disclaimers). - Leaves patterns without an `authors:` frontmatter entry untouched so the remaining ~46 patterns continue to render their handwritten lists during gradual migration. Config wiring: - mkdocs.yml: register hooks/authors.py, add `Authors: authors/index.md` to nav, and exclude /authors.yml from the docs tree. Template + contributor docs: - PATTERN-TEMPLATE.md: add commented `authors:` block in frontmatter and rewrite the Contributors section to explain the new auto-generation. - CONTRIBUTING.md: replace the "add your name and ORCID at the bottom" instruction with the two-step authors.yml + frontmatter flow. Example migrations (exercise both code paths in the hook): - embed-wellbeing-into-student-hackathons.md: 6 authors, no trailing prose. - onboarding-graduate-leads-for-open-source-internship-programs.md: 4 authors followed by an "A note on AI use" paragraph that the hook must preserve. Verified with `mkdocs build --strict --site-dir /tmp/patterns_site`: clean build, all 9 author pages generated, both migrated patterns render chips correctly, AI-use paragraph preserved, non-migrated pattern unchanged, and a negative test with an unknown slug aborts the build with a clear error message. --- .config/hooks/authors.py | 230 ++++++++++++++++++ .config/mkdocs.yml | 4 + CONTRIBUTING.md | 34 ++- PATTERN-TEMPLATE.md | 14 +- authors.yml | 53 ++++ embed-wellbeing-into-student-hackathons.md | 14 +- ...ads-for-open-source-internship-programs.md | 12 +- 7 files changed, 346 insertions(+), 15 deletions(-) create mode 100644 .config/hooks/authors.py create mode 100644 authors.yml diff --git a/.config/hooks/authors.py b/.config/hooks/authors.py new file mode 100644 index 0000000..97351db --- /dev/null +++ b/.config/hooks/authors.py @@ -0,0 +1,230 @@ +"""Authors hook for the CURIOSS patterns mkdocs site. + +Reads ``authors.yml`` (slug -> {name, orcid, affiliation}) and per-pattern +``authors:`` frontmatter (list of slugs), then: + +* Generates one page per author at ``authors/.md``. +* Generates an index page at ``authors/index.md``. +* Rewrites the ``## Contributors & Acknowledgement`` section in each pattern, + replacing the legacy bullet list with links to the per-author pages while + preserving any prose that follows the list. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import yaml +from mkdocs.exceptions import PluginError +from mkdocs.structure.files import File + +AUTHORS_FILE = "authors.yml" +CONTRIB_HEADING_RE = re.compile( + r"^##\s+Contributors\s*&\s*Acknowledg\w*\s*$", + re.MULTILINE, +) +BULLET_LINE_RE = re.compile(r"^\s*[-*]\s+\S") +HEADING_RE = re.compile(r"^#+\s", re.MULTILINE) + + +def _load_authors(docs_dir: str) -> dict: + path = Path(docs_dir) / AUTHORS_FILE + if not path.exists(): + raise PluginError(f"authors hook: {AUTHORS_FILE} not found at {path}") + data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} + if not isinstance(data, dict): + raise PluginError( + f"authors hook: {AUTHORS_FILE} must be a YAML mapping keyed by slug" + ) + return data + + +def _parse_frontmatter(text: str) -> tuple[dict, str]: + if not text.startswith("---"): + return {}, text + end = text.find("\n---", 3) + if end == -1: + return {}, text + fm_text = text[3:end].lstrip("\n") + body = text[end + 4 :].lstrip("\n") + try: + fm = yaml.safe_load(fm_text) or {} + except yaml.YAMLError: + return {}, text + return (fm if isinstance(fm, dict) else {}), body + + +def _read_authors_for_file(file: File) -> list[str]: + if not file.abs_src_path or not file.src_path.endswith(".md"): + return [] + try: + text = Path(file.abs_src_path).read_text(encoding="utf-8") + except OSError: + return [] + fm, _ = _parse_frontmatter(text) + raw = fm.get("authors") or [] + if not isinstance(raw, list): + return [] + return [s for s in raw if isinstance(s, str)] + + +def _pattern_title(file: File) -> str: + try: + text = Path(file.abs_src_path).read_text(encoding="utf-8") + except OSError: + return Path(file.src_path).stem + _, body = _parse_frontmatter(text) + for line in body.splitlines(): + if line.startswith("# "): + return line[2:].strip() + return Path(file.src_path).stem.replace("-", " ").title() + + +def _render_author_page(slug: str, record: dict, patterns: list[tuple[str, str]]) -> str: + name = record.get("name", slug) + affiliation = record.get("affiliation") + orcid = record.get("orcid") + + lines = [f"# {name}", ""] + if affiliation: + lines += [f"**Affiliation:** {affiliation}", ""] + if orcid: + lines += [f"**ORCID:** [{orcid}](https://orcid.org/{orcid})", ""] + + lines += ["## Patterns", ""] + if patterns: + for src_path, title in sorted(patterns, key=lambda p: p[1].lower()): + lines.append(f"- [{title}](../{src_path})") + else: + lines.append("_No patterns yet._") + lines.append("") + return "\n".join(lines) + + +def _render_authors_index(authors_map: dict) -> str: + lines = [ + "# Authors", + "", + "Everyone who has contributed to a CURIOSS pattern. " + "Click a name to see their patterns.", + "", + ] + for slug, record in sorted( + authors_map.items(), key=lambda kv: kv[1].get("name", kv[0]).lower() + ): + name = record.get("name", slug) + aff = record.get("affiliation") + line = f"- [{name}]({slug}.md)" + if aff: + line += f" — {aff}" + lines.append(line) + lines.append("") + return "\n".join(lines) + + +def _render_pattern_bullets(slugs: list[str], authors_map: dict) -> str: + bullets = [] + for slug in slugs: + record = authors_map.get(slug, {}) + name = record.get("name", slug) + aff = record.get("affiliation") + orcid = record.get("orcid") + line = f"- [{name}](authors/{slug}.md)" + extras = [] + if aff: + extras.append(aff) + if orcid: + extras.append(f"[ORCID](https://orcid.org/{orcid})") + if extras: + line += " — " + ", ".join(extras) + bullets.append(line) + return "\n".join(bullets) + + +def on_files(files, config): + authors_map = _load_authors(config["docs_dir"]) + + by_author: dict[str, list[tuple[str, str]]] = {slug: [] for slug in authors_map} + + for f in list(files): + if not f.is_documentation_page(): + continue + if f.src_path.startswith("authors/"): + continue + slugs = _read_authors_for_file(f) + if not slugs: + continue + title = _pattern_title(f) + for slug in slugs: + if slug not in authors_map: + raise PluginError( + f"authors hook: pattern '{f.src_path}' references unknown " + f"author slug '{slug}'. Add an entry to {AUTHORS_FILE} or " + f"fix the slug." + ) + by_author[slug].append((f.src_path, title)) + + for slug, record in authors_map.items(): + content = _render_author_page(slug, record, by_author.get(slug, [])) + files.append(File.generated(config, f"authors/{slug}.md", content=content)) + + files.append( + File.generated( + config, "authors/index.md", content=_render_authors_index(authors_map) + ) + ) + + config["_authors_map"] = authors_map + return files + + +def on_page_markdown(markdown, page, config, files): + authors_map = config.get("_authors_map") or {} + + if page.file.src_path.startswith("authors/"): + return markdown + + fm_authors = (page.meta or {}).get("authors") or [] + if not isinstance(fm_authors, list) or not fm_authors: + return markdown + + rendered = _render_pattern_bullets(fm_authors, authors_map) + + m = CONTRIB_HEADING_RE.search(markdown) + if not m: + sep = "" if markdown.endswith("\n") else "\n" + return ( + markdown + + f"{sep}\n## Contributors & Acknowledgement\n\n{rendered}\n" + ) + + heading_end = m.end() + next_h = HEADING_RE.search(markdown, heading_end + 1) + section_end = next_h.start() if next_h else len(markdown) + section_body = markdown[heading_end:section_end] + + body_lines = section_body.splitlines(keepends=True) + i = 0 + while i < len(body_lines) and body_lines[i].strip() == "": + i += 1 + while i < len(body_lines): + line = body_lines[i] + if BULLET_LINE_RE.match(line): + i += 1 + continue + if line.strip() == "" and i + 1 < len(body_lines) and BULLET_LINE_RE.match( + body_lines[i + 1] + ): + i += 1 + continue + break + trailing = "".join(body_lines[i:]) + + rebuilt = markdown[:heading_end] + "\n\n" + rendered + "\n" + if trailing.strip(): + if not trailing.startswith("\n"): + rebuilt += "\n" + rebuilt += trailing + rebuilt += markdown[section_end:] + return rebuilt diff --git a/.config/mkdocs.yml b/.config/mkdocs.yml index 86cea70..621f21f 100644 --- a/.config/mkdocs.yml +++ b/.config/mkdocs.yml @@ -5,6 +5,7 @@ nav: # This adds a link in the navigation bar on the left - Home: README.md - Template: PATTERN-TEMPLATE.md + - Authors: authors/index.md # Associated Git Repo Config repo_url: https://github.com/CURIOSSorg/curioss-patterns @@ -18,6 +19,7 @@ docs_dir: "../" exclude_docs: | /node_modules/ /site/ + /authors.yml # Theme Configuration # Colour options: red, pink, purple, deep purple, indigo, blue, light blue, @@ -73,6 +75,8 @@ markdown_extensions: plugins: - search - tags +hooks: + - hooks/authors.py extra: tags: # Set a tag with a identifier so then an icon can be set on the theme. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab988d5..bcd314d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,39 @@ Thanks for wanting to contribute! Open a PR or an issue for anything. ## When contributing -- Add your name and ORCID at the bottom of any pattern you edit! +When you contribute to a pattern, add yourself as an author so you get credit +on the website (and get your own author page listing every pattern you've +worked on). + +There are two short steps: + +1. **Add yourself to [`authors.yml`](https://github.com/CURIOSSorg/curioss-patterns/blob/main/authors.yml)** (at the root of this repo) + if you're not already there. Pick a short slug — usually + `firstname-lastname` — and fill in your name, ORCID (optional), and + affiliation (optional). For example: + + ```yaml + jane-doe: + name: Jane Doe + orcid: 0000-0001-2345-6789 + affiliation: Example University + ``` + +2. **Add your slug to the pattern's `authors:` frontmatter** at the top of the + pattern file. For example: + + ```yaml + --- + tags: + - Community Building + authors: + - jane-doe + --- + ``` + +You do **not** need to edit the "Contributors & Acknowledgement" section at +the bottom of the pattern — the site generates that automatically from the +frontmatter. ## Local Development diff --git a/PATTERN-TEMPLATE.md b/PATTERN-TEMPLATE.md index d879e96..ece63cf 100644 --- a/PATTERN-TEMPLATE.md +++ b/PATTERN-TEMPLATE.md @@ -15,6 +15,11 @@ tags: #- Rewards & Recognition #- Tools & Infrastructure #- Working with Tech Transfer / External Partners +authors: + # List the slug of every contributor, one per line. Slugs come from `authors.yml` + # at the root of this repo. If you're not in `authors.yml` yet, add yourself + # there first, then list your slug below. + #- your-slug-here --- # Pattern Name @@ -66,4 +71,11 @@ List resources or related patterns for further reading. ## Contributors & Acknowledgement -Recognize individuals or organizations that contributed to this pattern. +The list of contributors is generated automatically from the `authors:` field +in the frontmatter at the top of this file. You don't need to write the names +here yourself — just add your slug to the frontmatter and the site will fill +this section in. + +If you want to add extra acknowledgements (for example, a note about AI +assistance, or thanks to people who contributed without an ORCID), add them +as paragraphs below this line and they'll be preserved. diff --git a/authors.yml b/authors.yml new file mode 100644 index 0000000..062d47d --- /dev/null +++ b/authors.yml @@ -0,0 +1,53 @@ +# Authors of CURIOSS patterns. +# +# Each entry is keyed by a short slug (kebab-case, usually firstname-lastname). +# When you contribute to a pattern, add yourself here (if you're not listed yet), +# then put your slug in that pattern's `authors:` frontmatter list. +# +# Fields: +# name (required) Your full name as you'd like it shown. +# orcid (optional) Your ORCID iD, just the digits like 0000-0001-2345-6789. +# affiliation (optional) Your institution or organisation. + +angela-newell: + name: Angela Newell + affiliation: University of Texas at Austin + +ciara-flanagan: + name: Ciara Flanagan + orcid: 0009-0005-3153-7673 + affiliation: CURIOSS + +daniel-shown: + name: Daniel Shown + orcid: 0009-0002-5716-8835 + affiliation: Saint Louis University + +david-lippert: + name: David Lippert + orcid: 0009-0003-6444-9595 + affiliation: George Washington University + +emily-lovell: + name: Emily Lovell + orcid: 0000-0001-5531-5956 + affiliation: University of California Santa Cruz + +jeffrey-young: + name: Jeffrey Young + orcid: 0000-0001-9841-4057 + affiliation: OSPO@GT + +kendall-fortney: + name: Kendall Fortney + orcid: 0009-0006-3898-0771 + affiliation: University of Vermont + +laura-langdon: + name: Laura Langdon + affiliation: University of California, Davis + +tom-hughes: + name: Tom Hughes + orcid: 0009-0008-7516-3687 + affiliation: Carnegie Mellon University diff --git a/embed-wellbeing-into-student-hackathons.md b/embed-wellbeing-into-student-hackathons.md index a82a927..18235c1 100644 --- a/embed-wellbeing-into-student-hackathons.md +++ b/embed-wellbeing-into-student-hackathons.md @@ -3,6 +3,13 @@ tags: - Community Building - Education & Skills - Promoting Best Practices +authors: + - angela-newell + - ciara-flanagan + - david-lippert + - emily-lovell + - laura-langdon + - tom-hughes --- # Embed wellbeing into Student Hackathons @@ -113,10 +120,3 @@ Every team has a mentor - [Lower the barriers to entry for Student Hackathons](lower-the-barriers-to-entry-for-student-hackathons.md) ## Contributors & Acknowledgement - -- Angela Newell, University of Texas at Austin -- Ciara Flanagan, -- David Lippert, George Washington University, -- Emily Lovell, University of California Santa Cruz, -- Laura Langdon, University of California, Davis -- Tom Hughes, Carnegie Mellon University, diff --git a/onboarding-graduate-leads-for-open-source-internship-programs.md b/onboarding-graduate-leads-for-open-source-internship-programs.md index 6fbdf1d..2b6360b 100644 --- a/onboarding-graduate-leads-for-open-source-internship-programs.md +++ b/onboarding-graduate-leads-for-open-source-internship-programs.md @@ -2,12 +2,17 @@ tags: - Education & Skills - Promoting Best Practices +authors: + - ciara-flanagan + - daniel-shown + - kendall-fortney + - jeffrey-young --- # Onboarding Graduate Leads for Open Source Internship Programs ## Pattern Summary -Establish a structured onboarding process for graduate students taking on team lead roles in open source internship programs. +Establish a structured onboarding process for gradluate students taking on team lead roles in open source internship programs. ## Problem / Challenge @@ -133,9 +138,4 @@ A key insight from our experience is the importance of creating spaces where lea ## Contributors & Acknowledgement -- Ciara Flanagan (CURIOSS),() -- Daniel Shown (Saint Louis University), -- Kendall Fortney (University of Vermont), -- Jeffrey Young (OSPO@GT), - A note on AI use: In addition to working from Deep Dive transcripts, capturing learning from our community discussions and other patterns from our members, this pattern was drafted with the help of AI. As a small organization, tools like this help us turn rich conversations into written resources without losing the ideas along the way. As always, there were plenty of human eyes reviewing, editing and improving the content before this pattern made it to publication. Thanks go to our community for the insights. If you do spot any errors, please let us know so we can correct them! From 1472ef01a7ebf09f4f2368729cc760acbc3cfb7e Mon Sep 17 00:00:00 2001 From: Richard Littauer Date: Wed, 27 May 2026 12:38:23 +1200 Subject: [PATCH 2/2] Replace per-author pages with a single authors page (refs #89) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hook now generates only authors/index.md — a roster table plus one anchored section per author with their patterns — instead of one page per author. Pattern Contributors bullets link to authors/index.md# anchors. Enables attr_list so { #slug } heading IDs are stable. --- .config/hooks/authors.py | 75 ++++++++++++++++++++-------------------- .config/mkdocs.yml | 1 + CONTRIBUTING.md | 4 +-- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/.config/hooks/authors.py b/.config/hooks/authors.py index 97351db..caea62d 100644 --- a/.config/hooks/authors.py +++ b/.config/hooks/authors.py @@ -3,11 +3,11 @@ Reads ``authors.yml`` (slug -> {name, orcid, affiliation}) and per-pattern ``authors:`` frontmatter (list of slugs), then: -* Generates one page per author at ``authors/.md``. -* Generates an index page at ``authors/index.md``. +* Generates a single ``authors/index.md`` page with a roster table and one + anchored section per author listing the patterns they've contributed to. * Rewrites the ``## Contributors & Acknowledgement`` section in each pattern, - replacing the legacy bullet list with links to the per-author pages while - preserving any prose that follows the list. + replacing the legacy bullet list with links to anchors on the authors page + while preserving any prose that follows the list. """ from __future__ import annotations @@ -81,44 +81,45 @@ def _pattern_title(file: File) -> str: return Path(file.src_path).stem.replace("-", " ").title() -def _render_author_page(slug: str, record: dict, patterns: list[tuple[str, str]]) -> str: - name = record.get("name", slug) - affiliation = record.get("affiliation") - orcid = record.get("orcid") - - lines = [f"# {name}", ""] - if affiliation: - lines += [f"**Affiliation:** {affiliation}", ""] - if orcid: - lines += [f"**ORCID:** [{orcid}](https://orcid.org/{orcid})", ""] - - lines += ["## Patterns", ""] - if patterns: - for src_path, title in sorted(patterns, key=lambda p: p[1].lower()): - lines.append(f"- [{title}](../{src_path})") - else: - lines.append("_No patterns yet._") - lines.append("") - return "\n".join(lines) - +def _render_authors_index( + authors_map: dict, by_author: dict[str, list[tuple[str, str]]] +) -> str: + sorted_authors = sorted( + authors_map.items(), key=lambda kv: kv[1].get("name", kv[0]).lower() + ) -def _render_authors_index(authors_map: dict) -> str: lines = [ "# Authors", "", "Everyone who has contributed to a CURIOSS pattern. " - "Click a name to see their patterns.", + "Click a name to jump to their entry below.", "", + "| Author | Affiliation | ORCID |", + "| --- | --- | --- |", ] - for slug, record in sorted( - authors_map.items(), key=lambda kv: kv[1].get("name", kv[0]).lower() - ): + for slug, record in sorted_authors: + name = record.get("name", slug) + aff = record.get("affiliation") or "" + orcid = record.get("orcid") + orcid_cell = f"[{orcid}](https://orcid.org/{orcid})" if orcid else "" + lines.append(f"| [{name}](#{slug}) | {aff} | {orcid_cell} |") + + for slug, record in sorted_authors: name = record.get("name", slug) aff = record.get("affiliation") - line = f"- [{name}]({slug}.md)" + orcid = record.get("orcid") + lines += ["", f"## {name} {{ #{slug} }}", ""] if aff: - line += f" — {aff}" - lines.append(line) + lines += [f"**Affiliation:** {aff}", ""] + if orcid: + lines += [f"**ORCID:** [{orcid}](https://orcid.org/{orcid})", ""] + lines += ["**Patterns:**", ""] + patterns = by_author.get(slug, []) + if patterns: + for src_path, title in sorted(patterns, key=lambda p: p[1].lower()): + lines.append(f"- [{title}](../{src_path})") + else: + lines.append("_No patterns yet._") lines.append("") return "\n".join(lines) @@ -130,7 +131,7 @@ def _render_pattern_bullets(slugs: list[str], authors_map: dict) -> str: name = record.get("name", slug) aff = record.get("affiliation") orcid = record.get("orcid") - line = f"- [{name}](authors/{slug}.md)" + line = f"- [{name}](authors/index.md#{slug})" extras = [] if aff: extras.append(aff) @@ -165,13 +166,11 @@ def on_files(files, config): ) by_author[slug].append((f.src_path, title)) - for slug, record in authors_map.items(): - content = _render_author_page(slug, record, by_author.get(slug, [])) - files.append(File.generated(config, f"authors/{slug}.md", content=content)) - files.append( File.generated( - config, "authors/index.md", content=_render_authors_index(authors_map) + config, + "authors/index.md", + content=_render_authors_index(authors_map, by_author), ) ) diff --git a/.config/mkdocs.yml b/.config/mkdocs.yml index 621f21f..e87e7a7 100644 --- a/.config/mkdocs.yml +++ b/.config/mkdocs.yml @@ -72,6 +72,7 @@ markdown_extensions: permalink: ' §' - footnotes - sane_lists + - attr_list plugins: - search - tags diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bcd314d..ca56c27 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,8 +5,8 @@ Thanks for wanting to contribute! Open a PR or an issue for anything. ## When contributing When you contribute to a pattern, add yourself as an author so you get credit -on the website (and get your own author page listing every pattern you've -worked on). +on the website and on the [Authors page](authors/index.md), which lists every +contributor alongside the patterns they've worked on. There are two short steps: