docs: add Fumadocs documentation site - #448
Conversation
📝 WalkthroughWalkthroughAdds a standalone Fumadocs and React Router documentation site with installation content, static prerendering, search, LLM endpoints, SPA fallbacks, local tooling, and Cloudflare Pages deployment automation. ChangesPublic documentation site
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Browser
participant DocsRoute
participant FumadocsSource
participant DocsPage
Browser->>DocsRoute: request /docs/*
DocsRoute->>FumadocsSource: load page by slug
FumadocsSource-->>DocsRoute: page tree and metadata
DocsRoute->>DocsPage: render MDX content
DocsPage-->>Browser: documentation page
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/deploy-docs.yml:
- Line 65: Update the Wrangler command in the deploy workflow to use an
intermediate environment variable for the selected branch instead of
interpolating github.head_ref or other GitHub branch context directly. Define
the existing branch-selection expression under env, then quote that environment
variable in the command so Wrangler receives it as a single argument and shell
injection is avoided.
In `@website-docs/content/docs/index.mdx`:
- Around line 19-23: Replace the direct piped execution of the mutable
main-branch installer in the documentation’s installer instructions with a
version-pinned download flow. In the “Or use the installer script” section,
instruct users to download the versioned installer, independently verify its
checksum or signature, and execute the verified local file; update the repeated
command block as well.
In `@website-docs/react-router.config.ts`:
- Around line 17-22: Update the prerender path construction in the route
configuration to include the API search route, both LLM text routes, and the
generated llms.mdx documentation paths alongside the existing documentation
URLs. Ensure these routes are returned in the prerender paths so their loaders
execute during the build.
In `@website-docs/README.md`:
- Line 26: Update the deployment statement in the README to use future-tense
wording or explicitly mark https://docs.termul.dev as pending, avoiding any
claim that the custom domain is currently live.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: d4b8035e-cff1-479b-9f07-7402f2e9fa47
⛔ Files ignored due to path filters (2)
website-docs/bun.lockis excluded by!**/*.lockwebsite-docs/public/favicon.icois excluded by!**/*.ico
📒 Files selected for processing (31)
.github/workflows/deploy-docs.ymlpackage.jsonwebsite-docs/.gitignorewebsite-docs/PLAN.mdwebsite-docs/README.mdwebsite-docs/app/app.csswebsite-docs/app/components/mdx.tsxwebsite-docs/app/components/search.tsxwebsite-docs/app/lib/cn.tswebsite-docs/app/lib/layout.shared.tsxwebsite-docs/app/lib/shared.tswebsite-docs/app/lib/source.tswebsite-docs/app/llms/full.tswebsite-docs/app/llms/index.tswebsite-docs/app/llms/mdx.tswebsite-docs/app/root.tsxwebsite-docs/app/routes.tswebsite-docs/app/routes/docs.tsxwebsite-docs/app/routes/home.tsxwebsite-docs/app/routes/not-found.tsxwebsite-docs/app/routes/search.tswebsite-docs/content/docs/index.mdxwebsite-docs/package.jsonwebsite-docs/postcss.config.jswebsite-docs/public/_redirectswebsite-docs/react-router.config.tswebsite-docs/serve.jsonwebsite-docs/source.config.tswebsite-docs/tsconfig.jsonwebsite-docs/vite.config.tswebsite-docs/wrangler.toml
| apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | ||
| workingDirectory: website-docs | ||
| command: pages deploy build/client --project-name=termul-docs --branch=${{ github.ref_name == 'main' && 'main' || github.head_ref || github.ref_name }} --commit-dirty=true |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
workflow=".github/workflows/deploy-docs.yml"
if rg -n --pcre2 '\$\{\{[^}]*github\.(head_ref|ref_name)[^}]*\}\}.*--branch' "$workflow"; then
echo "Untrusted branch context is still interpolated into the command" >&2
exit 1
fi
rg -n -- 'DEPLOY_BRANCH|--branch="\$DEPLOY_BRANCH"' "$workflow"Repository: gnoviawan/termul
Length of output: 154
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "--- workflow excerpt ---"
sed -n '1,120p' .github/workflows/deploy-docs.yml | cat -n
echo
echo "--- trigger and pr references ---"
rg -n -- 'pull_request|github\.head_ref|github\.ref_name|pages deploy|wrangler|cloudflare' .github/workflows/deploy-docs.yml .github/workflows || true
echo
echo "--- surrounding repository action usage ---"
fd -a .github/workflows .github/workflows -x sh -c 'echo "### $1"; sed -n "1,120p" "$1"' sh {}Repository: gnoviawan/termul
Length of output: 5969
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "--- locked wrangler-action commit availability/annotations ---"
GITHUB_TOKEN="$GITHUB_TOKEN" gh api repos/cloudflare/wrangler-action/git/refs/tags/v3.15.0 | jq -r '
"\ttag ref type=\(.object.type) sha=\(.object.sha)",
if .object.type == "tag" then
"\ttarget tag object:" + (gh api "repos/cloudflare/wrangler-action/git/tags/\(.object.sha)" | jq -r "\t\ttype=\(.object.type) sha=\(.object.sha)") // ""
else
"\ttarget commit: \(.object.sha)"
end
' || true
echo
THAT_SHA="9acf94ace14e7dc412b076f2c5c20b8ce93c79cd"
echo "--- locked action metadata/files ---"
gh api "repos/cloudflare/wrangler-action/contents/action.yml?ref=${THAT_SHA}" | jq '.content' | base64 -d | cat -n
gh api "repos/cloudflare/wrangler-action/contents/src/action.mjs?ref=${THAT_SHA}" | jq '.content' | base64 -d | sed -n '1,220p' | cat -nRepository: gnoviawan/termul
Length of output: 627
🌐 Web query:
cloudflare wrangler-action v3 command implementation command executable shell
💡 Result:
The cloudflare/wrangler-action executes commands within a GitHub Actions environment using a shell-based execution model [1]. When you provide a custom command via the command, preCommands, or postCommands inputs, the action processes these commands through its internal execution logic [2][3]. For commands defined in preCommands and postCommands, the action uses the execShell function, which determines how to invoke the command [1]. If a command string begins with "wrangler", the action automatically prefixes it with the configured package manager's execution command (e.g., npx wrangler, pnpm wrangler) [1]. If the command does not start with "wrangler", it is executed directly as a shell command in the specified working directory [1]. For the primary command input (which defaults to "deploy"), the action handles execution through a dedicated wranglerCommands function [2][1]. This function: 1. Orchestrates the execution by prepending the package manager's execution path (e.g., npx) and the wrangler CLI prefix [1]. 2. Optionally injects environment flags (--env) and variable bindings (--var) based on your workflow configuration [1]. 3. Executes the resulting command string using the underlying GitHub Actions toolkit exec function [1]. 4. Captures stdout and stderr streams, which are then exposed as GitHub Action outputs (command-output and command-stderr) for use in subsequent steps [1][3]. In essence, while the primary command input is structured to specifically facilitate Wrangler operations, the preCommands and postCommands inputs provide a mechanism for running arbitrary shell commands alongside your deployment process [2][1][3].
Citations:
- 1: https://github.com/cloudflare/wrangler-action/blob/main/src/wranglerAction.ts
- 2: https://deepwiki.com/cloudflare/wrangler-action/3.4-custom-commands
- 3: https://github.com/cloudflare/wrangler-action/blob/main/action.yml
Do not interpolate GitHub branch context directly into Wrangler's command.
This workflow runs on PRs, so github.head_ref can contain attacker-controlled repository names. Pass the selected branch through an intermediate env value and quote it in command so Wrangler receives an argument rather than a shell-injective context.
Proposed fix
+ env:
+ DEPLOY_BRANCH: ${{ github.ref_name == 'main' && 'main' || github.head_ref || github.ref_name }}
with:
- command: pages deploy build/client --project-name=termul-docs --branch=${{ github.ref_name == 'main' && 'main' || github.head_ref || github.ref_name }} --commit-dirty=true
+ command: pages deploy build/client --project-name=termul-docs --branch="$DEPLOY_BRANCH" --commit-dirty=true📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| command: pages deploy build/client --project-name=termul-docs --branch=${{ github.ref_name == 'main' && 'main' || github.head_ref || github.ref_name }} --commit-dirty=true | |
| env: | |
| DEPLOY_BRANCH: ${{ github.ref_name == 'main' && 'main' || github.head_ref || github.ref_name }} | |
| with: | |
| command: pages deploy build/client --project-name=termul-docs --branch="$DEPLOY_BRANCH" --commit-dirty=true |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/deploy-docs.yml at line 65, Update the Wrangler command in
the deploy workflow to use an intermediate environment variable for the selected
branch instead of interpolating github.head_ref or other GitHub branch context
directly. Define the existing branch-selection expression under env, then quote
that environment variable in the command so Wrangler receives it as a single
argument and shell injection is avoided.
Source: MCP tools
| Or use the installer script: | ||
|
|
||
| ```bash | ||
| curl -fsSL https://raw.githubusercontent.com/gnoviawan/termul/main/scripts/install.sh | bash | ||
| ``` |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Do not execute an unpinned remote script directly.
Both commands execute whatever main/scripts/install.sh serves at install time. Publish a versioned installer with an independently verifiable checksum/signature, then instruct users to download, verify, and run it locally. The current installer does verify release assets, but the bootstrap script itself remains mutable and unverified. (raw.githubusercontent.com)
Also applies to: 31-35
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@website-docs/content/docs/index.mdx` around lines 19 - 23, Replace the direct
piped execution of the mutable main-branch installer in the documentation’s
installer instructions with a version-pinned download flow. In the “Or use the
installer script” section, instruct users to download the versioned installer,
independently verify its checksum or signature, and execute the verified local
file; update the repeated command block as well.
| for await (const entry of glob('**/*.mdx', { cwd: 'content/docs' })) { | ||
| const slugs = getSlugs(entry) | ||
| paths.push(getUrl(slugs), `/llms.mdx/docs/${[...slugs, 'content.md'].join('/')}`) | ||
| } | ||
|
|
||
| return paths |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate config and routes =="
git ls-files | rg '(^|/)react-router\.config\.ts$|(^|/)routes\.ts$|website-docs' || true
echo
echo "== react-router config =="
if [ -f website-docs/react-router.config.ts ]; then
cat -n website-docs/react-router.config.ts
fi
echo
echo "== app routes files =="
fd -a 'routes\.ts$' website-docs 2>/dev/null || true
echo
echo "== package refs =="
for f in package.json website-docs/package.json pnpm-lock.yaml package-lock.json yarn.lock; do
[ -f "$f" ] && echo "--- $f" && (rg -n '"react-router"|react-router|react-router-dom|vite-plugin-ssr|vercel|pages' "$f" || true)
doneRepository: gnoviawan/termul
Length of output: 2463
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== app/routes.ts =="
cat -n website-docs/app/routes.ts
echo
echo "== llms/search route handlers =="
for f in website-docs/app/routes/search.ts website-docs/app/llms/index.ts website-docs/app/llms/mdx.ts website-docs/app/llms/full.ts; do
if [ -f "$f" ]; then
echo "--- $f"
cat -n "$f" | awk '{printf "%4d %s\n", NR, $0}'
else
echo "--- missing $f"
fi
done
echo
echo "== server api refs/search handlers =="
rg -n "api/search|/api/search|handler\(|loader\(|action\(|export .*handler|llms|llms-full|llms.txt" website-docs/app website-docs/public website-docs/vite.config.ts website-docs/react-router.config.ts || trueRepository: gnoviawan/termul
Length of output: 3629
🌐 Web query:
React Router v8 ssr false prerender output static routes loaders server-side rendered routes
💡 Result:
In React Router v8, setting ssr: false in your configuration file disables runtime server-side rendering, effectively putting the application into "SPA Mode" by default [1][2]. However, you can still leverage static pre-rendering while keeping ssr: false to generate static HTML and data for specific routes at build time [1][3]. When ssr: false is configured: 1. SPA Mode (Default): If you do not provide a prerender configuration, React Router renders only the root route into a single index.html file at build time [1][2]. In this mode, you can use a loader in the root route, but you cannot use loaders in any other routes because the application determines which child routes to load only after hydrating in the browser [1][3]. 2. Pre-rendering with ssr: false: If you want to use loaders in routes other than the root, you must explicitly configure pre-rendering [1][3]. By defining routes to be pre-rendered, React Router will execute the associated loaders at build time to generate static HTML and data payloads for those specific paths [1][4][5]. Important Constraints: - Prohibited Functions: You cannot use actions or headers functions in any routes when ssr: false is set, as there is no runtime server available to execute them [1][3]. - SSR-Safe Code: Even with ssr: false, your application code must remain "SSR-safe" (i.e., avoiding browser-only APIs like window during initial render) because React Router performs an initial render of the root route at build time [2][6]. - Loaders: When using pre-rendering with ssr: false, loaders are permitted on any route explicitly matched by your pre-rendering configuration [3]. If a pre-rendered route has child routes, you must ensure that parent loader data can be determined at runtime by either pre-rendering the child routes or using clientLoader to handle data loading on the client [3]. You can configure these settings in your react-router.config.ts file [1][4]: export default { ssr: false, // Disables runtime SSR prerender: true, // Example: pre-renders all static routes defined in your routes } satisfies Config;
Citations:
- 1: https://reactrouter.com/how-to/pre-rendering
- 2: https://reactrouter.com/how-to/spa
- 3: https://raw.githubusercontent.com/remix-run/react-router/main/docs/how-to/pre-rendering.md
- 4: https://reactrouter.com/start/framework/rendering
- 5: https://unpkg.com/react-router@8.0.1/docs/start/framework/data-loading.md
- 6: https://unpkg.com/react-router@8.0.1/docs/how-to/spa.md
Prerender the API and LLM routes.
With ssr: false, non-root loaders run only for routes listed in prerender.paths; otherwise the app runs in SPA mode and /api/search, /llms.txt, /llms-full.txt, and llms.mdx/docs/* will not get pre-rendered loader payloads.
Proposed fix
for (const path of getStaticPaths()) {
if (!excluded.includes(path)) paths.push(path)
}
+ paths.push('/api/search', '/llms.txt', '/llms-full.txt', '/llms.mdx/docs/**')
+
for await (const entry of glob('**/*.mdx', { cwd: 'content/docs' })) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@website-docs/react-router.config.ts` around lines 17 - 22, Update the
prerender path construction in the route configuration to include the API search
route, both LLM text routes, and the generated llms.mdx documentation paths
alongside the existing documentation URLs. Ensure these routes are returned in
the prerender paths so their loaders execute during the build.
|
|
||
| Write public documentation in `content/docs/`. Each `.mdx` file becomes a page under `/docs`. | ||
|
|
||
| The app deploys to `https://docs.termul.dev`. Cloudflare Pages must be configured with the `termul-docs` project and `website-docs/build/client` as its build output directory. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Avoid presenting the custom domain as live before provisioning.
The PR objectives state that the Pages project and custom domain are not provisioned yet, but Line 26 says the app deploys there. Change this to “will deploy” or mark the domain as pending to avoid sending contributors to a dead URL.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@website-docs/README.md` at line 26, Update the deployment statement in the
README to use future-tense wording or explicitly mark https://docs.termul.dev as
pending, avoiding any claim that the custom domain is currently live.
Summary
docs.termul.devis provisioned.Related Issue
Closes #447
Searched open and closed pull requests for Fumadocs,
docs.termul.dev, and a public documentation site. No duplicate implementation was found. Existing documentation issues #254, #353, and #356 cover README feature coverage, missing ADRs, and internal ACP architecture respectively, not this public docs site.Type of Change
What Changed
website-docs/, a Fumadocs + React Router static documentation app.Install Termulguide for macOS, Windows, Linux, and first project setup.website-docs/PLAN.mdwith the proposed user documentation roadmap and contribution rules..github/workflows/deploy-docs.ymlfor Cloudflare Pages validation and deployment.termul-docsWrangler configuration and SPA fallback routing.docs.termul.devdomain are confirmed live.Maintainer infrastructure required before production deployment:
termul-docsdocs.termul.devHow It Was Tested
bun run ci(Biome lint + format + imports, strict mode)bun run typecheckbun run testcargo clippy --all-targets -- -D warnings(in src-tauri)cargo test(in src-tauri)Additional docs checks:
bun run docs:lintpassedbun run docs:typecheckpassedbun run docs:buildpassedgit diff --checkpassed/,/docs, local search data,llms.txt, page Markdown output, and SPA fallbackRenderer tests: 195 files passed, 1 skipped; 2,204 tests passed, 42 skipped.
Rust checks are currently blocked on the unchanged upstream
devsource by an existing syntax error:Both
cargo clippy --all-targets -- -D warningsandcargo teststop at that same parse error. This PR does not modifysrc-tauri/src/worktree/mod.rsor any Rust source.Screenshots or Recordings
The docs shell and installation page were manually verified through the local development server and production static build. A hosted preview depends on the Cloudflare Pages project being provisioned by a maintainer.
CI & Review Gate
Checklist
Summary by CodeRabbit
New Features
Documentation
Chores