Managing multiple related projects within a single monorepo can be challenging, especially when it comes to documentation. In this post, I'll share my approach to creating a unified documentation website that promotes discovery across projects while keeping each project's documentation independent and focused.
I have several projects under one umbrella, each with distinct use cases, APIs, and purposes. While they're related, they're essentially independent. The goal is to have a single documentation site that:
I've experimented with various documentation frameworks:
Fumadocs provides a solid foundation for building documentation sites with Next.js integration.
Ideally, documentation should be generated directly from source code to maintain a single source of truth. Using JSDoc annotations with TypeScript types provides rich, accurate documentation.
For example:
/**
* Stats in League of Legends do not increase linearly with champion level. Use
* this function to calculate the stat multiplier.
*
* @param lvl The current champion level
* @returns the stat multiplier
*
* @example
* ```ts
* const base = 658;
* const perLevel = 109;
* const lvl = 13;
* const stat = base + growth(lvl) * perLevel; // 1851.55
* ```
*/
export function growth(lvl: number) {
return (lvl - 1) * (0.7025 + 0.0175 * (lvl - 1));
}
This generates documentation with descriptions, parameter details and types from TypeScript, return types from TypeScript (inferred), and usage examples automatically.
Available tools for this include:
@structured-types/api
(though it's unmaintained)I currently use TypeDoc for non-React projects due to its flexibility.
Here's how to set up documentation generation and integration for multiple projects:
docs
directory within each projectFor two projects "my-library" and "my-documentation" (a Fumadocs site):
my-library/docs
content/docs/my-library
→ ../../node_modules/my-library/docs
The symlink name becomes the documentation URL path.
To generate Fumadocs-compatible documentation with TypeDoc, a configuration must be used with certain plugins:
pluginMarkdown
handles the conversion to Markdown format.pluginFrontmatter
is necessary for Fumadocs to read the generated
Markdown files using its default schema.pluginFrontMatterTitle
adds frontmatter titles to Markdown pages based
on the model name.pluginFumaDocsSidebar
function generates a meta.json
file with only
the title
and root: true
. The root: true
property ensures that when this
documentation section is opened, other projects' documentation is not
displayed. Only the title is needed for the index page; the rest of the page
titles are inferred from the frontmatter.import { MarkdownPageEvent } from "typedoc-plugin-markdown";
import { writeFileSync } from "node:fs";
import { load as pluginFrontmatter } from "typedoc-plugin-frontmatter";
import { load as pluginMarkdown } from "typedoc-plugin-markdown";
/**
* Plugin to create Fumadocs-compatible sidebar metadata
* @param {string} title - The title for the documentation section
*/
function pluginFumaDocsSidebar(title) {
return (app) => {
app.renderer.postRenderAsyncJobs.push(async (_renderer) => {
const outDir = app.options.getValue("out");
writeFileSync(
`${outDir}/meta.json`,
JSON.stringify({
title,
root: true,
})
);
});
};
}
/**
* Plugin to add frontmatter titles to Markdown pages
*/
function pluginFrontMatterTitle(app) {
app.renderer.on(MarkdownPageEvent.BEGIN, (page) => {
if (page.model?.name) {
page.frontmatter.title = page.model.name;
}
});
}
/** @type {import("typedoc").TypeDocOptions} */
const config = {
plugin: [
pluginMarkdown,
pluginFrontmatter,
pluginFrontMatterTitle,
pluginFumaDocsSidebar("My Library"),
],
};
export default config;
FYI: It is possible that the configuration above does not cover all of your use cases. You may need to adjust, especially the frontmatter plugin, to fit your specific requirements.