Project: infra-wiki — two-audience docs publishing

Status: live — full site on wiki (LXC 115) for the tailnet, curated portfolio subset on Cloudflare Pages.

Publish the sanitized documentation vault so people without Obsidian can browse it — friends/family get the full picture over the tailnet, and a strictly curated subset goes public for professional use. One source, two audiences, no forking of content.

Design

One source, two builds

The infra-wiki repo carries content + config only — no vendored Quartz source. Builds run the pinned Quartz OCI image (ghcr.io/jackyzha0/quartz:sha-*) under rootless podman, so the host needs no Node toolchain:

  • ./build.sh → public/ — the complete site, deployed to wiki
  • ./build.sh --portfolio → public-portfolio/ — staged through portfolio.exclude (drops whole pages) plus marker-driven stripping, deployed to Pages (huevoconchorizo.pages.dev)
  • ./deploy.sh local|portfolio — rsync --delete to the webroot, or containerized wrangler upload; creds resolve env → .deploy.env → pass
  • ./sync-content.sh — pulls the sanitized docs mirror into content/; curation markers live in infra-docs, never edited in the wiki copy

Curation markers

private HTML-comment markers in the source strip content only from the portfolio build — invisible comments on the full site. Three patterns: standalone-line block regions, single-line inline spans, and a trailing marker that drops a whole line (table rows). An empty pair after a wikilink unlinks it for the portfolio while keeping the link on the full site.

build.sh validates before stripping and fails closed on malformed markers — the classes that actually bit during development:

  • cross-line spans — open/close on different lines silently ate visible text (the residue rule deleted whole lines)
  • markers in headings/code fences — Quartz renders them literally there; they’re not invisible comments in those contexts
  • any surviving marker — hard error after the strip pass

Serving

Quartz emits leaf pages as x.html + folder x/index.html — “clean URLs” that static hosts resolve natively but a bare file_server doesn’t. The guest Caddyfile needs try_files {path} {path}.html {path}/index.html. Details in wiki.

Self-containment

No runtime CDN: fonts and the graph libraries (d3, pixi) are vendored. A tiny local theme-default plugin seeds dark mode for first-time visitors — exports must be factories returning components, since Quartz’s registry calls the export and uses the return value (a null-rendering component gets skipped before its resources collect). Per-audience pageTitle overrides come from env/pass so the real project name never enters the repo.

Gotchas worth remembering

  • Quartz’s CNAME emission is a GitHub Pages convention — inert on Cloudflare Pages.
  • rsync --delete on the webroot keeps emitted-file maps (contentIndex.json) and HTML in sync; a partial deploy could leave stale link maps.
  • Cloudflare Pages noindexs per-deploy preview URLs only — the production *.pages.dev hostname is indexable.