How Documints Works
A tour of the actual machinery behind documints dev/documints build, for anyone
extending documints itself or just curious what's happening under the hood.
One package, plus design tokens
documints - the CLI you install - owns the whole thing: the
fizmoo-defined commands (dev/build/init),
config resolution, doc discovery, the Vite pipeline, and the app shell (Layout, header, nav)
your docs render inside, all in one package.
That's deliberate, not an oversight: the shell and a specific project's own doc content have
to compile together, in the same Vite build, every single time dev/build runs. Splitting
that across a package boundary only adds indirection - a self-referencing import here, a
tsconfig rootDir to reason about there - without buying anything, since nothing outside
documints itself ever needs the shell on its own.
The one piece that is split out is design tokens (@documints/tokens) - genuinely useful
independent of the engine, so it's built and versioned separately with chamfer-css.
The Documints class
Nearly everything documints does is reached through one class. Its shape mirrors what it does:
- Static methods handle finding and locating, since they run before an instance
exists:
Documints.findConfigFile()walks up fromcwdlooking for.documints/config.ts;Documints.loadConfig()compiles that file in-process with esbuild and validates it against a Zod schema;Documints.getDirectories()resolves every path documints needs (content root, app root, output paths) from there.Documints.create()chains all three (offering to scaffold a project viaDocumints.bootstrap()if none is found) and returns a constructed instance - ornullif the user declines. - Instance methods handle parsing and building, since they operate on the resolved config/paths a specific instance holds: discovering docs into a route manifest, building the Vite config, generating virtual modules.
dev()/build()tie it together - the only two methods a CLI command actually calls.
Discovering routes
Given a resolved project, building a route manifest is a straight pipeline:
- Glob for files matching
config.docs(or the default), rooted at.documints/(not a fixed subfolder - this is what letsdocspoint anywhere reachable via a relative path). - Parse frontmatter out of each match. For
.doc.md/.doc.mdxthis is a direct read; for.doc.tsx, a leading/** --- ... --- */comment block is extracted and parsed as if it were the real thing - the file is never executed or transpiled just to discover its route, keeping discovery fast and side-effect-free. - Derive a route path from
title(slash-delimited, slugified per segment;slugoverrides the last segment;home: trueshort-circuits to/) - see Routing. - Detect collisions (same route from two files) and warn, keeping the later-discovered entry.
- Apply
order, if configured - re-inserting entries in the desired sequence rather than discovery order.
The flat manifest (route path → file info) then gets turned into a route graph - the
same flat data, nested by route-path segment, which is what actually drives the sidebar and
the section-type header links. This is a pure, path-agnostic transform: it only reads
routePath, so it doesn't care how those paths were derived.
Virtual modules
The route manifest and graph need to reach the actual React app, which happens through two
Vite virtual modules a plugin inside Documints generates on demand:
virtual:routes- the route index, the full route graph, and a list of every doc page, each with animportComponent()that dynamically imports the real file.virtual:data- the resolved header config (with anysectionlinks already expanded intodropdownlinks against the route graph - the client never sees the rawsectiontype at all).
In dev mode, the same plugin watches the docs directory: on any change, it rebuilds both
virtual modules, invalidates them in Vite's module graph, and forces a full client reload -
this is what makes adding, renaming, or editing a .doc.* file show up live without a
manual restart.
The dev server
Documints.dev() runs an Express app with Vite attached in middleware mode - not Vite's
own standalone dev server. Every request loads the project's SSR entry via
vite.ssrLoadModule() and renders it through React's streaming SSR API
(renderToPipeableStream), piped back through the response once rendering completes.
One detail worth knowing if you're reading the source: Vite's HMR websocket needs a real
http.Server to attach to. dev() creates one explicitly and hands it to Vite via
server.hmr.server - without that, Vite spins up a second, disconnected websocket-only
server instead, and HMR silently never connects.
The build
Documints.build() runs three passes:
- A client build - the real, hashed JS/CSS bundle a browser loads.
- A server-only build - a temporary SSR bundle that exists purely to render HTML; it's deleted once prerendering finishes and is never part of the deployed output.
- A prerender loop - for every entry in the route manifest, render it through the
server bundle to a real HTML string and write
index.htmlto.documints/static/<route>/.
The result has no runtime dependency on Node, Vite, or documints itself - it's genuinely static output, deployable anywhere.
Design tokens and the app shell
The app shell (nav, header, breadcrumbs, TOC) is a real React application living inside
documints itself, styled with design tokens from @documints/tokens
(chamfer-css-generated CSS custom
properties) - which is what makes Plugins and future theming both possible
without touching documints' own source: anything downstream can override a token by
redeclaring the corresponding --documints-* CSS variable.
