Skip to content

Add Zola-based documentation site with essays and solutions - #85

Open
alycda wants to merge 5 commits into
mainfrom
claude/zola-vs-mdbook-ghpages-krwcqb
Open

Add Zola-based documentation site with essays and solutions#85
alycda wants to merge 5 commits into
mainfrom
claude/zola-vs-mdbook-ghpages-krwcqb

Conversation

@alycda

@alycda alycda commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Summary

This PR introduces a static documentation site built with Zola, publishing the repository's essays and solutions as a browsable website. The site is authored in plain markdown with YAML frontmatter in docs/, then translated to Zola's format at build time via a Python collector script. The site is published to GitHub Pages via a new CI workflow.

Key Changes

  • Site infrastructure (site/)

    • config.toml: Zola configuration with taxonomies for tags and categories
    • sass/main.scss: Complete stylesheet with warm, paper-like aesthetic and dark mode support
    • templates/: Base layout, essay/solution/section/taxonomy templates, and shared macros
    • bin/collect.py: Python script that translates docs/ markdown + YAML into Zola's TOML format and renders CONCEPTS.md into a data file for entity references
  • Documentation sources (docs/)

    • essays/_index.md and solutions/_index.md: Section landing pages
    • essays/the-same-bug-three-times.md: Example essay demonstrating the [E01]-style entity reference system
  • CI/CD (.github/workflows/pages.yml)

    • Builds site on pushes to main/master and PRs
    • Runs collector and Zola build
    • Deploys to GitHub Pages on main/master only
  • Build integration (justfile)

    • docs-collect: Regenerate site content from sources
    • docs-build: Full site build
    • docs-serve: Local development server with live reload
    • docs-fonts: Instructions for optional webfont vendoring
  • Documentation (CLAUDE.md)

    • Added section explaining the docs site architecture and collector behavior
    • Clarified that site/content/ and site/data/ are build artifacts, never edited by hand

Implementation Details

Design philosophy: The site reads like "something a person wrote" rather than generated documentation. It uses serif fonts for prose, a single narrow column, and a warm paper-like color scheme with dark mode support.

Collector behavior: Rather than reformatting sources to suit Zola, collect.py translates them on the way in:

  • Converts YAML frontmatter to TOML under +++
  • Flattens nested solution directories into a flat list with category taxonomy
  • Strips redundant leading H1s that restate the title
  • Renders CONCEPTS.md into data/concepts.toml for entity reference resolution

Entity references: Essays can declare concepts: in frontmatter to reference shared vocabulary. The {{ e(name="...") }} shortcode resolves these to [E01]-style links in the rendered output, with definitions displayed at the end of each essay.

Gitignore: site/content/, site/data/, and site/public/ are marked as build artifacts and ignored.

https://claude.ai/code/session_01X7JLHmbox2A7tpyCU5HqvB

claude added 3 commits August 5, 2026 08:26
Publishing the repo's writing to GitHub Pages, with mdbook rejected in
favour of Zola. The deciding factor was the shape of the content: this is
a dated, tagged, growing knowledge store, not a linear manual. mdbook
models one ordered book behind a hand-maintained SUMMARY.md and has no
metadata layer at all — no frontmatter, no dates, no taxonomies — so the
"filed under" chips, the reading time, and next/previous would each have
been a preprocessor to write and keep working. Zola has all three natively.
Its default look is also nothing, which is the point: mdbook's chrome is
the clinical feel, and restyling it means fighting a theme built around
the book metaphor.

The one real cost of Zola is that it reads TOML frontmatter under +++,
and docs/solutions/ is YAML that agents already parse. Converting in
place would have been a one-way door on the sources to suit the
generator. So site/bin/collect.py translates on the way in instead:
docs/ stays plain markdown, site/content/ and site/data/ are build
artifacts, gitignored and rebuilt every time.

Details worth knowing:

- Unknown frontmatter keys pass through to [extra], so adding a field to
  a solution doc needs no collector change.
- Solutions flatten. On disk they nest a level deep by category, which is
  a good way to browse a repo; Zola would read that directory as a
  subsection wanting its own _index.md, so the nesting is replayed as a
  `category` taxonomy value instead.
