/CLAUDE.md
CLAUDE.md at /CLAUDE.md
Path: CLAUDE.md
zudo-doc
Minimal documentation framework built with zfb, MDX, Tailwind CSS v4, and Preact islands.
(Originally built on Astro 6; migrated to zfb in epic zudolab/zudo-doc#1333. Some historical references to Astro tooling may still surface in long-form prose elsewhere — they describe legacy state, not the current authoring target.)
Tech Stack
zfb (
@takazudo/zfb) — static site generator with MDX content collections, file-routedpages/, and a built-in dev/build/preview/check CLIMDX — authored under
src/content/, content directory configurable viadocsDirsetting; pipeline configured inzfb.config.tsTailwind CSS v4 — via
@tailwindcss/vitePreact — for interactive islands (TOC scroll spy, sidebar toggle, collapsible categories) and server-rendered content typography components; runs in compat mode for React API compatibility
syntect — built-in code highlighting, run by zfb's Rust pipeline at build time, configured for dual-theme output in
zfb.config.ts(codeHighlight.themeLight/themeDark=base16-ocean.light/base16-ocean.dark). Tokens are emitted as--shiki-light/--shiki-darkCSS custom properties (not inline colors) andsrc/resolves them viastyles/ global. css light-dark(), so code follows the light/dark toggle with no client JS. TheshikiThemefield on each color scheme is unrelated and vestigial — it rides along in zdtp's color-scheme config envelope (optional since zdtp 0.2.3) but has no visible effect: zdtp's Shiki preview is a no-op stub, and page highlighting is syntect's.@takazudo/zdtp (zdtp) — external npm package that owns the Design Token Panel UI; wired via
configurePanel(designTokenPanelConfig)insrc/; self-mounts as a side-effect (no Preact island registration needed)lib/ design- token- panel- bootstrap. ts TypeScript — strict mode (project
tsconfig.jsonsetsstrict: trueplus the full set ofstrict*flags directly)
Commands
pnpm dev— runs zfb dev (port 4321), doc-history-server (port 4322), a.claude/watcher, and tsup--watchfor@takazudo/zudo-docconcurrently viarun-p; edits to.claude/files regenerate the corresponding MDX live, and edits topackages/auto-rebuildzudo- doc/ src/ ** dist/so zfb HMR picks them up. If a previous dev process is still bound to 4321 / 4322, the new launch fails fast withEADDRINUSE— kill it manually before retrying (e.g.lsof -ti :4321 -ti :4322 | xargs -r kill, after confirming the matched PIDs are actually yours). The hook used to do this automatically, but matching by port alone meantpnpm devwould silently kill unrelated apps on the same port (4321 is the Vite default), which is the bug we're trading away.pnpm dev:zfb— zfb dev server only (port 4321)pnpm dev:history— doc history API server only (port 4322)pnpm dev:zudo-doc— tsup--watchfor@takazudo/zudo-doconly; host imports resolve throughdist/because the package now ships compiled JS (W8 Blocker-2 fix — Node 24 rejects raw.tsinnode_modules, so the package's source is private and dist is the API surface)pnpm dev:stable— alternative build-then-serve dev mode (avoids HMR crashes on content file add/remove)pnpm dev:network— zfb dev with--host 0.0.0.0for LAN accesspnpm build— static HTML export todist/(runszfb build)pnpm preview— serve the builtdist/(runszfb preview)pnpm check— type checking (runszfb check, which delegates totsc --noEmit)pnpm b4push— pre-push validation: 19-step suite (format check → template drift → pin parity → fixture drift → tags audit → token lint → z-index drift → e2e spec naming guard → @flaky tracking-issue guard → b4push/CI parity → typecheck → root unit tests → package tests → safelist check → build → link check → html validation → preview smoke → manual smoke); Playwright E2E runs in CI (pr-checks e2e job) and is intentionally excluded from b4push for time-budget reasons — seeTESTING.mdfor the full tier rationalepnpm test— unified test entry point: builds@takazudo/zudo-docdist/ then runs root unit tests (test:unit) and workspace package tests (test:packages); does not include e2e
First-time setup on a new machine
zfb and zdtp are consumed as published npm packages — there is no sibling-checkout build step. A plain install pulls everything, including zfb's prebuilt platform binary (shipped via a platform-specific npm optionalDependency, e.g. @takazudo/zfb-linux-x64-gnu):
pnpm installVersions are pinned in package.json (@takazudo/zfb, @takazudo/zfb-runtime, @takazudo/zfb-adapter-cloudflare, and @takazudo/zdtp) — that file is the single source of truth for which upstream versions this project builds against.
Editing zfb / zdtp from source (escape hatch)
When you need to develop against a local zfb or zdtp checkout (e.g. fixing an upstream bug), use a temporary pnpm.overrides entry in package.json pointing the package at a local path, then pnpm install:
"pnpm": {
"overrides": {
"@takazudo/zfb": "link:../zfb/packages/zfb"
}
}Run pnpm install to wire the link, do your work, then remove the override and re-run pnpm install to restore the published version. Do not commit the override.
Automation
These run automatically — be aware when working in this repo:
lefthook pre-commit (
lefthook.yml): on commit, staged*.mdand*.mdxfiles are formatted with@takazudo/mdx-formatterand re-added. You do not need to manuallypnpm formatmarkdown before committing.prepare:
pnpm installrunslefthook installandscripts/(the worktree push-guard hook). zfb and zdtp come straight from npm — there is no link/build postinstall step anymore.install- git- hooks. sh
Worktree push policy (enforced)
This repo uses / for multi-topic development. Child agents work in git worktrees under worktrees/. Pushing from a worktree is forbidden. Only the manager session — running from the main repo at the repo root — pushes, after merging topic branches into the base branch locally.
Why
CI runs on every push. Children pushing pre-empt the manager's merge + review step, multiplying CI cost across intermediate state.
Topic branches in
worktrees/*/are intermediate by design — they shouldn't appear as standalone PRs unless the manager creates them.
How it's enforced
. is a direct script (not managed via lefthook.yml) that blocks any push from a git worktree. It is auto-installed by pnpm install (via the prepare lifecycle script) and can be re-installed manually with:
pnpm init-worktreeThe installer source lives at scripts/; the hook itself at scripts/.
Emergency bypass (human use)
ALLOW_WORKTREE_PUSH=1 git push ...Use only when you genuinely need to push from a worktree (rare). Never set this in agent prompts.
Guidance for agents
Child agents working in
worktrees/*/: commit locally only. Pushing will fail with the message above — do not retry, do not invoke the bypass. Report back to the manager with the branch name and commit SHAs; the manager merges and pushes from the main repo./manager session: the hook does not affect you. Yourx- wt- teams git pushruns from the main repo (the cwd is the repo root, notworktrees/...). After every wave's local merges, push as usual. Do not passALLOW_WORKTREE_PUSHto children.
Key Directories
zfb. config. ts # zfb engine config (content collections, MDX pipeline, plugins)
plugins/ # zfb engine plugins (doc- history, llms- txt, search- index, . . . )
pages/ # File- routed pages (. tsx) — zfb resolves these
├── docs/ [. . . slug] # English doc routes
├── [locale]/ docs/ [. . . slug] # Locale- prefixed doc routes (e. g. / ja/ docs/ . . . )
├── api/ # API routes (e. g. ai- chat)
└── sitemap. xml. tsx # Sitemap generator
packages/
├── md- plugins/ # Legacy JS remark/ rehype plugins (superseded by zfb Rust pipeline; kept for fixture/ unit- test coverage)
├── search- worker/ # CF Worker for search API
├── doc- history- server/ # Doc history REST API + CLI generator
├── zudo- doc/ # Shared layout + integration package (header, doc- layout, . . . )
└── create- zudo- doc/ # CLI scaffold tool
src/
├── components/ # Preact components (. tsx) — islands and server- rendered overrides
│ └── content/ # MDX element overrides (server- rendered, no client JS)
├── config/ # Settings, color schemes, tag vocabulary
├── content/
│ ├── docs/ # English MDX content
│ └── docs- ja/ # Japanese MDX content (mirrors docs/ )
└── styles/
└── global. css # @theme tokens, feature styles, slots; @imports the
# shared content stylesheet @takazudo/ zudo- doc/ content. cssContent Collections
Schema and collection wiring live in
zfb.config.ts(Zod validation)Loaded via zfb's MDX content pipeline with a configurable
basedirectory from settingsContent directories:
docsDir(default:src/),content/ docs docsJaDir(default:src/)content/ docs- ja
Terminology: "Update docs"
When we say "update docs" or "update our doc," it means updating the showcase documentation content in src/ (English) and src/ (Japanese). Since zudo-doc is a documentation framework, its own content directories serve as the default showcase. These are the pages visible when running pnpm dev.
i18n
English (default):
/— content indocs/ . . . docsDir(default:src/)content/ docs Japanese:
/— content inja/ docs/ . . . docsJaDir(default:src/)content/ docs- ja Configured in
zfb.config.tswithprefixDefaultLocale: falseBilingual rule: when creating or updating any doc page, update both EN and JA versions. Detailed exceptions and the content-writing workflow live in
src/(auto-loaded when working on content).content/ CLAUDE. md
Doc Skill (setup-doc-skill)
The doc-skill (scripts/) generates . and symlinks docs into it. It is gitignored — do NOT track the generated SKILL.md in git. Run pnpm setup:doc-skill to regenerate. To update the skill template, edit scripts/.
This script is also the source template copied to downstream projects by create-zudo-doc when the skillSymlinker feature is enabled.
Doc History Architecture
Document git history is handled by a standalone package @takazudo/zudo-doc-history-server (at packages/doc-history-server/). It is intentionally decoupled from the main build pipeline so that expensive git log --follow calls do not block the main build.
It runs in two modes:
Server mode (local dev) — HTTP server on port 4322, started by
pnpm dev:history. The zfb plugin atplugins/proxiesdoc- history- plugin. mjs /requests to it.doc- history/ * CLI mode (CI) — batch-generates JSON files into
dist/doc-history/. Used by thebuild-historyCI job in parallel with the main site build.
SKIP_DOC_HISTORY env var
When SKIP_DOC_HISTORY=1 is set, the doc-history plugin short-circuits and writes an empty manifest ({}), skipping all git history calls. This causes the visible Created/Updated/Author block to be absent from every SSG page. Use only when intentionally bypassing git-based meta generation (e.g. a truly shallow clone or a custom CI variant).
GEN_DOC_HISTORY env var (local postBuild opt-in)
The plugin's postBuild step (which writes the per-page history-dropdown JSON into dist/doc-history/) is skipped by default on local builds and opt-in via GEN_DOC_HISTORY=1 (#1986). It defaults off locally because that step runs one git log --follow chain per content file, which on a large corpus exceeds zfb's 120s postBuild lifecycle-hook budget and fails a plain pnpm build. The JSON is redundant for the normal paths anyway: dev reads it live from the :4322 server, and CI generates it in the dedicated parallel build-history job. The decision table (in runDocHistoryPostBuild / shouldGeneratePostBuild):
SKIP_DOC_HISTORY=1→ never generate (wins over everything).GEN_DOC_HISTORY=1→ generate (local opt-in — e.g. beforepnpm previewof a locally-builtdist/).CI (
CI/GITHUB_ACTIONS) → generate (keeps the CI build-site artifact identical; the async generator stays within budget).otherwise (plain local build) → skip.
This gates only the postBuild dropdown JSON. The preBuild Created/Updated/Author manifest (gated by SKIP_DOC_HISTORY alone) still runs locally, so a plain pnpm build keeps real page metadata.
CI Pipeline
All three workflows (main-deploy.yml, pr-checks.yml, preview-deploy.yml) use parallel build jobs:
build-site — full clone (
fetch-depth: 0),pnpm build— preBuild populates.with real git dates so the SSG HTML contains the visible Created/Updated/Author blockzfb/ doc- history- meta. json build-history — full clone (
fetch-depth: 0),@takazudo/zudo-doc-history-server generate— generates per-page dropdown JSON files for the DocHistory islanddeploy/preview — merges both artifacts, deploys via
wrangler deployto Cloudflare Workers static assets atzudo-doc.takazudomodular.com
E2E tests also run with full clone (no SKIP_DOC_HISTORY).
Workers Cutover Runbook
One-time setup steps required before the first wrangler deploy succeeds for this project (epic zudolab/zudo-doc#1691). Run from the repo root with Wrangler authenticated.
1. Create the RATE_LIMIT KV namespace
wrangler kv namespace create RATE_LIMITCopy the returned id value and paste it into wrangler.toml under [[kv_namespaces]]:
[[kv_namespaces]]
binding = "RATE_LIMIT"
id = "<paste-id-here>"2. Add the Anthropic API key as a secret
wrangler secret put ANTHROPIC_API_KEYPaste the key when prompted. The value is stored in Cloudflare's secret store and never appears in wrangler.toml.
2b. (Optional) Add the IP-hash HMAC secret
wrangler secret put IP_HASH_SECRETOptional and non-breaking. When set, the ai-chat rate limiter and 7-day audit log key client IPs with HMAC-SHA-256(ip) instead of unsalted SHA-256(ip), which defeats reversing the stored hashes by enumerating the (small) IPv4 space (#2038). When the secret is absent the worker falls back to the original unsalted SHA-256, so existing deployments behave identically and the step can be skipped.
Rotation caveat. Setting or rotating
IP_HASH_SECRETchanges every derived key. In-flight rate-limit buckets reset (acceptable — 60s windows) and audit-entry hash continuity breaks for the current 7-day window (acceptable — entries age out).
3. Verify DOCS_SITE_URL
wrangler.toml already sets DOCS_. For preview deploys, override per-deploy:
wrangler deploy --var DOCS_SITE_URL=<preview-url>Or override via the Cloudflare dashboard per environment to avoid preview workers pointing at production docs.
Search worker (optional, opt-in deployment). The showcase site does NOT deploy
packages/search-worker/— on-site search is a custom-scorer widget (pages/) that fetcheslib/ _ search- widget. tsx search-index.jsonfromdist/and runs a built-in word-match scoring loop (MiniSearch is not imported by the host; the worker package has its ownminisearchdep). The worker exists as a template/example for downstream users who want a server-side search API for huge doc bases or programmatic API consumers. If you choose to deploy it, two caveats apply:
packages/carries its ownsearch- worker/ wrangler. toml DOCS_SITE_URL(used for CORS/referrer). A Cloudflare dashboard environment-variable override on the search Worker shadows the file value and persists across deploys, so if you've ever set one, clear/replace it when redeploying.The search worker also has its own
RATE_LIMITKV namespace (separate from the main worker's).packages/ships with a placeholder id — seesearch- worker/ wrangler. toml packages/for the creation runbook (search- worker/ README. md wrangler kv namespace create RATE_LIMITrun from that directory). Deploying without replacing the placeholder produces error code 10042.
4. Bind the custom domain
wrangler.toml already contains:
[[routes]]
pattern = "zudo-doc.takazudomodular.com"
custom_domain = trueThe domain binding is activated on first wrangler deploy. Ensure the DNS record for zudo-doc.takazudomodular.com exists in the Cloudflare zone (CNAME or proxied A record pointing at the Worker). Cloudflare will issue a certificate automatically.
5. Pages project deletion (Wave 5 — #1698)
The legacy zudo-doc Cloudflare Pages project and its zudo-doc.pages.dev subdomain remain active until Wave 5 (#1698). Do not delete the Pages project until that wave is explicitly greenlit.
Feature Change Checklist
When adding or removing a feature from zudo-doc, update the create-zudo-doc generator to stay in sync:
src/— Add/remove the setting fieldconfig/ settings. ts packages/— Add/remove the setting in generated outputcreate- zudo- doc/ src/ settings- gen. ts packages/— Create/update feature module with injectionscreate- zudo- doc/ src/ features/ <name>. ts packages/— Add/remove feature-specific filescreate- zudo- doc/ templates/ features/ <name>/ files/ packages/— If the feature introduces a new plugin or collection, updatezudo- doc/ src/ preset. ts zudoDocPreset()to wire it fromsettings.*. The generatedzfb.config.tsis now a thin preset-based file (S5b #2329) — it delegates all plugin/collection/markdown logic to the preset and reads the settings field you added in step 1.packages/does NOT need updating for features that are settings-driven (the preset handles them).create- zudo- doc/ src/ zfb- config- gen. ts packages/— Add/remove dependencies increate- zudo- doc/ src/ scaffold. ts generatePackageJson()packages/— Update testscreate- zudo- doc/ src/ _ _ tests_ _ / scaffold. test. ts Run
/to verify no drift remainsl- update- generator
e2e fixture sync: Adding a field to src/ also requires mirroring it into all five e2e/ files — or adding an allowlist entry in .fixture-settings-drift-allowlist with a # reason: comment. This is now enforced in CI by the Fixture Settings Drift Check job.
Important: This checklist also applies to incremental improvements (CSS token migrations, icon sizing, spacing changes, etc.) — not just new features. If you change a file that has a template counterpart, update the template too. Run pnpm check:template-drift to verify (note: allowlisted files such as src/, plugin re-exports, and other slot-based files listed in .template-drift-allowlist are excluded from automated checks and need manual review).
Content typography (.zd-content) is NOT per-project — it ships from the package. The content/markdown stylesheet lives once at packages/ (shipped as @takazudo/) and is @imported by both src/ and the generator template. Change content typography there — do NOT re-inline .zd-content rules into either global.css (that copy-drift is exactly what zudolab/zudo-doc#2188 retired). Both global.css files keep only @theme tokens, feature styles, and slots. When editing content.css, rebuild the package (pnpm --filter @takazudo/zudo-doc build) so dist/ updates for consumers. Note: generated projects only pick up content.css changes after a new @takazudo/zudo-doc version is published and create-zudo-doc's pinned dependency is bumped (the lockstep release handles this).
Tauri (two modes)
zudo-doc ships two independent Tauri apps:
Mode 1 — Standalone offline reader (src-tauri/)
Bundles zudo-doc's own pre-built dist/ into a self-contained desktop app.
Build (shipped product):
cargo tauri buildEmbedsdist/viafrontendDist; WebView loadsWebviewUrl::App. There is nobeforeBuildCommand, so build the embeddeddist/first — and useGEN_DOC_HISTORY=1 pnpm buildso the offline reader includes the per-page history-dropdown JSON (postBuild JSON is opt-in for local builds, #1986; a plainpnpm buildwould silently ship adist/without it).cargo tauri dev(contributor convenience only): Runspnpm devviabeforeDevCommandand opens the WebView athttp:(the zfb dev server). This is NOT a shipped product — it exists solely for zudo-doc contributors who want to work on both the Tauri shell and site content at the same time. The/ / localhost: 4321/ beforeDevCommand/devUrlfields insrc-must be kept for this workflow.tauri/ tauri. conf. json
Mode 2 — Configurable dev wrapper for end users (src-tauri-dev/)
A standalone Tauri app that any project can use as a desktop dev wrapper. It reads the target project URL and settings from a per-user config file rather than hard-coding anything.
Build (shipped product):
cd src-tauri-dev && cargo tauri buildConfig file (macOS):
~/(Windows/Linux paths differ; seeLibrary/ Application Support/ com. takazudo. zudo- doc- dev/ config. json src-tauri-dev/for details.)
Key distinction
Mode 1 cargo tauri dev and Mode 2 are both "dev wrappers" superficially, but they target
completely different audiences. Mode 1 dev is a repo-internal contributor convenience
(hard-coded to this project, not shipped). Mode 2 is a product delivered to end users of
any project (configurable, shipped as a standalone installer).
See src- for a full comparison table.
Testing
See TESTING.md (repo root) for the full testing strategy — levels (L1 vitest through L6 test-flow skills), tiers (T0 local fast pass / T1 CI gates / T3 nightly exam), tag taxonomy (@flaky quarantine rules), retry budgets, anti-gaming rules, and wait-pattern rules.
Directory-scoped CLAUDE.md files
These auto-load when working in the corresponding directory — read them when relevant work is in scope:
src/— components, design tokens, three-tier color/font-size strategy, CSS rulesCLAUDE. md src/— tag vocabulary and tag governanceconfig/ CLAUDE. md src/— doc-writing rules (frontmatter, admonitions, linking, bilingual workflow)content/ CLAUDE. md e2e/— Playwright fixture architecture and how-to (policy in TESTING.md)CLAUDE. md packages/— per-package architecture notes (workers, generator, doc-history-server)*/ CLAUDE. md