Managing Documentation for Multiple Projects in a Monorepo

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.

The Challenge

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:

  • Allows easy discovery of all projects
  • Maintains separate sidebars for each project to avoid clutter
  • Leverages code-based documentation generation where possible

Documentation Framework Choices

I've experimented with various documentation frameworks:

  • RsPack: Appears promising but had issues with basic features like relative links. Documentation often needs conversion to compatible Markdown.
  • Fumadocs: My current choice. It handles core functionality well but struggles with very large pages (memory issues). I haven't fully investigated the root cause yet.

Fumadocs provides a solid foundation for building documentation sites with Next.js integration.

Code-Based Documentation Generation

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:

  • react-docgen-typescript: Focused on React components
  • TypeDoc with typedoc-plugin-markdown: Highly customizable with extensive options
  • Custom tooling using AST libraries like @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:

  1. Configure documentation generation: Set up TypeDoc (or your chosen tool) to output to a docs directory within each project
  2. Install projects as dependencies: Add each project as a dev dependency in your documentation site
  3. Create symlinks: Use relative symlinks to connect generated docs to your site's content structure

For two projects "my-library" and "my-documentation" (a Fumadocs site):

  • Generate docs to my-library/docs
  • Install my-library in my-documentation
  • Create symlink: content/docs/my-library../../node_modules/my-library/docs

The symlink name becomes the documentation URL path.

TypeDoc Configuration for Fumadocs Compatibility

To generate Fumadocs-compatible documentation with TypeDoc, a configuration must be used with certain plugins:

  • The pluginMarkdown handles the conversion to Markdown format.
  • The pluginFrontmatter is necessary for Fumadocs to read the generated Markdown files using its default schema.
  • The pluginFrontMatterTitle adds frontmatter titles to Markdown pages based on the model name.
  • The 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.

Creating a component libraryConfigure Fumadocs for Root-Level Documentation