Architecture
The core/adapter boundary, and how it is kept honest.
Mermaid source
flowchart TB F["docs-overlay-fumadocs"] --> C["docs-overlay"] D["docs-overlay-docusaurus"] --> C L["docs-overlay-cli"] --> C L -.-> D Y["your adapter"] --> C
The dependency direction is one-way and enforced by tests, not by convention. Adding support for another framework means writing an adapter; it must never require a change to the core.
The dotted line is the one edge that is not a hard dependency: the command line declares
docs-overlay-docusaurus as an optional peer and imports it lazily, so cut and prune work on a
repository that has never heard of Docusaurus. Nothing points upwards anywhere on this drawing, and that
is the whole rule.
What belongs where
The question to ask before adding anything: is this part of versioned documentation itself, or part of a documentation framework?
| Core | Adapter |
|---|---|
| versions, ordering, inheritance chains | URLs, baseUrl, basePath, trailing slashes |
| pages, slugs, resolution | page trees, sidebars, tabs |
| aliases, redirects as slug → slug | HTTP status codes, next.config redirects |
| tombstones and their metadata | React components, layouts |
| metadata as an opaque payload | the meaning of pages: [], "...", "!x", root: true |
| caching, dependency graph | search indexing and filtering |
| — | filesystem access, MDX compilation |
The rules the core lives by
Zero runtime dependencies, no Node built-ins. docs-overlay runs in a browser, a worker or
an edge runtime. The semver comparator is sixty hand-written lines rather than a dependency.
Synchronous. A Fumadocs StaticSource is already a materialised array, and making the core async
would spread await across resolution, caching and the graph for no gain. A future filesystem source
resolves its I/O before constructing entries.
Metadata is opaque. ResolvedPage.meta is a generic the core never inspects, and directives are
read through an injected readDirectives. This is what keeps compiled MDX, structuredData and
Fumadocs' navigation grammar out of the package.
It never throws on bad content. Problems become Diagnostics. A broken page must not take down a
dev server; whether an error diagnostic should fail a build is the caller's decision.
Metadata is inherited whole, never merged. Understanding what is inside a meta.json is an
adapter's job — the core only ever applies inheritance to it, per directory. Merging is delegated
through a MetaMerger the adapter supplies.
The guards
Two things fail the build if the rule is broken:
packages/core/test/architecture.test.ts— asserts the manifest declares no dependency, and scans every shipped source file for a forbidden import. It lives outsidesrc/because it needsnode:fs, which is exactly what it forbids there..oxlintrc.json— ano-restricted-importsoverride scoped topackages/core/src/**.
Plus npm run verify:independence, which packs the core, unpacks it into a directory with no
node_modules at all, and runs it there. TypeScript alone would not catch a stray fumadocs-core
import in this monorepo, because npm hoists it and the compiler resolves it happily — that is
precisely why the text scan and the sandbox exist.
Adapter contract
An adapter consumes DocumentationSource, the deliberately small stable interface:
interface DocumentationSource<M = unknown> {
getVersions(): readonly Version[];
getPages(version: VersionId): readonly ResolvedPage<M>[];
getPage(version: VersionId, slug: Slug | SlugKey): ResolvedPage<M> | undefined;
}Overlay extends it with resolution, metadata, redirects, the dependency graph and invalidation. See
Writing an adapter.