- CONCEPTS.md becomes the entity index. `{{ e(name="Base profile") }}`
  renders a linked [E04], and a page's `concepts:` list produces
  definitions at its foot. This is the borrowed part of the design that
  had to be made load-bearing: an entity reference with no glossary
  behind it is decoration, and the glossary already existed.
- A concept name with no entry degrades to a visible [Name?] rather than
  failing the build, so renaming one cannot break a deploy.
- Section numbering (§ 01) is a CSS counter. Sources stay plain `##`.
- No webfonts are vendored; the stack falls back to faces already on most
  machines, so the site is fully styled with zero font bytes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7JLHmbox2A7tpyCU5HqvB
Both recipes and both CI steps get Zola and pyyaml from nixpkgs via
`nix shell` rather than a marketplace action, so `just docs-build` and CI
build with the same versions — the same reasoning as the statix/deadnix
steps in nix.yml.

The Pages workflow is path-filtered to docs/, CONCEPTS.md and site/. A
Nix-only change is the common case in this repo and has no effect on the
site, so it should not spend a deploy. PRs build without publishing,
which is enough to catch a broken template or an unresolvable internal
link before it reaches main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7JLHmbox2A7tpyCU5HqvB
The trap here is that site/content/ and site/data/ look like ordinary
content and are not: they are wiped and regenerated on every build, so
editing them is always wrong and always silently lost. Writing that down
next to the collector's behaviour table, before anyone has made the
mistake, since the whole point of the generated tree is that docs/ stays
the thing you edit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7JLHmbox2A7tpyCU5HqvB
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

⊕ Entity-level changes

.github/workflows/pages.yml

Status Type Name
+ property name
+ section on
+ section permissions
+ section concurrency
+ section jobs

.gitignore

Status Type Name
Δ chunk lines 1-20
- chunk lines 21-36
+ chunk lines 21-40
+ chunk lines 41-45

CLAUDE.md

Status Type Name
Δ heading Repository Structure
+ heading The docs site (site/, published to GitHub Pages)
Δ heading Meta: Updating This Document

docs/essays/_index.md

Status Type Name
+ preamble (preamble)

docs/essays/the-same-bug-three-times.md

Status Type Name
+ preamble (preamble)
+ heading What I thought the boundary was
+ heading Why the obvious fix does not generalise
+ heading The part I keep relearning

docs/solutions/_index.md

Status Type Name
+ preamble (preamble)

flake.nix

Status Type Name
Δ binding outputs

justfile

Status Type Name
Δ chunk lines 81-100
+ chunk lines 101-120
↻ Δ chunk lines 101-115 -> lines 121-138

site/bin/collect.py

Status Type Name
+ orphan module-level
+ function toml_value
+ function split_frontmatter
+ function to_zola_frontmatter
+ function humanize
+ function strip_leading_h1
+ function collect_page
+ function parse_concepts
+ function write_concepts
+ function main
+ orphan module-level

site/config.toml

