Skip to main content
doc0blade47/fumadocsdocumentation home
Sign in

Quick Start

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

Getting started with Fumadocs involves bootstrapping a dedicated documentation workspace, configuring content collections, rendering routing hierarchies, and managing development environments. The initialization workflow is powered by Create App Generator and the Fumadocs CLI, which scaffold template applications, install dependencies, and configure linters across supported frameworks.

Sources: packages/create-app/src/bin.ts:22-54, packages/cli/src/index.ts:17-27

Bootstrapping with CLI tooling

Scaffolding Workspaces and Command Options

Fumadocs provides dedicated command-line utilities to initialize documentation projects, manage configuration files, and interact with component registries. The primary package for scaffolding workspaces is create-fumadocs-app, which is versioned at 16.0.123 and bundles dependencies such as @clack/prompts, tinyexec, and ts-morph.

Sources: packages/create-app/package.json:3-52

Once inside a workspace, the fumadocs CLI (@fumadocs/cli) offers core routines for workspace initialization, layout customization, component downloads, and tree transformations. The CLI is powered by commander and picocolors.

Sources: packages/cli/src/index.ts:4-20

CommandAliasesOptionsDescription
fumadocs (default)none--config <string>Initializes a ./cli.json configuration file if one does not already exist
customisecustomize--dir <string>, --config <string>Simple way to customize layouts with Fumadocs UI
addnone--dir <string>, --config <string>Downloads and adds new components to your docs
export epubnone--framework <name>, --output <path>, --scaffold-onlyExports documentation to EPUB format
treenone--js, --no-root, --import-name <name>Generates JavaScript or MDX file trees from JSON or directory arguments

Sources: packages/cli/src/index.ts:17-83

Note

The default action of the fumadocs binary checks for the existence of ./cli.json. If it does not exist, it initializes the file and logs a success message; if it already exists, it emits a notice indicating that the config file is already present. Sources: packages/cli/src/index.ts

Registry Connectors and Framework Targets

When executing commands that fetch remote or local components or layouts, the CLI resolves registry sources using createClientFromDir. Short-form directory shortcuts map specific keywords to designated endpoints.

const dirShortcuts: Record<string, string> = {
  ':preview': 'https://preview.fumadocs.dev/registry',
  ':dev': 'http://localhost:3000/registry',
};

Sources: packages/cli/src/index.ts:39-42

The createClientFromDir function inspects the directory path or shortcut. If the destination begins with http:// or https://, it instantiates an HttpRegistryConnector; otherwise, it returns a LocalRegistryConnector.

function createClientFromDir(dir = 'https://fumadocs.dev/registry') {
  if (dir in dirShortcuts) dir = dirShortcuts[dir];
 
  return dir.startsWith('http://') || dir.startsWith('https://')
    ? new HttpRegistryConnector(dir)
    : new LocalRegistryConnector(dir);
}

Sources: packages/cli/src/index.ts:108-114

Warning

When executing fumadocs export epub, the --framework option is required. The supported framework names accepted by the command parser are next, tanstack-start, react-router, and waku. Sources: packages/cli/src/index.ts

Configuring content collections and schemas

Declaring Document Collections

Documentation sources are structured through collection configuration helpers provided by @fumadocs/content. The docsCollection function binds MDX documentation documents and metadata files into a unified store, accepting directory paths and custom validation schemas for both types of content.

export interface DocsCollectionConfig<
  FrontmatterSchema extends StandardSchemaV1 | undefined,
  MetaSchema extends StandardSchemaV1 | undefined,
> {
  dir: string;
  docs: Omit<DocsMDXCollectionConfig<FrontmatterSchema>, 'dir'>;
  meta: Omit<DataCollectionConfig<MetaSchema>, 'dir'>;
}
 
export function docsCollection<
  FrontmatterSchema extends StandardSchemaV1 | typeof pageSchema = typeof pageSchema,
  MetaSchema extends StandardSchemaV1 | typeof metaSchema = typeof metaSchema,
>(config: DocsCollectionConfig<FrontmatterSchema, MetaSchema>) {
  return {
    index: new DocsCollection(),
    meta: dataCollection({ dir: config.dir, ...config.meta }),
    doc: docsMdxCollection({ dir: config.dir, ...config.docs }),
  };
}

Sources: packages/content/src/index.ts:35-53

