Architecture

What this repo is

trickfire-docs (TrickfireRobotics/docs) has two jobs that share one codebase:

  1. CLI (trickfire-docs init|dev|build), published to npm and run via npx from any member repo. It scaffolds a docs/ folder, then previews that repo's docs locally.
  2. The aggregator site - the actual docs.trickfirerobotics.com website. It syncs docs/ content from every member repo and renders them all as one multi-project Fumadocs site with a project switcher.

There is only one Fumadocs (Next.js + fumadocs-mdx) app - the aggregator, src//app/ at the repo root. trickfire-docs dev/build don't run a separate copy of it: the CLI keeps a global clone of this repo on the member's machine (~/.cache/trickfire-docs/repo, always reset to origin/main), symlinks the member's docs/ into that clone's content/<repo>/, and runs the aggregator's own site:dev/site:build from there - the same code, same pnpm patches, same output shape production builds. Local preview shows the member's project alongside general (org-wide docs) and this repo's own framework docs, matching what's actually deployed.

Repo layout

plaintext
trickfire-docs/
├── cli/              CLI source (dev|build|init) - published as the npm package
│   ├── cli.ts              entry point, dispatches to commands/
│   ├── commands/           dev.ts, build.ts, init.ts
│   ├── config/             docs.config.json schema, loading
│   └── utils/              repo-cache.ts - clones/updates the global cache, symlinks the
│                           member's docs/ into it, runs pnpm install + site:dev/build there
│
├── scaffold/               Files copied into a member repo by `trickfire-docs init`
│   ├── docs.config.json    starter config
│   ├── docs/               starter markdown pages
│   └── .../docs.yml
│
├── src/, app/              The web app source for the actuall documentation site, written
│                           in React using Fumadocs components
│
├── docs/                   This repo's own docs content (what you're reading now),
├── docs.config.json        uses same format as other docs
│
├── content/                Subdocumentation pages that do not have their own repo. You
│                           can also link projects using trickfire-docs using the dev script
│
└── scripts/                Scripts that the app or the developer uses

Rule of thumb: if you're changing how docs.trickfirerobotics.com (or a member's local trickfire-docs dev/build preview, which now runs the exact same app) looks or behaves, edit src//app/ at the repo root - there's only one Fumadocs app to keep in sync with. If you're changing CLI behavior (init/dev/build, docs.config.json schema, how the cache clone is managed), edit cli/.

Components

cli/ - the CLI

Published as the trickfire-docs npm package (bin: trickfire-docs). Member repos consume it via npx trickfire-docs <command> and never install it as a dependency.

  • init (cli/commands/init.ts) - copies scaffold/docs.config.json, scaffold/docs/, and a GitHub Actions workflow into the target repo, and appends dist/, docs/**/meta.json to its .gitignore.
  • dev / build (cli/commands/dev.ts / build.ts) - both:
    1. Verify pnpm is on PATH (ensurePnpm) - required because the cache clone's pnpm install applies the patchedDependencies fumadocs-ui fix (see "Patching dependencies" below); plain npm can't.
    2. Clone TrickfireRobotics/docs to ~/.cache/trickfire-docs/repo on first use, otherwise git fetch + reset --hard origin/main (ensureRepoCache) - shared by every project previewed on the machine, always latest, same as production (scripts/build.sh updates the same way).
    3. Symlink the member's docs/ + docs.config.json into the clone's content/<repo-name>/ (linkMemberProject) - and remove any other project's symlink left over from a previous dev/build run for a different repo, so the cache never accumulates more than one linked project. content/general (tracked, org-wide docs) is untouched.
    4. pnpm install --frozen-lockfile --ignore-scripts in the clone (installCacheDeps) - the same install scripts/build.sh runs in production. --ignore-scripts only skips husky's prepare hook, pointless in a throwaway clone; --prod looked tempting to skip eslint/vitest/etc, but this repo's own root tsconfig.json makes Next's dev server insist on typescript being present, so it doesn't actually save anything.
    5. Run pnpm exec tsx scripts/generate-sources.ts then pnpm exec next dev / next build with cwd set to the clone. build then copies the clone's out/ to <projectRoot>/dist/.
  • docs.config.json schema: cli/config/schema.ts (DocsConfig, SidebarConfig). Parsing/defaults: cli/config/load.ts.

Root src/, app/ - the aggregator site

This repo's own (and only) Fumadocs app, serving every project at once under /[repo]/[[...slug]]. Reads from content-sources.generated.ts, which scripts/generate-sources.ts writes at the start of site:dev/site:build (postinstall also runs it, so a fresh clone has it before the first build). This is exactly what runs both in production and inside a member's local cache clone - see cli/utils/repo-cache.ts above.