Status Type Name
+ orphan module-level
+ property base_url
+ property title
+ property description
+ property compile_sass
+ property build_search_index
+ property generate_feeds
+ property feed_filenames
+ property taxonomies
+ property { name
+ property { name
+ section markdown
+ property markdown.highlighting
+ section extra

site/sass/main.scss

Status Type Name
+ chunk lines 1-20
+ chunk lines 21-40
+ chunk lines 41-60
+ chunk lines 61-80
+ chunk lines 81-100
+ chunk lines 101-120
+ chunk lines 121-140
+ chunk lines 141-160
+ chunk lines 161-180
+ chunk lines 181-200
+ chunk lines 201-220
+ chunk lines 221-240
+ chunk lines 241-260
+ chunk lines 261-280
+ chunk lines 281-300
+ chunk lines 301-320
+ chunk lines 321-340
+ chunk lines 341-360
+ chunk lines 361-374

site/templates/base.html

Status Type Name
+ orphan module-level
+ element title
+ orphan module-level
+ element body
+ element a
+ element header
+ element a
+ element nav
+ element a
+ element a
+ element a
+ element a
+ element main
+ element footer
+ element div
+ element h2
+ element ul
+ element li
+ element a
+ element li
+ element a
+ element li
+ element a
+ element li
+ element a
+ element p
+ element a
+ element code
+ orphan module-level

site/templates/concepts.html

Status Type Name
+ chunk lines 1-20
+ chunk lines 21-36

site/templates/essay.html

Status Type Name
+ chunk lines 1-20
+ chunk lines 21-40
+ chunk lines 41-60
+ chunk lines 61-64

site/templates/index.html

Status Type Name
+ chunk lines 1-20
+ chunk lines 21-40
+ chunk lines 41-42

site/templates/macros/bits.html

Status Type Name
+ chunk lines 1-20
+ chunk lines 21-40
+ chunk lines 41-60
+ chunk lines 61-62

site/templates/section.html

Status Type Name
+ chunk lines 1-20

site/templates/shortcodes/e.html

Status Type Name
+ chunk lines 1-12

site/templates/solution.html

Status Type Name
+ chunk lines 1-20
+ chunk lines 21-40
+ chunk lines 41-53

site/templates/taxonomy_list.html

Status Type Name
+ chunk lines 1-20

site/templates/taxonomy_single.html

Status Type Name
+ chunk lines 1-20
+ chunk lines 21-22

Summary: 109 added, 6 modified, 1 deleted, 1 renamed across 21 files (6 added orphans)

functions and classes, not lines · sem

CI failed to build the site: nixpkgs ships Zola 0.22.1, and the config
was written against 0.19, whose [markdown] schema it rejects outright.

The syntax error is the symptom. The cause is that `nix shell
nixpkgs#zola` resolves against the machine's flake registry rather than
this repo's flake.lock, so a local build and a CI build of the same
commit could get different Zola versions — and did. Fixing only the
config would leave that live, with the next nixpkgs bump free to break
the build again from a clean tree.

So the toolchain moves into a `docs` devShell alongside the existing
`tools` and `default` shells, and both `just` and the workflow go
through `nix develop .#docs`. A version bump can now only arrive via
flake.lock, where it is visible in a diff and lands on one commit
instead of appearing spontaneously on whichever machine updated its
registry last.

The 0.22 config changes, for the record:

- Highlighting moved from flat [markdown] keys (highlight_code,
  highlight_theme, highlight_themes_css) to a [markdown.highlighting]
  table taking style = "class" plus a light_theme/dark_theme pair.
  Class-only output without a named theme is no longer possible.
- Theme names come from giallo now: gruvbox-light-medium, not
  gruvbox-light.
- Generated stylesheets are giallo-{light,dark}.css, so base.html and
  the gitignore entry follow.

Verified against 0.22.1 rather than the 0.19 I had been building with —
`pre.giallo.z-code` renders with the site's own panel background, since
`.prose pre` outspecifies giallo's `.z-code`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7JLHmbox2A7tpyCU5HqvB
@alycda
alycda marked this pull request as ready for review August 5, 2026 08:48
alycda pushed a commit that referenced this pull request Aug 5, 2026
The skill-creator trigger evaluation (20 realistic queries x 3 runs
via claude -p) showed the original description badly under-triggered:
0/10 should-trigger queries consulted the skill, while all 10
near-miss negatives correctly stayed quiet. The failing clause was
the abstract one — "use whenever the question is semantic rather than
textual" loses to grep/git-diff muscle memory every time.

Part of the zero was harness artifact (the eval runs in an empty
directory and only counts the skill if it's the FIRST tool call), so
the rewrite was validated manually instead: with this description,
queries run inside a real repo consult the skill as their first
action and then run the availability check it teaches (4/4 previously
failing queries), while the riskiest near-miss — "review PR #85 but
just the commit messages" — still routes to commit-craft, not here.

What changed: abstract trigger conditions became concrete task
shapes ("what breaks if I rename X", "which parts of this 4000-line
PR need human review", "blame wrecked by a formatting commit",
"false conflicts from parallel agents in one file"), plus an explicit
consult-this-FIRST framing — skills under-trigger by default, and a
description has to outbid the model's habit of just using plain git.
Every clause stays function/code-anchored so semver, lockfile
conflicts, and commit-message review keep failing to match.

Eval set and results are in the session workspace, not committed —
they're a measurement, not content; re-derive with skill-creator's
run_eval.py when the description next changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2Mv7iPiENFihUPt2zNdRe
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants