Repository navigation
Replace Zola with a std-only Rust generator, deployed by GitHub Pages actions - #14
kenesparta wants to merge 120 commits into
Conversation
Four short notes back Plan 0002: an inventory of what the Zola site uses today, a design for a std-only Rust generator, Docker builds with an official-actions Pages deploy, and what to keep from the August plan.
A small plan of reviewable steps: pin the broken deploy action first, make content Markdown-only and drop Bulma while Zola still deploys, then build a std-only Rust workspace next to Zola and switch Pages over in one final step.
shalzz/zola-deploy-action@master now installs Zola 0.23.6, which stops at templates/macros.html:1 with "Unknown tag", so every PR build and the next deploy fail. Pin the action to v0.22.1 and actions/checkout to v7.0.1 by commit SHA, give each job only the token permissions it needs, and name Zola 0.22.1 in the README for local builds.
fix: pin the Zola deploy action to v0.22.1
Every step now lands as a reviewed PR on the fork's main, and the finished work goes upstream as one PR (new step 9), so Part B no longer waits for upstream's agreement. Mark step 1 as done and record the step 2 choices: resources as a Markdown list without the 3 empty subcategories, the entries of upstream PRs #3 and #9 ported with credit, and Zulip linked through /zulip/.
docs: add Plan 0002 and its research notes
Nine of the eleven content files now start with a YAML header (---) that uses only keys Zola and the planned generator both read. News posts list their authors in the page-level authors key instead of [extra] author, and the byline joins them with "and", so it reads as before. The news section gains a title. The resources and Zulip files follow in their own commits.
Zola already turns _ into - in URLs, so /news/call-for-participation/ and /news/campus-tour/ stay the same; the files now match them.
The 3-level TOML data becomes headings and a list item per resource
("[Title](url) — Author. Description."), so adding one is a one-line
edit with a GitHub preview. Headings get real ids, which the old #slug
links lacked, and the inline code in a description now renders. The
three empty subcategories (Macros, Networking, Web) are dropped, and
resources.html and macros.html are deleted. The intro note keeps its
text and links straight to the editor for this file.
Ported from upstream PR rust-edu#3, which added the entry to the TOML data this branch replaces. Co-authored-by: ZhangHanDong <27893+ZhangHanDong@users.noreply.github.com>
Ported from upstream PR rust-edu#9, which added the entry to the TOML data this branch replaces. Co-authored-by: Alexandru Radovici <2838177+alexandruradovici@users.noreply.github.com>
content/zulip.md becomes the section content/zulip/_index.md, because Zola only honours redirect_to on sections; Zola's built-in redirect replaces redirect.html. The menu and the home page link to /zulip/ instead of zulip.rust-edu.org, which answers with a redirect to plain http://, so the Zulip host lives in one file.
The announcement button (a div with Bulma classes and class="is", which matches no CSS rule) becomes a plain link, and the contact address a Markdown mailto link. content/ now holds no HTML tags.
Three links between pages resolved under the current page and 404ed live (cfp-2022.html, paper-format-latex/, code-of-conduct); they are now root-relative. The contact link used an email: scheme wrapped in <> and becomes a plain mailto link. Two http://rust-edu.org links point to / instead.
The GOALS list in the announcement switched from * to - for its last item, which splits it into two lists; it now uses * throughout. Two pages repeated the title as a second h1 in their content; those headings become h2 (their ids stay the same). Two invisible zero-width spaces inside a campus-tour quote are removed.
Without sort_by, Zola lists a section's pages in no set order; the live /news/ shows 2022, 2026, 2025. sort_by: date lists them newest first.
Zola turns a relative link such as proceedings.pdf into an absolute https://rust-edu.github.io/... URL, which costs a 301 to rust-edu.org. The five file links on the workshop pages are now root-relative, so all links between pages and files in content/ use the same form.
docs/editing.md maps each URL to its file and lists the header keys, the Markdown in use, the link forms that work on every page, the common changes (a news post, a resource, a menu link), proposing a change on github.com, and a local preview with Zola 0.22.1.
refactor: Markdown-only content
base_url was https://rust-edu.github.io, so the stylesheet, logo and every permalink went through a 301 to rust-edu.org, and sitemap.xml and robots.txt advertised the github.io host; it is now rust-edu.org. title and description sat under [link_checker], where Zola ignores them, and description gets the site's tagline. build_search_index (misplaced under [search], no search UI), render_emoji (no emoji codes in content), minify_html (the default) and stale comments go. Menu links end in /, which saves a redirect on GitHub Pages. The built pages are otherwise unchanged.
static/style.css (7.6 KB, served at the same /style.css) replaces the Sass build of Bulma and Bulmaswatch (429 KB, Bulma included twice, plus a runtime Google Fonts import). It holds every color in one token block with a dark theme by prefers-color-scheme, uses the system font stack, styles the HTML that Markdown produces without classes, links in brand green #0b7261 (underlined inside text), and meets WCAG 2.1 AA contrast in both themes. The logo is inverted in the dark theme, since its lettering is black. The templates drop the Bulma markup: a header, nav, main and footer with a skip link and visible focus; no JavaScript, so the mobile menu wraps instead of collapsing; no nested link around the logo, no target="_blank"; news dates use the same "Jul 11, 2022" format as the pages; and the content is no longer run through Markdown a second time. home.html (unused), sass/ and static/js/site.js are deleted.
Nothing uses ext/bulma or ext/bulmaswatch since style.css is plain CSS, so the submodules, .gitmodules and the checkout's submodules option go. A fresh clone builds with no extra step; the README and docs/editing.md lose the submodule step, the README its stale troubleshooting section (including a config snippet that was invalid TOML), and it now points to the editing guide and to where the layout and style live.
refactor: plain CSS, no Bulma, no submodules
A virtual workspace for the std-only site generator: resolver 3, edition 2024 and Rust 1.99.1, pinned in rust-toolchain.toml (minimal profile plus rustfmt and clippy) and as rust-version; publish = false, and license CC0-1.0 like the rest of the repository (plan decision 6). The workspace lints forbid unsafe code and warn on unwrap, expect, panic, todo, unimplemented, dbg!, printing to stdout, mod.rs files, glob imports and lossy casts; clippy.toml allows unwrap, expect and panic in tests. Cargo.lock is committed and lists no dependencies. crates/site is edu-site, with the edu binary (plan decision 5). It knows two commands so far, help and version; any other command line is a usage error that exits with status 2. Arguments are read with args_os, so one that is not valid Unicode is an error, not a panic. The other crates of the plan (edu-yaml, edu-markdown, edu-template) arrive with their first code.
The Dockerfile's check stage runs cargo fmt --check, clippy with
warnings denied, the tests and a release build, all --locked --offline
and with no network, in rust:1.99.1-slim-trixie pinned by digest. The
same command runs the checks on a laptop:
docker build --target check .
The .dockerignore is an allowlist, so .git, target/ and public/ never
reach the build.
ci.yml runs that build on pull requests and pushes to main with a
read-only token, after two guards: Cargo.lock lists no crate from a
registry or git, and the Dockerfile's FROM tag names the Rust version
of rust-toolchain.toml. The runner is ubuntu-latest, since the checks
run inside the pinned image. The README says how to run the checks;
Zola still builds and deploys the site.
Decisions 2 and 3 (links in brand green, no JavaScript) were applied in step 3; decisions 5 and 6 (the edu names, and CC0 for the code too) were taken on 2026-11-01 for step 4. Steps 0, 2 and 3 are marked done. Step 4 now adds each crate with its first code instead of creating four crates at once, empty ones included, so its done-when asks that Cargo.lock list only our own crates.
feat: generator workspace, edu command and Docker CI
The first half of the YAML subset of research 0002 §3: `key: value` maps, maps indented under `key:`, plain, single-quoted and double-quoted scalars, and comments. Every scalar stays a string for its consumer to type. Anything outside the subset (tabs, anchors, aliases, tags, flow collections, block scalars, values over several lines, `: ` in a plain value) is an error at a line and column, the column counted in characters. Lists, page headers and `edu check` follow in the next PR.
The YAML subset with its tests comes to about 690 lines, over the 600-line limit of a PR, so it lands in two halves where research 0002 §9 already split it: maps, scalars and errors first; lists, page headers and `edu check` next. Part B grows to about 4,850 lines in about twelve PRs.
feat: edu-yaml, the YAML subset's maps, scalars and errors
ci: deploy the site on each push to main, and remove Zola
…t with a dot The generator followed a symbolic link among the files of the site, so `edu build` copied whatever the link pointed to, inside or outside the repository, into the built site. A link in `content/`, `static/` or `layouts/`, a linked folder of them, `site.yaml` or `urls.txt` now stops the check, naming the link. Files and folders whose names start with `.`, such as `.DS_Store`, are no longer part of the site. A planned site is now written by its own function, so the test of a copy that fails no longer needs a broken link to make one fail.
…eets Pages have no HTML, but the build copied any other file of `content/` and `static/` as it was, so an `.html` or `.js` file would have run its scripts on the site as one of its pages. The check now stops at a file of another type than the site publishes, and names the types it does publish; an HTML file gets a hint to write the page in Markdown. The test sites that clashed with `static/index.html` and `static/sitemap.xml` now clash with `.txt` files instead.
A heading with a letter outside ASCII, such as `## Taller en Perú` or a heading in Chinese, failed the check with "an id is made of ASCII letters and digits", although the guide says to type `é` as it is. Its id now keeps its letters and digits in any script, in lowercase, as GitHub keeps them: `taller-en-perú`. Every heading of the site is ASCII, so no id changes. The check of links decodes the `%` escapes of an id, so a link to `#taller-en-perú` finds it. An error about a link shows it as the page writes it, `/news/café/` instead of `/news/caf%C3%A9/`, at its line and column instead of at the top of the page.
Some editors on Windows start a UTF-8 file with U+FEFF, which shows as nothing. A page saved that way failed the check with "expected a line `---` that opens the header", though its first line reads `---`. The YAML parser now leaves the mark out, for pages and for `site.yaml`.
`edu check` names the file, line and column of each mistake, but in CI that line sat inside the log of the Docker build, and the pull request only said that the check failed. The step now also writes each such line as an annotation, which GitHub shows on that line under *Files changed* and in the summary of the checks. `docs/editing.md` says so, and that the check of a first pull request waits until a maintainer lets it run.
Zola no longer builds the site, and a contributor has no reason to know
it. The messages for a `~` and for `{…}` at the end of a heading now say
that GitHub may read `~` as strikethrough while the site would not, and
that `{…}` after a heading is not supported.
fix: refuse symbolic links, and publish only images, PDF, text, LaTeX and CSS files
fix: accept headings in any language and a byte order mark, and show errors on the pull request
A browser that opens an SVG image at its own address runs the image's scripts as if it were a page of the site. The check now scans each SVG image, and stops at a `script` or `foreignObject` element, an event attribute such as `onload` or an animation of one, a `javascript:` address or the XHTML namespace; and at what could hide one of them from the scan: a reference other than XML's own five, a processing instruction other than the XML declaration, a `<!DOCTYPE>` with declarations, or markup that does not close. The site's banner passes as it is.
GitHub Pages sends an SVG image without a charset, so a browser reads it in the encoding that its XML declaration names, where the same bytes can be other characters than the UTF-8 text that the scan reads. The check now stops at a declaration that names another encoding than UTF-8, and at a control character other than a tab or a line end, which XML does not allow either.
fix: publish an SVG image only if it has no way to run a script
|
@kenesparta This looks great! Have some things though. Presumably the new site gen was AI-generated to some degree? If so, please make there's an AI acknowledgement in the PR and elsewhere so that folks know what tools were used and how. We're sure we want to maintain our own static site generator forever, right? That's no small commitment. The original plan was not to merge a PR, but rather to just rename and archive this repo, then move your fork into place here. If you'd rather do it by PR, maybe that makes more sense though. The Atom feed is going to need to happen fairly quickly — I follow this site this way, and I suspect a few others do too. All that said — thanks very, very much for this big piece of work! Let me know what you think here and we'll get this merged or whatever as soon as we can. |
AGENTS.md holds the instructions, and CLAUDE.md and .gemini/settings.json point Claude Code and Gemini CLI to it. The Makefile gives one command for the checks, which also run format and lint, since CI stops at those as well.
docs: add one set of instructions for AI coding agents, and a Makefile
…hoto The editing guide, README.md and AGENTS.md now name the `hero` list of site.yaml, which holds the next steps under the home page's heading, and the editing guide says that the home page shows the site's description as that heading. A new item says how to save a photo: 960 pixels wide, in the 16:9 shape, as WebP of at most 90 KB. The stylesheet's header no longer says that the body is the column, which is not so in this design.
…of the editing guide README.md and AGENTS.md now name the `hero` key of site.yaml, as the previous commit said they did. The editing guide no longer says that the title is every page's `#` heading, since the home page shows the site's description there; its image example uses a .webp file, as the photo item advises; it says that the check follows the links of site.yaml, not only the menu's; and the photo item says how far a portrait photo moves the text. Two comments, in site.yaml and in the stylesheet, now say what their lines do.
feat: a simpler, modern look, WebP photos and a lighter logo
…ions Headings take three colors built from the crab's orange, the brand's color: titles take it as rust; section headings take blue, the split complement of orange that the teal of links does not take; and sub-sections take violet, blue's neighbor. Each has a contrast of at least 5.5:1 on the page, in both themes. A bold line right above a photo, such as a place on the campus tour, takes the sub-section color, and when it follows text it keeps the space above it. The home page's statement keeps the text color beside the crab. On the campus tour, "Where We've Been" becomes a heading like the post's other sections.
The button sits at the end of the menu. A short script in the head sets the choice, kept in localStorage, before the page paints, so it holds on every page and on the next visit; until the button is used, the site follows the system's theme. Without JavaScript the button stays hidden and the site still follows the system's theme, and printing stays light. The dark colors now apply when the system asks for them and the button has not chosen light, or when the button has chosen dark. These few inline lines in layouts/layout.html are the site's only JavaScript, which README.md now says. Plan 0002 chose none (decision 3); the site stays whole without it.
From 46rem up, the bullets and numbers of a list hung in the margin left of the column, so that the text of each item lined up with the paragraphs. On screen they read as out of line. Lists now keep their indent of 1.5rem at every width, with their bullets inside the column.
…button feat: colored headings, a light and dark theme button, and bullets inside the column
The dark theme inverted the whole logo with a filter, which made the black cap white, paled the crab and inverted its eyes. The filter is gone. The header now holds the logo twice and the stylesheet shows the one for the theme: `#dark` in the banner's address turns its lettering to the light paper's color, and the crab and the cap keep their own colors. A prefers-color-scheme rule inside the banner would not do, because in Safari it follows the system's theme and not the theme button. Without `#dark` the lettering stays black, so a site that embeds the banner shows the lettering it showed before, and so does a printed page. The cap stays black in both themes. A fine line in the light paper's color runs around its two parts: it disappears on the light theme and sets the cap off on the dark one. It takes the place of the white shapes and strokes behind the cap. The tassel is gone, since it hung behind the crab and only three fragments of it showed. The cap sits one unit lower, so that its line covers a notch of the shell's edge at its left corner. The crab is unchanged. The banner goes from 3,897 to 2,752 B with gzip, and style.css from 2,831 to 2,883 B. Where the stylesheet does not load, both logos show.
The icon was a crab drawn for the tab alone. It is now the banner's own symbol, the crab under its black cap, without the lettering and centered in a square. The line around the cap has the light paper's color, so it sets the cap off on a dark tab and is hardly seen on a light one, and the icon needs no rule for the dark theme. At 16 px the line is about half a pixel of a 2x screen; on a dark tab of a 1x screen the cap's edge is faint. The icon goes from 349 to 1,932 B with gzip.
feat: a logo that keeps its colors in the dark theme, and a tab icon drawn from it
The site moves from Zola to its own small generator, written in Rust with only the standard library, and GitHub Pages publishes it with GitHub's own actions. Every page stays one Markdown file under
content/, now with a short YAML header, and keeps its address.The deploy that runs today is broken.
main.ymlusesshalzz/zola-deploy-action@master, which now installs Zola 0.23.6, and that version stops on this repository attemplates/macros.html:1(Unknown tag). So the next push tomainfails to deploy, and every pull request build fails too.The result is live on the fork's Pages site, built by the
deploy.ymlof this PR: https://kenesparta.github.io/rust-edu.github.io/ For now that site shows a preview with the feeds of the pull requests named under "How to review". The next merge into the fork'smainbrings it back to this PR's head.For people who edit the site
title, plusdateandauthorsfor a news post.docs/editing.mdsays where each page lives and lists the keys.site.yaml.urls.txtstill exist. An error shows on its line under Files changed, and the check attaches the built site, drafts included, as the artifactsite-preview.docker compose up, then http://127.0.0.1:1111. A reload builds the site again, or shows the error of the check.What changes on the site
static/style.cssof 9.2 KB replaces the 429 KB of Bulma and Sass. It has a light and a dark theme, WCAG AA contrast, a skip link and a visible focus. A button at the end of the menu switches between the two themes. Its two short inline scripts are the site's only JavaScript: without JavaScript the button stays hidden, and the page follows the system's theme. The menu wraps on small screens.urls.txtstill exist: every page and file of rust-edu.org today, except/js/site.js, the script of the old menu./zulip/still redirects to the Zulip chat, and the menu and the home page now link to it.email:link are fixed. The news list is sorted newest first, and the three empty subcategories of the resources page are gone.The generator
crates/holds a Cargo workspace of four crates, andCargo.locklists only those:edu-yaml,edu-markdown,edu-templateandedu-site, which builds the binaryedu. They have 7,553 lines of Rust, tests included.edu checkchecks the site,edu buildwrites it intopublic/, andedu serveserves it locally.file:line:coland a hint, never a guess.javascript:address or HTML, and at a symbolic link.layouts/. A change of layout or style needs no Rust.research/record the decisions. The README documents the build, the check, the preview and the deploy.AI acknowledgement
I made this with Claude Code, Anthropic's coding agent, using the Claude Opus 5.5 model since 2026-10-28. Claude Code wrote most of the generator's code and tests, the layouts and the stylesheet, the docs, the research notes, the commit messages and this description, and it drafted Plan 0002 from goals and decisions I set. It also did the checks that the pull requests report: the comparisons with what Zola 0.22.1 deployed, the security review behind kenesparta#30 to kenesparta#32, and the mutation tests. I wrote three commits myself: 068bfca, the
Openenum inedu-template, in kenesparta#15; and 2f012d3, the README's "How it works", and 8d5d63b, which moved template parsing intocrates/template/src/parse.rs, both in kenesparta#17. Each change was its own pull request on my fork (kenesparta#2 to kenesparta#35, and kenesparta#37), with green CI, and I read each one before it was merged. My feedback went into the sessions or into my own commits, so those pull requests have no review comments. The changes to the logo and the tab icon in kenesparta#37 were made with Claude Code using the Claude Fable 5.2 model. The August plan that Plan 0002 replaces (tagarchive/initial-project-planon my fork) was drafted with Claude Fable 5. I used Gemini CLI only for reviews and questions: none of the committed code or docs is its output.AGENTS.mdholds the instructions that the agents followed.How to review
These are 85 commits from 35 pull requests, each merged into
mainof the fork (kenesparta#2 to kenesparta#35, and kenesparta#37). kenesparta#30 to kenesparta#32 come from a review of the security of the generator and of how easy it is to add a news post, kenesparta#33 addsAGENTS.mdand aMakefile, and kenesparta#34, kenesparta#35 and kenesparta#37 give the site a new look. Merging with a merge commit keeps them as they were merged on the fork. All the actions are GitHub's own and pinned by commit SHA. Dependabot proposes updates of the actions weekly. It also proposes updates of the Rust image together withrust-toolchain.toml, in one pull request.Eleven pull requests on the fork join this PR as they merge, in order: kenesparta#38 to kenesparta#47, then kenesparta#49.
/atom.xmland/rss.xml. The fork's Pages site previews them for now: given the address of each feed there, the W3C Feed Validation Service finds both valid, with no warning.Checklist for the admin who merges
deploy.ymlneeds one setting that an admin or a maintainer (GitHub's Maintain role) can change, and a second one, which only an admin can change, keeps other branches from deploying:rust-edu.orgfor it: organization settings → Pages → Add a domain, then a TXT record at the DNS host, as GitHub shows it, then Verify. The record can take up to 24 hours to show. Then nobody else can claim the domain if it is unlinked for a moment during the switch.rust-edu.org, and Enforce HTTPS stays on.main. The environment dates from the Zola deploys and allows every branch today, anddeploy.ymlcan also be run by hand on any branch. On the fork, where Pages started with GitHub Actions, GitHub set this rule itself.https://rust-edu.org/style.cssis about 9 KB instead of 429 KB, andhttps://rust-edu.org/zulip/still redirects to the chat..github/dependabot.ymlwas checked.Rollback
Settings → Pages → Source: Deploy from a branch,
gh-pages,/ (root). Pages then serves the last Zola build, whichgh-pagesstill holds. The branch stays for two weeks after the merge.Reverting the merge alone does not bring the old deploy back. The
main.ymlit restores installs Zola 0.23.6, which fails as described above.