Deep reference for the site build script and Quartz. Read this when you need to edit what the site includes, add a redaction, or debug a build. The main flow is in SKILL.md.

The build script

garden/scripts/build-site-content.mjs is first-party, dependency-free Node. Run from the repo root: node garden/scripts/build-site-content.mjs. It wipes and rebuilds garden/content/ from repo source files, then Quartz turns garden/content/ into garden/public/.

All the lists below are declared at the top of the script for auditability. Editing the site’s contents = editing one of these lists (usually copying an existing entry). After any edit, re-run the script and preview.

Config lists — what to edit

MAPPINGS — whole directories of markdown + assets

Each entry recursively copies .md and images/PDFs from a source dir to a site folder, preserving subfolders (so ![[img.png]] embeds resolve).

{ src: "obsidian/configs", dest: "configs" },
  • Add a new top-level content area: add one entry, e.g. { src: "snippets", dest: "snippets" }.
  • Optional keys: readmeAsIndex: true (a folder’s README.md becomes its landing page), indexBasename: "SKILL.md" (named file becomes the folder index).
  • Asset types copied automatically: .png .jpg .jpeg .gif .svg .webp .pdf (see ASSET_EXT).
  • Dirs never entered: .git .obsidian .claude node_modules dist build coverage test src scripts syntaxes language-configuration themes .vscode (see SKIP_DIRS). .claude is skipped by the walker, so .claude/skills is mapped explicitly.

CODE_FILES — single config files as highlighted code pages

Each entry renders one file inside a fenced, syntax-highlighted block (Shiki) wrapped in a small page.

{ src: "dotfiles/git/.gitconfig", dest: "dotfiles/gitconfig.md", lang: "ini", title: ".gitconfig" },
  • Add a new dotfile / config page: copy a line, set src, dest (always .md), lang (Shiki language id, e.g. ini, toml, json, hcl, bash), and title.

CODE_DIRS — a directory of config files, by extension

Walks a dir recursively and renders every file with a matching extension as a code page. Used for the Terragrunt blueprint’s .hcl files.

{ src: "blueprints/01-terragrunt", dest: "blueprints/01-terragrunt", langByExt: { ".hcl": "hcl" } },
  • New .hcl under that dir → automatic. To cover another extension, add to langByExt (e.g. ".sh": "bash").

DOWNLOADS — binaries offered for download

Copies matching binaries as static assets and emits a small page linking to them.

{ src: "books/golang", dest: "books/golang", exts: [".epub"], title: "Learning Go", blurb: "…" },

SINGLE_FILES — one-off files with special handling

obsidian/MOC.mdMOC.md, and README.mdindex.md (the home page, home: true triggers link rewrites + H1 strip). Rarely needs changes.

Transforms the script applies

Per markdown file, in order:

  1. Tags line → frontmatter. The vault’s line-1 tags: #a #b becomes real YAML tags: frontmatter so Quartz renders tag chips. Files that already start with --- are left as-is.
  2. Redactions (see below) — applied to file contents and to generated dest paths (redactPath), so a redacted value can’t leak via a URL either.
  3. Home links (README only) — rewrites repo-relative links (obsidian/…, vscode/plugins/harp/…, the dotfiles) to site paths via HOME_LINK_REWRITES, and strips the # configs H1.

Code pages get a > [!info] header showing the source path, then the file body in a fence sized to survive any backticks inside.

Redaction (public-site safety)

REDACTIONS is a list of [from, to] string replacements applied everywhere in the public output. Each entry is something deliberately kept off the public site; the private repo keeps the real value.

const REDACTIONS = [
  ["123456789012", "123456789012"], // real AWS account id → docs placeholder
  ["123456789012", "123456789012"], // HashiCorp tutorial acct id in notes
  ["you@example.com", "you@example.com"], // personal email
];

To redact a new secret: add a [from, to] pair, re-run the script, then confirm zero leaks: grep -r '<secret>' garden/public must return nothing. Prefer redaction over deleting content — the note stays useful, the secret doesn’t ship.

Quartz / build gotchas

  • garden/content/ must NOT be in .gitignore. Quartz’s file discovery uses globby with gitignore: true hardcoded, so a .gitignore entry would make it skip the generated content (0 input files). It’s kept out of git via .git/info/exclude instead (which globby does not read). garden/public, node_modules, .quartz, .quartz-cache are in .gitignore.
  • The install-plugins npm script is broken (it references the old v4 externalPlugins and runs via tsx, which dies on .scss). Never run npm run build either — its prebuild hook calls that broken script. The correct v5 command is npm --prefix garden run quartz -- plugin install (populates garden/.quartz/plugins/ from quartz.lock.json). Needed after a fresh clone or when quartz.lock.json changes; the build won’t compile without it (Head.tsx imports ../../.quartz/plugins).
  • Config is YAML, not merged. garden/quartz.config.yaml overrides quartz.config.default.yaml wholesale — a custom file must be complete. Site title, baseUrl (configs.themaybe.uk), analytics-off, and the color palette live here.
  • CSS overrides → garden/quartz/styles/custom.scss (committed, imported by componentResources.ts). Never edit garden/.quartz/plugins/** — git-ignored and regenerated by plugin install every build, so edits vanish. High-specificity selectors there win over plugin CSS regardless of bundle order.
  • Run commands as npm --prefix garden run quartz -- <cmd> (cwd resolves to garden/, output to garden/public). Permission rules in .claude/settings.local.json allow this form plus node garden/scripts/build-site-content.mjs.

Commands

# regenerate site content from repo source (run from repo root)
node garden/scripts/build-site-content.mjs

# build + live preview at localhost:8080
npm --prefix garden run quartz -- build --serve

# build only (CI does this)
npm --prefix garden run quartz -- build

# after a fresh clone: fetch community plugins (from quartz.lock.json)
npm --prefix garden run quartz -- plugin install

# stop the preview server
lsof -ti tcp:8080 | xargs -r kill