generate-sources.ts:

  1. Scans content/* (synced member repos, or - locally - the one repo the CLI symlinked in) and this repo's own docs/ for a docs.config.json (or, for this repo, a hardcoded cliRepo entry).
  2. Writes one export const repo_x = defineDocs({ dir: "..." }) per project into content-sources.generated.ts - a literal file rather than a runtime loop, because fumadocs-mdx needs static defineDocs() calls it can analyze, not a dynamically-built list.
  3. Writes meta.json into each project's content dir from its docs.config.json sidebar array.
  4. Writes repos.generated.json (name/description/icon per project, used by the homepage project cards and switcher).

Debian server (production)

Hosts everything at /home/trickfire/docs/. A self-hosted GitHub Actions runner (label docs) runs directly on this machine.

plaintext
/home/trickfire/docs/
├── content/              ← gitignored, populated by member repos' sync jobs
│   └── <repo-name>/
│       ├── docs/
│       └── docs.config.json
├── out/                  ← static export output, served by nginx
├── scripts/build.sh      ← git pull + pnpm install + site:build
└── src/, app/, node_modules/, …  ← from git

Two workflow types, both ending in scripts/build.sh:

WorkflowFileTrigger
Member syncsync-docs.yml (reusable)Called by each member repo's CI
CLI deploydeploy.ymlPush to main in this repo

A cloudflared tunnel proxies docs.trickfirerobotics.com to nginx on localhost:80 - no inbound ports opened on the server.

Data flow: a member repo's doc update

  1. Developer pushes changes to docs/ in a member repo (on main).
  2. That repo's docs.yml calls this repo's reusable sync-docs.yml.
  3. The self-hosted runner: rsync docs/ → content/<repo>/, copies docs.config.json, runs scripts/build.sh.
  4. build.sh → git pull, pnpm install, pnpm site:build.
  5. site:build → generate-sources.ts (regenerates content-sources.generated.ts + all meta.json) → next build statically exports the whole aggregator site to out/.
  6. nginx serves the new out/ immediately (static files, no server restart needed).

Data flow: a member repo running trickfire-docs dev locally

  1. Developer runs npx trickfire-docs dev in their repo.
  2. cli/commands/dev.ts checks for pnpm, clones/updates ~/.cache/trickfire-docs/repo to origin/main, symlinks the project's docs/ + docs.config.json into that clone's content/<repo-name>/ (pruning any other repo's symlink left there from a previous run), runs pnpm install --prod, then pnpm exec next dev with cwd set to the clone.
  3. The aggregator (src/lib/source.ts, reading content-sources.generated.ts) picks up every project under content/ - the member's own repo, general, and this repo's own cli docs - and serves a live preview at localhost:3000. It's the same app, same data, that production runs; the only difference is which repos are symlinked into content/.

What to edit for common changes

I want to...Edit
Change docs.config.json fields/validationcli/config/schema.ts, cli/config/load.ts
Change how meta.json (sidebar structure) is generatedscripts/generate-sources.ts
Change what trickfire-docs init scaffoldsscaffold/, cli/commands/init.ts
Change how the local cache clone is managedcli/utils/repo-cache.ts, cli/commands/dev.ts/build.ts
Change styling/layout/MDX components (affects prod AND local preview)root src/, app/
Change codeblock behavior (titles, shell prompt, copy button)src/lib/shiki-transformers.ts
Fix a bug in Fumadocs UI itself (sidebar, codeblock, etc.)patches/ via pnpm patch - see below
Change the production server / nginx / systemd setupscripts/build.sh, scripts/nginx.conf, this doc's server section
Change what gets synced from member repossync-docs.yml (in .github/), scripts/generate-sources.ts

Patching dependencies

A couple of upstream @fumadocs/base-ui bugs (non-collapsible sidebar folder headers losing their classes; the codeblock copy button not respecting inline nd-copy-ignore prefixes) are fixed via pnpm patch rather than forking the package. The patch lives at patches/@[email protected] and is applied automatically on pnpm install (see patchedDependencies in pnpm-workspace.yaml). Because trickfire-docs dev/build run a real pnpm install inside the cache clone (not npm - see ensurePnpm in cli/utils/repo-cache.ts), the patch also fixes every member repo's local preview - there's nothing to do on their end.

If you hit another upstream Fumadocs bug, prefer extending this patch (or adding a new one) over vendoring a full component copy - pnpm re-applies it on every install, and there's only one app (src/) it needs to apply to.

On this page