docs: migrate the manual to a Starlight documentation site #72

Merged
rootiest merged 15 commits from docs-starlight-site into main 2026-07-26 04:27:30 +00:00
Owner

Replaces the hand-maintained docs/fish-config.md monolith and the chunked
HTML/wiki output with a docs/manual/** source-of-truth tree and an Astro
Starlight site built from it.

What changed

New SSOT. docs/fish-config.md was split into docs/manual/** — one file
per section, one directory for the 14 function categories, each carrying YAML
frontmatter (title, manTitle, sidebar.order, helpKeywords). Everything
downstream is generated from this tree.

Generated outputs. docs/build-manual.py produces the concatenated
markdown (pandoc's input for the man page) and the Starlight content
collection. docs/manualtools.py holds the shared frontmatter/ordering
library. A round-trip test asserts the concat reproduces the original file
byte-for-byte, so the man page cannot silently drift.

Retired. docs/split-wiki.py, docs/wiki/**, and the chunked-HTML
pipeline. The old wiki generator has no remaining callers.

Site. Astro Starlight in docs/site/, Catppuccin Mocha/Latte, Pagefind
full-text search, deployed to Cloudflare Pages by .gitea/workflows/build-docs.yml
on push to main.

Syntax highlighting. The manual is authored man-page style — every
synopsis, example, option table, and description sits in one 4-space-indented
block, which declares no language and renders as an unhighlighted grey slab.
The site build now splits each block into paragraphs and classifies them:
synopsis and shell examples become fenced fish, descriptions become prose,
and column-aligned reference tables keep their indentation. 175 blocks
highlight; the 412 lines of genuine tables are untouched.

This transform is site-only. docs/manual/** keeps the indented form that
pandoc and config-help depend on, and a test fails if a fence is ever
written back to the SSOT.

config-help. Now resolves sections against the manual tree's headings
and gained a --html flag that opens the site.

Deployment prerequisite

The Cloudflare Pages project must exist before CI can deploy to it. Create
fish-config-docs in Direct Upload mode and seed it with the pre-built
bundle (docs/site/dist/, also zipped at ~/fish-config-docs-site.zip), then
add repository secrets CF_API_TOKEN (scope: Cloudflare Pages: Edit) and
CF_ACCOUNT_ID. Until those exist the deploy step fails; nothing else in the
workflow depends on it.

Manual Verification

  • python3 docs/verify-manual.py reports 16/16 passing
  • cd docs/site && npx astro build completes and emits 120 pages
  • npx astro preview — code blocks on /functions/file-and-directory/rm/
    are syntax-highlighted in Catppuccin colours, not flat grey
  • The pkg page keeps its package-manager query table column-aligned
    rather than fencing it as shell
  • The Key Bindings page's box-drawing table renders unchanged
  • The sidebar shows one "Functions Reference" group, leading with
    "Overview", with title-cased category names
  • Search returns results for a function name (e.g. mkcd)
  • Toggle light/dark — both Catppuccin flavours read correctly
  • help config opens the terminal manual and paginates
  • help config abbreviations jumps to the Abbreviations section
  • help config --man renders the compiled man page with correct headings
  • help config --html opens the documentation site
  • git diff main -- docs/fish-config.md shows only regeneration noise,
    no content loss
Replaces the hand-maintained `docs/fish-config.md` monolith and the chunked HTML/wiki output with a `docs/manual/**` source-of-truth tree and an Astro Starlight site built from it. ## What changed **New SSOT.** `docs/fish-config.md` was split into `docs/manual/**` — one file per section, one directory for the 14 function categories, each carrying YAML frontmatter (`title`, `manTitle`, `sidebar.order`, `helpKeywords`). Everything downstream is generated from this tree. **Generated outputs.** `docs/build-manual.py` produces the concatenated markdown (pandoc's input for the man page) and the Starlight content collection. `docs/manualtools.py` holds the shared frontmatter/ordering library. A round-trip test asserts the concat reproduces the original file byte-for-byte, so the man page cannot silently drift. **Retired.** `docs/split-wiki.py`, `docs/wiki/**`, and the chunked-HTML pipeline. The old wiki generator has no remaining callers. **Site.** Astro Starlight in `docs/site/`, Catppuccin Mocha/Latte, Pagefind full-text search, deployed to Cloudflare Pages by `.gitea/workflows/build-docs.yml` on push to `main`. **Syntax highlighting.** The manual is authored man-page style — every synopsis, example, option table, and description sits in one 4-space-indented block, which declares no language and renders as an unhighlighted grey slab. The site build now splits each block into paragraphs and classifies them: synopsis and shell examples become fenced `fish`, descriptions become prose, and column-aligned reference tables keep their indentation. 175 blocks highlight; the 412 lines of genuine tables are untouched. This transform is **site-only**. `docs/manual/**` keeps the indented form that pandoc and `config-help` depend on, and a test fails if a fence is ever written back to the SSOT. **`config-help`.** Now resolves sections against the manual tree's headings and gained a `--html` flag that opens the site. ## Deployment prerequisite The Cloudflare Pages project must exist before CI can deploy to it. Create `fish-config-docs` in **Direct Upload** mode and seed it with the pre-built bundle (`docs/site/dist/`, also zipped at `~/fish-config-docs-site.zip`), then add repository secrets `CF_API_TOKEN` (scope: *Cloudflare Pages: Edit*) and `CF_ACCOUNT_ID`. Until those exist the deploy step fails; nothing else in the workflow depends on it. ## Manual Verification - [x] `python3 docs/verify-manual.py` reports 16/16 passing - [x] `cd docs/site && npx astro build` completes and emits 120 pages - [x] `npx astro preview` — code blocks on `/functions/file-and-directory/rm/` are syntax-highlighted in Catppuccin colours, not flat grey - [x] The `pkg` page keeps its package-manager query table column-aligned rather than fencing it as shell - [x] The Key Bindings page's box-drawing table renders unchanged - [x] The sidebar shows one "Functions Reference" group, leading with "Overview", with title-cased category names - [x] Search returns results for a function name (e.g. `mkcd`) - [x] Toggle light/dark — both Catppuccin flavours read correctly - [x] `help config` opens the terminal manual and paginates - [x] `help config abbreviations` jumps to the Abbreviations section - [x] `help config --man` renders the compiled man page with correct headings - [x] `help config --html` opens the documentation site - [x] `git diff main -- docs/fish-config.md` shows only regeneration noise, no content loss
rootiest added 15 commits 2026-07-26 04:21:16 +00:00
Replace .lstrip('\n') with .removeprefix("\n") to preserve body text that
legitimately starts with blank lines. The serialize() function inserts exactly
one separator newline; removing only that one newline (via removeprefix) rather
than all leading newlines (via lstrip) maintains parse/serialize roundtrip
losslessness.

Adds regression test to verify bodies with leading blank lines roundtrip
correctly.
Adds docs/build-manual.py (--concat) and a round-trip test in
verify-manual.py that reproduces the pre-migration fish-config.md
exactly from docs/manual/.

Fixes found by the round-trip check, applied in split-manual.py and
re-run to regenerate docs/manual/:
- NAME/SYNOPSIS/TABLE OF CONTENTS had no sidebar.order, so they sorted
  after every numbered section instead of interleaving with DESCRIPTION
  in original document order. All manual pages now get sidebar.order
  from a single position counter matching source order.
- split_h1/split_h2 used .strip() on section bodies, which also ate
  leading indentation on the first body line, corrupting the 4-space
  indented code blocks that open SYNOPSIS and TABLE OF CONTENTS.
- The source's leading pandoc metadata block (title/section/header/
  date/author) was discarded entirely by the splitter. It's now parsed
  off via manualtools.parse and stashed on index.md under a "pandoc"
  key; build-manual.py re-emits it verbatim ahead of the first heading.
index.md is both the man-page LANDING section and a Starlight content
page. Astro errors on any frontmatter key outside the fixed
man/site/manTitle/helpKeywords schema, so folding the source
fish-config.md pandoc header (title/section/header/date/author) onto
index.md under a fifth "pandoc" key would break the docs-site build
the moment it's scaffolded.

Relocate that block to docs/manual/_pandoc.yml: a plain, fence-free
YAML file that Astro content collections ignore (leading underscore).
split-manual.py now extracts it as raw text (no yaml.safe_load/
safe_dump round-trip) so build-manual.py's --concat re-emits it
byte-for-byte instead of merely "verbatim" in comment only. Also guard
build_concat against a manual/ tree with no _pandoc.yml/index.md,
removing the unconditional index.md parse that previously raised
FileNotFoundError on such a tree.

Regenerated docs/manual/ via split-manual.py; concat output remains
byte-identical to the pre-migration docs/fish-config.md.
test_concat_roundtrips_original previously compared through
_normalise(), which strips trailing whitespace and drops blank lines.
Mutation testing showed it still passed after joining chunks with a
single newline, deleting all 635 blank lines, and appending trailing
double-spaces to every line. Blank lines are load-bearing for pandoc
(blank_before_header defaults on), so losing them merges paragraphs
and stops headings being headings while the test stays green.

Compare the raw got/want text directly to decide pass/fail. Keep
_normalise only to build the diff shown on failure, normalising both
sides first so whitespace noise doesn't swamp the real difference. If
the exact compare fails but the normalised sides match, say so
explicitly in the assertion message instead of emitting an empty diff.
build-docs.yml ran verify-manual.py before regenerating
docs/fish-config.md, so test_concat_roundtrips_original compared a
fresh concat of docs/manual/** against the stale, pre-push copy on
disk. Any ordinary edit under docs/manual/** — the exact trigger for
this workflow's paths filter — failed the job before it ever
regenerated anything.

Swap the "Generate concatenated markdown" and "Verify manual
integrity" steps so regeneration runs first. Verification still gates
pandoc and the auto-commit step. Also add docs/verify-manual.py to the
paths filter so edits to the integrity checker itself retrigger the
job.
Scaffold docs/site/ via `npm create astro@latest ... --template
starlight`. Extend the docs collection schema in
src/content.config.ts with the four custom frontmatter fields (man,
site, manTitle, helpKeywords) needed by the generator in a later
task, using z.strictObject so unrecognized keys fail the build
instead of being silently stripped by Zod's default behavior.

Ignore generated site output (node_modules, dist, .astro, generated
content, and sidebar.json) in .gitignore.
Add build_manual.build_site(), which walks docs/manual and emits the
Astro Starlight content collection: plain pages copied with pipeline-only
frontmatter (man/site/manTitle/helpKeywords) stripped, and *-functions/
category files exploded into one page per function plus a CardGrid/LinkCard
overview. Writes src/sidebar.json for astro.config.mjs to import. Wires
--site alongside the existing --concat flag.

Fixes two latent defects found while building the real site against the
strict content.config.ts schema:
- _split_entries now tracks fenced code blocks (like
  manualtools.shift_headings does) so a `## ` inside a fence can't be
  mistaken for an entry boundary.
- LinkCard title/description are escaped for JSX attribute context, since
  shell synopses routinely contain `<placeholder>` angle brackets that
  would otherwise open unterminated MDX/JSX parsing.

Also fixes the generated sidebar shape for the functions category: Starlight
0.39+ dropped support for a bare `autogenerate` sibling of `label` on a
top-level group, so the autogenerate config now nests inside `items`.

Verified with a full `astro build` (temporarily pointing astro.config.mjs
at the generated sidebar.json, then reverted since replacing that config
is a later task's deliverable): 120 pages built cleanly, no
content-collection/frontmatter/MDX errors.
Adds Node setup, an Astro site build, and a wrangler Pages deploy step
to the existing docs workflow, gated on docs/site/** via the paths
trigger. Also fixes astro.config.mjs's site: URL to match the
fish-config-docs Cloudflare Pages project name (it previously pointed
at fish-config.pages.dev, which is not the project being deployed).
docs/manual/** plus the Astro Starlight site (https://fish-config-docs.pages.dev/)
replace the Gitea wiki (docs/wiki/) and chunked offline HTML (docs/html/,
docs/html-style.html) as the published documentation surface. Retires the
now-unused docs/split-wiki.py and docs/split-manual.py generators alongside them.

- config-help --html now opens the published site root instead of resolving
  a local sitemap anchor; when a keyword was given it prints a note that deep
  links aren't available yet and to use the site's search box.
- open-url and docs/manual/05-functions/14-miscellaneous.md examples
  repointed from docs/html/index.html to the site URL.
- docs/manual/11-viewing-this-manual.md rewritten to document the four
  access paths: the website, the man page, in-terminal pager, and reading
  docs/manual/** directly. fish-config.index updated to match the new
  headings.
- README.md documentation section and Zoxide attribution link repointed at
  the site; corrected the stale "single source file" claim about
  fish-config.md.
- Regenerated docs/fish-config.md via build-manual.py --concat.
The manual is authored man-page style: every synopsis, example, option
table, and description sits in one 4-space-indented block. On the site
that renders as a single unhighlighted grey slab, because an indented
block declares no language.

Split each block into its paragraphs at site-build time and classify
them: synopsis and shell examples become ```fish fences, descriptions
become real prose, and column-aligned reference tables keep their
indentation. 175 blocks now highlight; the 412 lines of genuine tables
are left alone.

The transform is site-only. docs/manual/** keeps the indented form the
pandoc man-page pipeline and config-help depend on, and a test enforces
that no fence is ever written back to the SSOT.

Also:
- Point Expressive Code at the bundled Catppuccin Mocha/Latte themes so
  code blocks match the palette in catppuccin.css.
- Build the functions sidebar group explicitly. `autogenerate` labelled
  it with the raw directory slug and republished the directory index as
  a child of the group it already titled, producing the duplicate
  "Functions Reference" row.
- Skip `Synopsis:` lines when deriving card descriptions; they restated
  the calling convention the card already shows as its title.
- Widen the palette: tinted heading levels, inline code, links, card
  hover accents, aside accents, and table headers.

Fixes a bug where _split_entries stripped the leading indentation of an
entry's first line, detaching `Synopsis:` from the block it opens.
Swaps the two usages: green now carries the site title, H2 headings, and
the card hover accent; mauve moves to the tip aside it displaces.
rootiest merged commit 1bfcd519b8 into main 2026-07-26 04:27:30 +00:00
rootiest deleted branch docs-starlight-site 2026-07-26 04:27:31 +00:00
Sign in to join this conversation.