The underlying DocsCollection class extends the base Collection implementation from fuma-content/collections. During its emit lifecycle (onEmit.pipe), it dynamically generates code files for code generation workflows, writing module imports and export definitions for runtime stores.

export class DocsCollection extends Collection {
  constructor() {
    super();
    this.onEmit.pipe(async (data, ctx) => {
      const file = await ctx.createCodeGenerator(`${this.name}.ts`, ({ codegen }) => {
        const docName = `${this.name}$doc`;
        const metaName = `${this.name}$meta`;
        codegen.addNamedImport([docName], `./${docName}`);
        codegen.addNamedImport([metaName], `./${metaName}`);
        codegen.addNamedImport(['docsStore'], '@fumadocs/content/runtime');
        codegen.push(`export const ${this.name} = docsStore(${docName}, ${metaName})`);
      });
      data.push(file);
      return data;
    });
  }

Sources: packages/content/src/index.ts:55-70

Note

The DocsCollection generator creates code files named <name>.ts that import the respective document and metadata files, import docsStore from @fumadocs/content/runtime, and export a combined store constant. Sources: packages/content/src/index.ts

Defining Content Schemas and Runtime Stores

Content schemas validate frontmatter attributes against standard schema specifications such as @Standard-schema/spec. By default, docsMdxCollection and docsCollection fall back to pageSchema and metaSchema imported from fumadocs-core/source/schema.

export function docsMdxCollection<
  FrontmatterSchema extends StandardSchemaV1 | typeof pageSchema = typeof pageSchema,
>(config: DocsMDXCollectionConfig<FrontmatterSchema>) {
  const { options = {}, frontmatter = pageSchema, ...rest } = config;
 
  return mdxCollection<FrontmatterSchema>({
    ...rest,
    frontmatter: frontmatter as never,
    async options() {
      const mdxOptions = typeof options === 'object' ? options : await options();
      const { remarkInclude } = await import('fuma-content/plugins/remark/include');
      return mdxPreset({
        ...mdxOptions,
        _include: remarkInclude,
      });
    },
  });
}

Sources: packages/content/src/index.ts:16-33

The runtime integration layer converts raw MDX and metadata file stores into static source representations consumed by Fumadocs routing and layout utilities. The toFumadocsSource function iterates over the list of pages and metadata files, normalizing paths and combining compiled frontmatter properties.

export function toFumadocsSource<
  Mdx extends MDXStoreData<PageData> | MDXStoreLazyData<PageData, unknown> = MDXStoreData<
    PageData,
    unknown
  >,
  Meta extends { data: MetaData } = { data: MetaData },
>(mdxStore?: FileCollectionStore<Mdx>, metaStore?: FileCollectionStore<Meta>) {
  const out: StaticSource<{
    pageData: ToPageData<Mdx>;
    metaData: Meta['data'];
  }> = { files: [] };
  for (const page of mdxStore?.list() ?? []) {
    out.files.push({
      type: 'page',
      data: ('compiled' in page
        ? { ...page, ...page.compiled.frontmatter }
        : { ...page, ...page.frontmatter }) as unknown as ToPageData<Mdx>,
      path: page.path,
      absolutePath: page.fullPath,
    });
  }
  for (const file of metaStore?.list() ?? []) {
    out.files.push({
      type: 'meta',
      data: file.data,
      path: file.path,
      absolutePath: file.fullPath,
    });
  }
  return out;
}

Sources: packages/content/src/runtime.ts:12-42

Function / ClassParametersReturn TypeDescription
docsMdxCollectionconfig: DocsMDXCollectionConfig<FrontmatterSchema>MDX collection instanceConfigures MDX collections with preset options and remark include plugins
docsCollectionconfig: DocsCollectionConfig<FrontmatterSchema, MetaSchema>{ index, meta, doc }Combines metadata and MDX collections into a unified documentation workspace
DocsCollectionnoneDocsCollection class instanceExtends base collections to emit code generator output files during build pipelines
toFumadocsSourcemdxStore, metaStoreStaticSourceTransforms file collection stores into standard Fumadocs static source structures
docsStoremdxStore, metaStore{ toFumadocsSource() }Wraps file stores with a method to export static source data

Sources: packages/content/src/index.ts:16-53, packages/content/src/index.ts:55-70, packages/content/src/runtime.ts:12-56

Warning

When defining custom options for docsMdxCollection, the options parameter can be passed directly as an object or as an asynchronous factory function returning MDXBundlerPresetOptions. The runtime automatically injects the remarkInclude plugin into the bundler preset. Sources: packages/content/src/index.ts

Mounting documentation pages and layouts

Layout and Preset Resolution

Documentation routes are handled by dynamic page components receiving URL slugs. The preview server loads the runtime configuration and resolves the metadata source before determining whether to use the default docs layout preset or the alternative flux preset.

export default async function DocPage({ slugs }: PageProps<'/docs/[...slugs]'>) {
  const config = await getConfigRuntime();
  const source = await getSource(config);
  const page = source.getPage(slugs);
  const mdPreset = config.layout.presets?.md ?? 'docs';
  let mdPresetComponents: MdPresetComponents;
 
  if (mdPreset === 'docs') {
    mdPresetComponents = {
      layout: await import('fumadocs-ui/layouts/docs'),
      page: await import('fumadocs-ui/layouts/docs/page'),
    };
  } else {
    mdPresetComponents = {
      layout: await import('fumadocs-ui/layouts/flux'),
      page: await import('fumadocs-ui/layouts/flux/page'),
    };
  }
 
  return <MdContent slugs={slugs} page={page} components={mdPresetComponents} />;
}

Sources: packages/preview/src/pages/...slugs.tsx:76-96

Layout PresetLayout Component ImportPage Component Import
docsfumadocs-ui/layouts/docsfumadocs-ui/layouts/docs/page
fluxfumadocs-ui/layouts/fluxfumadocs-ui/layouts/flux/page

Sources: packages/preview/src/pages/...slugs.tsx:83-93

Note

The layout preset is read directly from config.layout.presets?.md. If it is not explicitly configured, it defaults to the standard docs layout. Sources: packages/preview/src/pages/[...slugs].tsx:80-88

Markdown Compilation and Container Rendering

The MdContent component builds the surrounding container structure, integrating AI search capabilities if supported by the environment. It checks if the requested page exists in the source tree; if missing, it either presents a home overview cards listing or a standard 404 Not Found view.

async function MdContent({
  slugs,
  page,
  components,
}: {
  slugs: string[];
  page?: SourcePage;
  components: MdPresetComponents;
}) {
  const config = await getConfigRuntime();
  const source = await getSource(config);
  const layout = layoutConfig(config);
  const { DocsLayout } = components.layout;
  const { DocsBody, DocsTitle, DocsPage, DocsDescription } = components.page;
 
  async function renderContainer(children: ReactNode) {
    const hasAI = await isAISupported();
    return (
      <DocsLayout {...await layout.docs()}>
        {hasAI && (
          <AISearch>
            <AISearchPanel />
            <AISearchTrigger
              position="float"
              className={cn(
                buttonVariants({
                  variant: 'secondary',
                  className: 'text-fd-muted-foreground rounded-2xl',
                }),
              )}
            >
              <MessageCircleIcon className="size-4.5" />
              Ask AI
            </AISearchTrigger>
          </AISearch>
        )}
 
        {children}
      </DocsLayout>
    );
  }

Sources: packages/preview/src/pages/...slugs.tsx:98-138

Markdown strings are processed through a configured compiler instance equipped with remark and rehype plugins for GitHub-Flavored Markdown, math equations, diagrams, heading anchors, package managers, code tabs, and syntax highlighting.

const compiler = createMarkdownCompiler({
  remarkPlugins: [
    remarkGfm,
    remarkMath,
    remarkMdxMermaid,
    plugin(remarkHeading, { generateToc: false }),
    plugin(remarkNpm, { persist: { id: 'package-manager' } }),
    remarkCodeTab,
  ],
  remarkRehypeOptions: {
    passThrough: ['mdxJsxFlowElement', 'mdxJsxTextElement'],
  },
  rehypePlugins: [
    rehypeKatex,
    plugin(rehypeCode, { lazy: true, fallbackLanguage: 'text' }),
    plugin(rehypeToc, { exportToc: { as: 'data' } }),
  ],
});

Sources: packages/preview/src/pages/...slugs.tsx:29-46

Caution

If compilation fails, the catch block intercepts the error and renders a dedicated error view displaying the error message and its stack trace inside a code block. Sources: packages/preview/src/pages/[...slugs].tsx:170-194

Running the live preview server

Standalone CLI Execution and Port Allocation

The preview runtime is executed via the fumadocs-preview command-line interface, defined using the commander package. The CLI provides two primary commands: init, which scaffolds a default configuration file named fumadocs.config.ts, and start, which serves as the default action for starting the live preview server.

program
  .command('init')
  .description('create configuration file')
  .action(async () => {
    if ((await findConfigPath()) === null) {
      await fs.writeFile(
        'fumadocs.config.ts',
        `import { defineConfig } from "fumadocs-preview/config";\n\nexport default defineConfig();`,
      );
 
      console.log('note: make sure to install `fumadocs-preview` as a dev dependency too.');
    }
  });
 
program
  .command('start', { isDefault: true })
  .description('start Fumapress')
  .argument('[dirs...]', 'a list of content directories to view')
  .option('-p, --port <PORT>')
  .option('-h, --host <HOST>')
  .action(async (dirs: string[] | undefined, options: { port?: string; host?: string }) => {
    await runStart({ ...options, dirs });
  });

Sources: packages/preview/src/cli/index.ts:8-30

CommandDescriptionArguments / Options
initCreate configuration fileNone
startStart Fumapress[dirs...], -p, --port <PORT>, -h, --host <HOST>

Sources: packages/preview/src/cli/index.ts:8-30

When the start command executes, runStart loads environment variables via loadEnv() and overrides NODE_ENV to production. It invokes getFreePort starting from a default port of 8080 (or a custom port specified via options) to find an available TCP port by probing network sockets with net.createServer(). If the port is occupied with an EADDRINUSE error, it automatically increments the port number until an open socket is secured.

async function getFreePort(startPort: number): Promise<number> {
  for (let port = startPort; ; port++) {
    try {
      await new Promise<void>((resolve, reject) => {
        const srv = net
          .createServer()
          .once('error', reject)
          .once('listening', () => srv.close(() => resolve()))
          .listen(port);
      });
      return port;
    } catch (err) {
      if ((err as NodeJS.ErrnoException).code !== 'EADDRINUSE') {
        throw err;
      }
    }
  }
}

Sources: packages/preview/src/cli/commands.ts:36-53

Warning

If getFreePort encounters any socket error other than EADDRINUSE, it does not retry and immediately rejects with that error. Sources: packages/preview/src/cli/commands.ts:47-51

Environment Setup and Server Bootstrapping

Prior to launching the server bundle, runStart configures several required process environment variables to pass parameters down to the Waku server runtime.

  if (host) {
    process.env.HOST = host;
  }
  process.env.PORT = String(port);
  process.env.ROOT_DIR = process.cwd();
  if (!dirs || dirs.length === 0) {
    dirs = [''];
  }
  process.env.DEFAULT_PROJECT_DIR = JSON.stringify(dirs.map((v) => path.resolve(v)));
  process.env.HOT_RELOAD = '1';
  process.chdir(baseDir);
  await import(serveFileUrl);
 
  console.log(`ready: Listening on http://${host || 'localhost'}:${port}/`);
}

Sources: packages/preview/src/cli/commands.ts:20-34

Environment VariableAssigned Value / LogicPurpose
HOSThost option parameter (if provided)Binds the server to a specific network host address
PORTString(port)Sets the listening port resolved by getFreePort
ROOT_DIRprocess.cwd()Establishes the root directory context for project file resolution
DEFAULT_PROJECT_DIRJSON string of dirs mapped via path.resolve()Specifies content directories to view; defaults to [''] if empty
HOT_RELOAD'1'Enables dynamic hot-reloading behavior in the preview environment

Sources: packages/preview/src/cli/commands.ts:20-29

Tip

After configuring environment variables, runStart changes the working directory to baseDir and dynamically imports the compiled Node server entry point (dist/waku/serve-node.js) using pathToFileURL. Sources: packages/preview/src/cli/commands.ts:5-31

Enabling dynamic local markdown updates

Local Markdown Source Configuration and Compilation

The @fumadocs/local-md package provides the localMd factory function to manage local markdown and metadata files as a content source. It accepts a LocalMarkdownConfig object defining the root content directory (dir), glob patterns (include), frontmatter and metadata schemas, and optional compiler settings.

export interface LocalMarkdownConfig<
  FrontmatterSchema extends StandardSchemaV1,
  MetaSchema extends StandardSchemaV1,
> extends MarkdownCompilerOptions {
  /**
   * root directory for content files.
   */
  dir: string;
  /**
   * a list of glob patterns, customize the content files to be scanned.
   */
  include?: string[];
  rendererOptions?: Pick<MarkdownRendererASTOptions, 'executor'>;
 
  frontmatterSchema?: FrontmatterSchema;
  metaSchema?: MetaSchema;
}

Sources: packages/local-md/src/index.ts:24-40

Configuration PropertyTypeDefault / Purpose
dirstringRoot directory for content files
includestring[]Glob patterns to scan content files
frontmatterSchemaStandardSchemaV1Validation schema for page frontmatter
metaSchemaStandardSchemaV1Validation schema for metadata files

Sources: packages/local-md/src/index.ts:24-40

When staticSource or dynamicSource is invoked, createFiles reads pages and metadata via internal storage and builds virtual files. Each page includes a load function that checks a weak map cache (compilerCache) or compiles the markdown content using createMarkdownCompiler. Depending on the compiler result type, it delegates to fromAst or fromJS to generate the final renderer.

async load() {
  let promise = compilerCache.get(page);
  if (!promise) {
    promise = compiler.compile({
      path: page.absolutePath,
      value: page.content,
      data: { frontmatter: page.frontmatter },
    });
  }
 
  const res = await promise;
  return res.type === 'ast'
    ? fromAst({
        tree: res.tree,
        filePath: res.file.path,
        rehypeToc: res.file.data.rehypeToc,
        structuredData: res.file.data.structuredData,
        ...config.rendererOptions,
      })
    : fromJS({
        code: res.code,
        filePath: res.file.path,
        baseUrl: pathToFileURL(res.file.path).href,
        structuredData: res.file.data.structuredData,
      });
},

Sources: packages/local-md/src/index.ts:125-150

Note

The compilerCache uses a WeakMap<RawPage<unknown>, Promise<CompileResult>> to memoize compilation promises per raw page object and prevent redundant parsing during development. Sources: packages/local-md/src/index.ts:90-92

Hot-Reloading via Development Server and WebSocket Clients

To support real-time editing, the localMd instance exposes a devServer method and an invalidateFile mechanism. When devServer is called, it connects to a WebSocket server, sends a watch-dir message with the configured directory and glob patterns, and listens for change events to invalidate modified files.

async devServer(url = getDevServerUrlFromEnv()) {
  if (!url) {
    console.warn(
      `[@fumadocs/local-md] dev server URL could not be found, try passing the URL to devServer() explicitly instead`,
    );
    return;
  }
 
  const { connectDevServer } = await import('@/dev/node-client');
  const conn = connectDevServer(url);
  conn.send({
    type: 'watch-dir',
    dir: path.resolve(config.dir),
    includes: config.include ?? defaultInclude,
  });
 
  conn.subscribe((event) => {
    if (event.type === 'change') {
      this.invalidateFile(event.absolutePath);
    }
  });
},

Sources: packages/local-md/src/index.ts:174-195

The React client component DevClient establishes a WebSocket connection to the development server using the URL resolved by getDevServerUrlFromEnv. Upon receiving an incoming message, it decodes the event using decodeDevEvent. If the decoded event type is a change, it logs the update and calls router.refresh() to trigger an immediate page reload in the framework router.

export function DevClient(): ReactNode {
  const router = useRouter();
 
  const onUpdate = useEffectEvent((event: DevServerEvent) => {
    if (event.type === 'change') {
      console.log(`[@fumadocs/local-md] "${event.absolutePath}" updated`);
      router.refresh();
    }
  });
 
  useEffect(() => {
    const url = getDevServerUrlFromEnv();
    if (!url) return;
 
    const ws = new WebSocket(url);
    ws.onopen = () => {
      console.log(`[@fumadocs/local-md] connected to dev server at ${url}`);
    };
 
    ws.onmessage = (event) => {
      const decoded = decodeDevEvent(String(event.data));
      if (decoded) onUpdate(decoded);
    };
 
    ws.onclose = () => {
      console.log(`[@fumadocs/local-md] disconnected from dev server at ${url}`);
    };
 
    return () => {
      ws.close();
    };
  }, []);
 

Sources: packages/local-md/src/dev/react-client.ts:6-38

Warning

If getDevServerUrlFromEnv returns no URL during client initialization or inside devServer(), the connection attempt is aborted with a warning message or early return. Sources: packages/local-md/src/index.ts:174-180, packages/local-md/src/dev/react-client.ts:16-18

Was this page helpful?