Architecture
What this repo is
trickfire-docs (TrickfireRobotics/docs) has two jobs that share one
codebase:
- CLI (
trickfire-docs init|dev|build), published to npm and run vianpxfrom any member repo. It scaffolds adocs/folder, then previews that repo's docs locally. - The aggregator site - the actual
docs.trickfirerobotics.comwebsite. It syncsdocs/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
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 usesRule 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) - copiesscaffold/docs.config.json,scaffold/docs/, and a GitHub Actions workflow into the target repo, and appendsdist/,docs/**/meta.jsonto its.gitignore.dev/build(cli/commands/dev.ts/build.ts) - both:- Verify
pnpmis onPATH(ensurePnpm) - required because the cache clone'spnpm installapplies thepatchedDependenciesfumadocs-ui fix (see "Patching dependencies" below); plainnpmcan't. - Clone
TrickfireRobotics/docsto~/.cache/trickfire-docs/repoon first use, otherwisegit fetch+reset --hard origin/main(ensureRepoCache) - shared by every project previewed on the machine, always latest, same as production (scripts/build.shupdates the same way). - Symlink the member's
docs/+docs.config.jsoninto the clone'scontent/<repo-name>/(linkMemberProject) - and remove any other project's symlink left over from a previousdev/buildrun for a different repo, so the cache never accumulates more than one linked project.content/general(tracked, org-wide docs) is untouched. pnpm install --frozen-lockfile --ignore-scriptsin the clone (installCacheDeps) - the same installscripts/build.shruns in production.--ignore-scriptsonly skips husky'spreparehook, pointless in a throwaway clone;--prodlooked tempting to skip eslint/vitest/etc, but this repo's own roottsconfig.jsonmakes Next's dev server insist ontypescriptbeing present, so it doesn't actually save anything.- Run
pnpm exec tsx scripts/generate-sources.tsthenpnpm exec next dev/next buildwithcwdset to the clone.buildthen copies the clone'sout/to<projectRoot>/dist/.
- Verify
docs.config.jsonschema: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:
- Scans
content/*(synced member repos, or - locally - the one repo the CLI symlinked in) and this repo's owndocs/for adocs.config.json(or, for this repo, a hardcodedcliRepoentry). - Writes one
export const repo_x = defineDocs({ dir: "..." })per project intocontent-sources.generated.ts- a literal file rather than a runtime loop, becausefumadocs-mdxneeds staticdefineDocs()calls it can analyze, not a dynamically-built list. - Writes
meta.jsoninto each project's content dir from itsdocs.config.jsonsidebararray. - 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.
/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 gitTwo workflow types, both ending in scripts/build.sh:
| Workflow | File | Trigger |
|---|---|---|
| Member sync | sync-docs.yml (reusable) | Called by each member repo's CI |
| CLI deploy | deploy.yml | Push 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
- Developer pushes changes to
docs/in a member repo (onmain). - That repo's
docs.ymlcalls this repo's reusablesync-docs.yml. - The self-hosted runner:
rsync docs/ → content/<repo>/, copiesdocs.config.json, runsscripts/build.sh. build.sh→git pull,pnpm install,pnpm site:build.site:build→generate-sources.ts(regeneratescontent-sources.generated.ts+ allmeta.json) →next buildstatically exports the whole aggregator site toout/.- nginx serves the new
out/immediately (static files, no server restart needed).
Data flow: a member repo running trickfire-docs dev locally
- Developer runs
npx trickfire-docs devin their repo. cli/commands/dev.tschecks forpnpm, clones/updates~/.cache/trickfire-docs/repotoorigin/main, symlinks the project'sdocs/+docs.config.jsoninto that clone'scontent/<repo-name>/(pruning any other repo's symlink left there from a previous run), runspnpm install --prod, thenpnpm exec next devwithcwdset to the clone.- The aggregator (
src/lib/source.ts, readingcontent-sources.generated.ts) picks up every project undercontent/- the member's own repo,general, and this repo's own cli docs - and serves a live preview atlocalhost:3000. It's the same app, same data, that production runs; the only difference is which repos are symlinked intocontent/.
What to edit for common changes
| I want to... | Edit |
|---|---|
Change docs.config.json fields/validation | cli/config/schema.ts, cli/config/load.ts |
Change how meta.json (sidebar structure) is generated | scripts/generate-sources.ts |
Change what trickfire-docs init scaffolds | scaffold/, cli/commands/init.ts |
| Change how the local cache clone is managed | cli/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 setup | scripts/build.sh, scripts/nginx.conf, this doc's server section |
| Change what gets synced from member repos | sync-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.