CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this is
Static website for PPGCosmo (International PhD Program in Astrophysics, Cosmology and Gravitation at Ufes, Brazil), built with Quarto and deployed to GitHub Pages at ppgcosmo.org.
Commands
pip install jupyter # one-time: Python + Jupyter are required to execute embedded code
quarto preview # live preview with auto-reload
quarto render # full build → _site/There is no test suite or linter. “Correctness” means the site renders without errors and the pages look right in preview.
Deployment
Pushing to main triggers .github/workflows/publish.yml, which runs quarto render (Quarto pinned to 1.7.32) and publishes _site/ to GitHub Pages. No manual deploy step. If you change how pages render, verify with quarto render locally before pushing, since a render failure breaks the live site.
Architecture
The site is data-driven: .qmd pages contain embedded Python (executed at render time via Jupyter) that reads JSON/CSV from _data/ and prints HTML into the page (#| echo: false + #| output: asis). Editing content almost always means editing a _data/ file, not the pages.
- Pages:
index.qmd(home + news + coursework),people.qmd,applications.qmd(plain Markdown, edit directly),contributors.qmd,wabb2026.qmd. _data/: the content source of truth — people lists (faculty_brazil,phd_students,postdocs,postdocs_previous,alumni,collaborators),institutions,news.json, and the WABB CSVs._quarto.yml: site-wide config (navbar, footer,flatlytheme,page-layout: full).styles.cssholds CSS overrides.contributors.qmdbuilds its table by shelling out togit log --numstatat render time and maps author names → GitHub handles via_data/github_usernames.json. Add new committers there to get them linked. This is why the CI checkout usesfetch-depth: 0— a shallow clone would silently truncate the table.tools/: local coordination scripts, not part of the site (see below).
The generated HTML depends on class names defined in styles.css (people-grid, person-card, person-name, person-links, person-qualified, person-qualifying-pending, faculty-grid, collapsible-section, table-wrap, prev-coord-table, people-cta, btn-people, btn-ufes, connections/connection-item/conn-*, photo-credit). Changing a class in one place means changing it in both.
news.json entries have three shapes
index.qmd’s news_feed() renders three kinds of entries (see TYPE_COLORS / TYPE_LABELS / ROLE_LABELS in the file):
- Call / announcement — has
"type": "call"(or"announcement") and a"title". Rendered with a colored type tag. - Person achievement — has a
"people"array of{"name", "role"}where role isfac|pd|s|a. Notype/title. - Qualification exam — has
"type": "qualification"and"student"(a plain name string — nopeople/role, since the tag already says these are students), plus"title"(the exam title),"when"(free display string, house style"14 August 2026, 16:00 BRT") and"committee"(list of{"name", "affiliation"}, affiliation optional). The student heads the card instead of the title; the title,whenand committee are rendered as body lines.dateis the announcement date, not the exam date.
Any entry whose type is in TYPE_COLORS gets a tag; the tag text is TYPE_LABELS.get(type, type), which is how qualification displays as “qualification exam”. A typed entry heads with student when type is qualification and with title otherwise — both keys are required, not optional, for their respective shapes.
All shapes use date (ISO "2026-05-15" or year-only "2026"), text (may be ""), and urls (possibly []). Entries are sorted newest-first; the home feed shows only the current and previous calendar year (years=2 cutoff).
people.qmd specifics
people_grid()renders any of the people JSONs. It only needsname,photo, and optionalurl;collapsible=wraps the grid in a<details>(used for Alumni and Previous Postdocs), andfilter_fn=selects a subset — the Student Representatives section isphd_students.jsonfiltered on a truthystudent_representativeflag.phd_students.jsoncarries far more per-person fields than the grid displays (e-mail,orcid,cv_lattes,fellowship,enrolment,advisor,coadvisor,qualifying,country,grad_*,masters_*,slug). Preserve them when editing;CONTRIBUTING.mdstill documents only the old{name, photo}shape._data/students_data.csvis the spreadsheet those student records came from. No page reads it — it is not a build input.- The Leaflet map is generated from
institutions.json+collaborators.json. Each institution tooltip lists faculty by substring match ofinstitutions[].nameinsidefaculty_brazil[].affiliation— so combined affiliations like"CBPF & UNSAM"intentionally match both. Renaming an institution silently empties its tooltip. - The Coordination and Previous Coordinators tables are hardcoded Markdown/HTML in
people.qmd._data/organization.jsonis stale and read by nothing — edit the page, not the JSON. - The rotating banner is a hardcoded
slidesarray in the page’sinclude-in-headerscript (image, caption, link), not data-driven. Three slides on an 18 s cycle; the caption interval (6000ms) and the CSSanimationDelay(i * 6) must stay in step with the slide count. - Card icons (
person_links()): e-mail, Lattes, ORCID, website, then the qualifying check mark. Every item is skipped when its field is empty, which is why the same function works for alumni and postdocs, whose records lack these fields. - E-mail obfuscation:
_quarto.yml’semail-obfuscation: javascriptonly covers links Pandoc parses from Markdown, not the raw HTML these chunks print. Soemail_attrs()splits each address intodata-u/data-dand the script in this page’sinclude-in-headerreassembles it. Both halves are required — printing a plainmailto:here would publish harvestable addresses. - The Lattes icon is referenced as
images/lattes.svgrather than inlined on purpose: Pandoc lowercases attribute names when re-serialising HTML, turning an inlineviewBoxinto an ignoredviewboxand breaking the icon’s scaling.
Qualifying-exam status
One field drives everything: qualifying in phd_students.json, as "YYYY-MM".
- On the People page a filled field earns a green check; an empty one earns a grey “to be done” check only for students whose
enrolmentis on or afterQUALIFYING_RECORDS_FROM("2023-01") — earlier cohorts predate record-keeping and get no mark. That cutoff is a fixed string by design: deriving it from who has already passed would creep forward and silently drop the pending mark from students who still owe the exam. - Deliberately only two states, never a “running late” shade — the page is public and flagging named students as overdue exposes more than the programme wants.
tools/qualifying_report.pyis where that oversight lives: a stdlib-only script (python3 tools/qualifying_report.py) that prints an overdue-first ranking to the terminal and writes nothing. It stays a.pyprecisely soquarto rendercannot pick it up. ItsRECORDS_FROMduplicatesQUALIFYING_RECORDS_FROMinpeople.qmd— change one, change the other.tools/README.mddocuments the groups and settings.
wabb2026.qmd (workshop page)
Event page for the V WABB workshop, written in Portuguese. Mostly hand-written Markdown, plus Python chunks that render _data/wabb/wabb2026_program.csv (schedule) and _data/wabb/wabb2026_registered.csv (registrant list) as Bootstrap tables. Registration itself goes through an external Google Form linked from the page.
Everything specific to the workshop is namespaced so future editions can be added without touching the rest of the site: data in _data/wabb/ (year in the filename) and images in images/wabb/ (v_wabb_header.jpeg, iv_wabb_2025.webp, apoio_wabb.webp). The page also uses images/logo-ppgcosmo_1.webp and images/campus1-4.webp, which stay in images/ as shared assets.
csv_table() is defined once, in a hidden setup chunk above “Lista de inscritos”; the later chunks only call it (Quarto runs all of a page’s chunks in one Jupyter kernel). It must never raise — a missing or renamed CSV prints UNAVAILABLE instead, because an exception here aborts quarto render for the whole site, not just this page. Cells are HTML-escaped and then given back a fixed Markdown subset — [text](url), **bold**, *italic* — which is how **Palestra 1** in the program CSV renders bold. All-empty trailing columns and surrounding whitespace are stripped, so a spreadsheet export padded with ,, still renders cleanly.
Only edition-specific tables are data-driven. Dates, invited speakers, both committees, hotels and fees are deliberately still hand-written Markdown — this is the first WABB edition built in Quarto, and generalising the page into a template is deferred until a second edition actually needs it.
Its navbar entry in _quarto.yml is commented out, so the page renders and is reachable by URL but not linked from the nav. Uncomment those two lines to publish it.
Profile photos
images/people/firstname_lastname.jpg (lowercase, no accents/spaces), referenced by the photo field in the people JSON. Extensions vary in practice (.jpg, .jpeg) — the photo value must match the real filename exactly. Displayed as 90×90 px circles. Use images/people/unknown_person.jpg as the placeholder.
Notes
*.quarto_ipynbfiles are Quarto’s intermediate render artifacts — gitignored, safe to ignore;.quarto/and_site/likewise.- Quarto renders every root-level
.md, not just the.qmdpages:CLAUDE.mdandCONTRIBUTING.mdare published asCLAUDE.htmlandCONTRIBUTING.html. (README.mdis the one Quarto skips by default, which is also whytools/README.mdnever renders.) Nothing links to them, but they are publicly reachable — write them accordingly. _quarto.yml’sresourcescopiesimages/**andCNAMEinto_site/;_data/itself is not copied (only_site/_data/CNAME, which is just the resource keeping its path), so the JSON files are read at render time and never published.CONTRIBUTING.mdis the human-facing editing guide and covers the common content edits in more detail.