---
title: "Preview Server"
description: "The Fumadocs preview server provides an opinionated runtime environment built on Waku and Hono to instantly scaffold, render, and live-reload documentation projects [[packages/preview/package.json:..."
last_updated: "2026-10-12T07:53:59.736226+00:00"
canonical_url: "https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/developer-tooling/preview-server"
---

<details>
<summary>Relevant source files</summary>

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

- [packages/preview/src/waku.server.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/waku.server.tsx)
- [packages/preview/src/cli/commands.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/cli/commands.ts)
- [packages/preview/src/components/hot-reload.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/hot-reload.tsx)
- [packages/preview/src/pages/_api/og/...slugs/image.webp.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/og/%5B...slugs%5D/image.webp.tsx)
- [packages/preview/src/pages/...slugs.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx)
- [packages/preview/src/components/ai/search.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/ai/search.tsx)
- [packages/openapi/src/server/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/server/index.tsx)
- [packages/preview/waku.config.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/waku.config.ts)
- [packages/preview/src/pages/_api/api/chat.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/chat.ts)
- [packages/asyncapi/src/server/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/server/index.tsx)
- [packages/core/src/framework/waku.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/framework/waku.tsx)
- [packages/preview/src/pages/_root.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_root.tsx)
- [packages/preview/src/pages/_api/img.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/img.ts)
- [packages/preview/src/components/provider.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/provider.tsx)
- [packages/local-md/src/dev/react-client.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/dev/react-client.ts)
- [packages/mdx/src/runtime/dynamic.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/runtime/dynamic.ts)
- [packages/local-md/src/dev/node-server.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/dev/node-server.ts)
- [packages/base-ui/src/provider/waku.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/provider/waku.tsx)
- [packages/preview/src/lib/ai.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/ai.ts)
- [packages/preview/src/lib/source/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/source/index.ts)
- [packages/mdx/src/runtime/server.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/runtime/server.ts)
- [packages/preview/src/pages/_api/api/search.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/search.ts)
- [packages/core/src/search/orama/create-server.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/search/orama/create-server.ts)
- [packages/preview/package.json](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/package.json)
- [packages/preview/src/lib/env.d.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/env.d.ts)
- [packages/radix-ui/src/provider/waku.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/provider/waku.tsx)
- [packages/preview/src/cli/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/cli/index.ts)
- [packages/preview/src/lib/source/watcher.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/source/watcher.ts)
- [packages/preview/fumadocs.config.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/fumadocs.config.ts)
- [packages/base-ui/src/og/takumi.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/og/takumi.tsx)
</details>

## Overview

The Fumadocs preview server provides an opinionated runtime environment built on Waku and Hono to instantly scaffold, render, and live-reload documentation projects [[packages/preview/package.json:4-4], [packages/preview/src/waku.server.tsx:1-38]]. By combining file-system routing with reactive source collection loaders, it resolves markdown and MDX files dynamically while serving specialized endpoints for full-text search, AI-assisted chat, and dynamic OpenGraph image generation [[packages/preview/src/pages/%5B...slugs%5D.tsx:76-96], [packages/preview/src/pages/_api/api/chat.ts:143-156], [packages/preview/src/pages/_api/api/search.ts:23-27], [packages/preview/src/pages/_api/og/%5B...slugs%5D/image.webp.tsx:7-21]].

Sources: [packages/preview/package.json:4-4](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/package.json#L4-L4), [packages/preview/src/waku.server.tsx:1-38](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/waku.server.tsx#L1-L38), [packages/preview/src/pages/...slugs.tsx:76-96](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx#L76-L96), [packages/preview/src/pages/_api/api/chat.ts:143-156](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/chat.ts#L143-L156), [packages/preview/src/pages/_api/api/search.ts:23-27](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/search.ts#L23-L27), [packages/preview/src/pages/_api/og/...slugs/image.webp.tsx:7-21](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/og/%5B...slugs%5D/image.webp.tsx#L7-L21)

## CLI Startup and Server Bootstrap

### Command Initiation and Environment Configuration

The CLI entry point uses `commander` to register the default `start` command alongside an `init` utility command [packages/preview/src/cli/index.ts:8-30](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/cli/index.ts#L8-L30). When executed, `runStart` loads environment variables and overrides `NODE_ENV` to production [packages/preview/src/cli/commands.ts:16-17](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/cli/commands.ts#L16-L17).

```typescript
export async function runStart({
  host,
  dirs,
  port: defaultPort = '8080',
}: {
  dirs?: string[];
  host?: string;
  port?: string;
}) {
  loadEnv();
  overrideNodeEnv('production');
  const port = await getFreePort(parseInt(defaultPort, 10));
  const serveFileUrl = pathToFileURL(path.resolve(baseDir, 'dist', 'waku', 'serve-node.js')).href;
  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:7-34](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/cli/commands.ts#L7-L34)

### Port Resolution and Adapter Bootstrapping

Port allocation is managed by probing `net.createServer` incrementally starting from the default port (`8080`) until an available socket is found, ignoring non-in-use errors [packages/preview/src/cli/commands.ts:36-52](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/cli/commands.ts#L36-L52). The Waku Node adapter evaluates file-system router modules from `src/pages` and configures middleware for hot reloading [packages/preview/src/waku.server.tsx:1-38](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/waku.server.tsx#L1-L38).

```tsx
import { createNodeWebSocket, NodeWebSocket } from '@hono/node-ws';
import adapter from 'waku/adapters/node';
import { fsRouter } from 'waku/router/server';
import { startWatcher } from './lib/source/watcher';
import { getConfigRuntime } from './config/load-runtime';
import { encodeEvent, WebSocketEvent } from './lib/waku/hot-reload';
import { filesCache } from './lib/source/storage';
import { serve } from '@hono/node-server';
import { getSource } from './lib/source';

const modules = import.meta.glob('./pages/**/*.{ts,tsx,js,jsx}', {
  base: '/src',
});

const pages = fsRouter(modules);
let nodeWs: NodeWebSocket | undefined;

const v = adapter(pages, {
  middlewareFns: [
    ({ app }) => {
      if (process.env.HOT_RELOAD === '1') {
        nodeWs = createNodeWebSocket({ app });

        void initHotReload().catch((e) => {
          console.error(e);
          process.exit(1);
        });

        app.get(
          '/_ws',
          nodeWs.upgradeWebSocket(() => ({})),
        );
      }

      return (_c, next) => next();
    },
  ],
});
```
Sources: [packages/preview/src/waku.server.tsx:1-38](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/waku.server.tsx#L1-L38)

## File Watching and Live Hot Reloading

### Source Tree Watcher Initialization

The preview server initializes file system monitoring using Chokidar through `startWatcher`, which parses application configuration to normalize project directories [packages/preview/src/lib/source/watcher.ts:8-9](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/source/watcher.ts#L8-L9). It builds a set of ignored patterns by asynchronously reading `.gitignore` files from both the root working directory (falling back to `node_modules`, `dist`, and `build`) and individual project paths, utilizing the `ignore` package to validate and convert rules into Chokidar-compatible matcher functions [packages/preview/src/lib/source/watcher.ts:10-20](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/source/watcher.ts#L10-L20), [packages/preview/src/lib/source/watcher.ts:29-48](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/source/watcher.ts#L29-L48). 

```typescript
const watcher = watch([], {
  ignoreInitial: true,
  followSymlinks: false,
  ignored: ignored.filter((v) => v !== undefined),
});

for (const project of projects) {
  if (project.watch) watcher.add(project.dir);
}
```
Sources: [packages/preview/src/lib/source/watcher.ts:16-24](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/source/watcher.ts#L16-L24)

The Chokidar watcher is instantiated with specific options to skip initial addition events, disable symlink following, and filter paths against the compiled ignore rules [packages/preview/src/lib/source/watcher.ts:16-20](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/source/watcher.ts#L16-L20). It iterates over normalized projects and adds each project directory to the watch list if `project.watch` is enabled [packages/preview/src/lib/source/watcher.ts:22-24](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/source/watcher.ts#L22-L24).

> [!NOTE]
> Initial file discovery events are ignored via `ignoreInitial: true` to prevent triggering redundant revalidation cycles during server boot.

Sources: [packages/preview/src/lib/source/watcher.ts:16-17](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/source/watcher.ts#L16-L17)

### WebSocket Event Propagation

When `HOT_RELOAD` is enabled (`HOT_RELOAD === '1'`), the server attaches a Node WebSocket instance via `@hono/node-ws` on the `/_ws` route [packages/preview/src/waku.server.tsx:21-33](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/waku.server.tsx#L21-L33). The `initHotReload` function coordinates watcher events with connected WebSocket clients by encoding messages through `encodeEvent` and broadcasting them across all active client sockets in `wss.clients` [packages/preview/src/waku.server.tsx:56-64](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/waku.server.tsx#L56-L64).

```tsx
watcher.on('all', (event, filePath) => {
  switch (event) {
    case 'change':
    case 'unlink':
      // if file wasn't parsed, no revalidation is needed
      if (!filesCache.has(filePath)) return;
  }

  if (event === 'change') {
    filesCache.delete(filePath);
    send({
      type: 'clear-cache',
      absolutePath: filePath,
    });
  }

  switch (event) {
    case 'add':
    case 'change':
    case 'unlink':
      getSource.revalidate(false);
      send({
        type: 'revalidate',
      });
      break;
  }
});
```
Sources: [packages/preview/src/waku.server.tsx:66-92](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/waku.server.tsx#L66-L92)

File system modifications trigger specific handling depending on the Chokidar event type:

| Event Type | Cache Action | Source Revalidation | WebSocket Payload Sent |
| :--- | :--- | :--- | :--- |
| `change` | Deletes entry from `filesCache` if present | Triggers `getSource.revalidate(false)` | Clears cache path, then sends `revalidate` |
| `unlink` | Bypasses cache eviction if unparsed | Triggers `getSource.revalidate(false)` | Sends `revalidate` |
| `add` | None | Triggers `getSource.revalidate(false)` | Sends `revalidate` |

Sources: [packages/preview/src/waku.server.tsx:66-92](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/waku.server.tsx#L66-L92)

> [!WARNING]
> If a changed or unlinked file is absent from `filesCache` (`filesCache.has(filePath)` is false), cache clearing is skipped to avoid unnecessary revalidation overhead for unparsed assets [packages/preview/src/waku.server.tsx:70-72](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/waku.server.tsx#L70-L72).

Sources: [packages/preview/src/waku.server.tsx:70-72](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/waku.server.tsx#L70-L72)

### Client-Side Page Re-Rendering

Client-side hot reloading is integrated into root element rendering via the `HotReload` component when `HOT_RELOAD === '1'` [packages/preview/src/pages/_root.tsx:13](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_root.tsx#L13). The client establishes a WebSocket connection to `/_ws`, dynamically switching between `ws://` and `wss://` protocols based on the current window location [packages/preview/src/components/hot-reload.tsx:8-11](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/hot-reload.tsx#L8-L11).

```tsx
useEffect(() => {
  const url = new URL('/_ws', window.location.href);
  url.protocol = url.protocol === 'https' ? 'wss' : 'ws';
  const socket = new WebSocket(url);

  socket.addEventListener('message', (event) => {
    const decoded = decodeEvent(event.data);
    if (!decoded) return;

    console.log('Received:', decoded);
    if (decoded.type === 'revalidate') {
      void router.reload();
    }
  });

```
Sources: [packages/preview/src/components/hot-reload.tsx:8-22](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/hot-reload.tsx#L8-L22)

Upon receiving incoming messages, the browser runtime decodes the event payload using `decodeEvent` [packages/preview/src/components/hot-reload.tsx:13-14](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/hot-reload.tsx#L13-L14). If the decoded event type matches `revalidate`, the client invokes `router.reload()` from Waku to fetch updated server content and re-render the page without a full browser refresh [packages/preview/src/components/hot-reload.tsx:18-20](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/hot-reload.tsx#L18-L20).

## Content Source Loading and Page Routing

### Reactive Source Collection Loader

The content source collection is initialized through `getSource`, which wraps a revalidable factory function utilizing `loader` and `source` from `fumadocs-core/source` alongside `lucideIconsPlugin()` with a base URL of `'/'` [packages/preview/src/lib/source/index.ts:7-15](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/source/index.ts#L7-L15). This loader processes raw page records retrieved from file storage via `getPages(config.content)` based on the parsed global configuration [packages/preview/src/lib/source/index.ts:4-10](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/source/index.ts#L4-L10). Page image routes are constructed by appending `'image.webp'` to the provided slug segments under the `/og/` path prefix [packages/preview/src/lib/source/index.ts:20-26](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/source/index.ts#L20-L26).

```typescript
export const getSource = revalidable({
  async create(config: ParsedAppConfig) {
    return loader({
      source: source(await getPages(config.content)),
      plugins: [lucideIconsPlugin()],
      baseUrl: '/',
    });
  },
});
```
Sources: [packages/preview/src/lib/source/index.ts:7-15](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/source/index.ts#L7-L15)

> [!NOTE]
> The source collection leverages `revalidable` wrapper semantics to ensure that file system modifications trigger cache invalidation without requiring a full application restart [packages/preview/src/lib/source/index.ts:7](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/source/index.ts#L7).

Sources: [packages/preview/src/lib/source/index.ts:7](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/source/index.ts#L7)

### Root Layouts and Provider Configuration

Root HTML document structure and client context wrapping are established in the root layout module [packages/preview/src/pages/_root.tsx:7-17](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_root.tsx#L7-L17). The `RootElement` component renders an `<html>` element with language metadata and suppression of hydration warnings, containing a `<body>` tag configured with a minimum screen height and a version data attribute (`data-version="1.0"`) [packages/preview/src/pages/_root.tsx:7-11](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_root.tsx#L7-L11). Client-side UI context is provided by wrapping children in the `Provider` component, which embeds `RootProvider` from `fumadocs-ui/provider/waku` [packages/preview/src/components/provider.tsx:5-7](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/provider.tsx#L5-L7). When `HOT_RELOAD === '1'`, the `HotReload` component is additionally injected into the document body [packages/preview/src/pages/_root.tsx:13](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_root.tsx#L13). Both the root layout and dynamic route components export a `getConfig` function defining a `'dynamic'` render mode [[packages/preview/src/pages/_root.tsx:19-23], [packages/preview/src/pages/%5B...slugs%5D.tsx:225-228]].

```tsx
export default async function RootElement({ children }: { children: ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head></head>
      <body data-version="1.0" className="flex flex-col min-h-screen">
        <Provider>{children}</Provider>
        {process.env.HOT_RELOAD === '1' && <HotReload />}
      </body>
    </html>
  );
}
```
Sources: [packages/preview/src/pages/_root.tsx:7-17](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_root.tsx#L7-L17)

### Dynamic Catch-All Route Rendering

Catch-all route rendering is managed by the `DocPage` component located at `[...slugs].tsx`, which retrieves runtime configuration and loads the content source [packages/preview/src/pages/%5B...slugs%5D.tsx:76-79](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx#L76-L79). Depending on the configured layout preset (`config.layout.presets?.md`), the page dynamically imports either the `'docs'` or `'flux'` layout and page component sets from `fumadocs-ui/layouts` [packages/preview/src/pages/%5B...slugs%5D.tsx:80-93](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx#L80-L93).

| Layout Preset | Layout Import Path | Page Component Import Path |
| :--- | :--- | :--- |
| `docs` | `fumadocs-ui/layouts/docs` | `fumadocs-ui/layouts/docs/page` |
| Other (Flux) | `fumadocs-ui/layouts/flux` | `fumadocs-ui/layouts/flux/page` |

Sources: [packages/preview/src/pages/...slugs.tsx:83-93](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx#L83-L93)

Markdown compilation is executed via a pre-configured markdown compiler instance incorporating remark and rehype plugins [packages/preview/src/pages/%5B...slugs%5D.tsx:29-46](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx#L29-L46).

```tsx
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx#L29-L46)

The remark and rehype plugin suite applied during compilation consists of the following processing modules:

| Plugin Category | Plugin Name | Configuration Options / Behavior |
| :--- | :--- | :--- |
| Remark | `remarkGfm` | GitHub Flavored Markdown support |
| Remark | `remarkMath` | Mathematical notation parsing |
| Remark | `remarkMdxMermaid` | Mermaid diagram embedding in MDX |
| Remark | `remarkHeading` | Heading processing (`generateToc: false`) |
| Remark | `remarkNpm` | Package manager tab persistence (`persist: { id: 'package-manager' }`) |
| Remark | `remarkCodeTab` | Code block tab grouping |
| Rehype | `rehypeKatex` | KaTeX math rendering |
| Rehype | `rehypeCode` | [Syntax highlighting](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/core-architecture/syntax-highlighting) (`lazy: true`, `fallbackLanguage: 'text'`) |
| Rehype | `rehypeToc` | [Table of contents](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/ui-layouts/table-of-contents) extraction (`exportToc: { as: 'data' }`) |

Sources: [packages/preview/src/pages/...slugs.tsx:30-45](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx#L30-L45)

> [!WARNING]
> If compilation fails due to malformed syntax or missing references, the renderer catches the error and displays a fallback container rendering the exact error message and stack trace inside a code block [packages/preview/src/pages/%5B...slugs%5D.tsx:170-194](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx#L170-L194).

Sources: [packages/preview/src/pages/...slugs.tsx:170-194](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx#L170-L194)

## AI Assistant and Flexsearch Endpoints

### Flexsearch Search Indexing Endpoint

The preview server exposes a dedicated search endpoint via `GET(request)` in `packages/preview/src/pages/_api/api/search.ts` that provides full-text document querying for the client search panel [packages/preview/src/pages/_api/api/search.ts:23-28](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/search.ts#L23-L28). It relies on a cached server instance created through the `revalidable` wrapper [packages/preview/src/pages/_api/api/search.ts:7-21](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/search.ts#L7-L21). The server compiles the content source using `flexsearchFromSource`, building index entries for every page [packages/preview/src/pages/_api/api/search.ts:8-19](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/search.ts#L8-L19).

```typescript
return flexsearchFromSource(source, {
  buildIndex(page) {
    return {
      id: page.absolutePath!,
      structuredData: structure(page.data.content),
      title: page.data.title,
      description: page.data.description,
      url: page.url,
    };
  },
```
Sources: [packages/preview/src/pages/_api/api/search.ts:9-18](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/search.ts#L9-L18)

Each indexed document extracts the absolute file path as its identifier, uses `structure(page.data.content)` to extract structural data, and maps the title, description, and target URL [packages/preview/src/pages/_api/api/search.ts:10-17](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/search.ts#L10-L17). The underlying `server.GET(request)` handler processes incoming query parameters to return matching documentation segments.

Sources: [packages/preview/src/pages/_api/api/search.ts:23-27](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/search.ts#L23-L27)

### AI Chat Endpoint and Model Routing

The server-side chat API located in `packages/preview/src/pages/_api/api/chat.ts` processes incoming user messages through the AI SDK `streamText` function [packages/preview/src/pages/_api/api/chat.ts:129-138](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/chat.ts#L129-L138). Before executing completions, the handler validates AI support via `isAISupported()` and enforces IP-based rate limits if enabled in the runtime configuration [packages/preview/src/pages/_api/api/chat.ts:110-125](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/chat.ts#L110-L125).

| Rate Limit Setting / Variable | Default Value / Behavior | Source Reference |
| :--- | :--- | :--- |
| `rateLimitWindowMs` | `60 * 1000` (60 seconds) | [packages/preview/src/pages/_api/api/chat.ts:55](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/chat.ts#L55) |
| `rateLimitMaxRequests` | `20` requests per window | [packages/preview/src/pages/_api/api/chat.ts:56](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/chat.ts#L56) |
| IP Headers Checked | `x-forwarded-for`, `cf-connecting-ip`, `x-real-ip`, `x-client-ip` | [packages/preview/src/pages/_api/api/chat.ts:63-77](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/chat.ts#L63-L77) |

Sources: [packages/preview/src/pages/_api/api/chat.ts:55-77](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/chat.ts#L55-L77), [packages/preview/src/pages/_api/api/chat.ts:110-125](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/chat.ts#L110-L125)

> [!WARNING]
> If a client exceeds `rateLimitMaxRequests` within the `rateLimitWindowMs` sliding window, the `POST` handler aborts immediately and returns a `429 Too Many Requests` status alongside a `Retry-After` response header indicating the remaining window time in seconds.

Sources: [packages/preview/src/pages/_api/api/chat.ts:98-103](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/chat.ts#L98-L103), [packages/preview/src/pages/_api/api/chat.ts:118-124](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/chat.ts#L118-L124)

Language models are initialized through `defaultModel()` in `packages/preview/src/lib/ai.ts`, which dynamically imports `@openrouter/ai-sdk-provider` and configures OpenRouter authentication using `OPENROUTER_API_KEY` and `OPENROUTER_MODEL` (defaulting to `anthropic/claude-3.5-sonnet`) [packages/preview/src/lib/ai.ts:4-11](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/ai.ts#L4-L11).

```typescript
export async function defaultModel(): Promise<LanguageModel> {
  const { createOpenRouter } = await import('@openrouter/ai-sdk-provider');
  const openrouter = createOpenRouter({
    apiKey: process.env.OPENROUTER_API_KEY,
  });

  return openrouter.chat(process.env.OPENROUTER_MODEL ?? 'anthropic/claude-3.5-sonnet');
}
```
Sources: [packages/preview/src/lib/ai.ts:4-11](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/ai.ts#L4-L11)

Sources: [packages/preview/src/lib/ai.ts:13-16](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/ai.ts#L13-L16)

### Server-Side Search Tool and Prompt Architecture

The chat completion stream equips the language model with a server-side `search` tool built using Flexsearch [[packages/preview/src/pages/_api/api/chat.ts:23-44], [packages/preview/src/pages/_api/api/chat.ts:143-156]]. The tool accepts a `query` string and an integer `limit` between 1 and 100 (defaulting to 10), executing `searchAsync` with index merging and enrichment enabled [packages/preview/src/pages/_api/api/chat.ts:145-154](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/chat.ts#L145-L154).

```typescript
const searchTool = tool({
  description: 'Search the docs content and return raw JSON results.',
  inputSchema: z.object({
    query: z.string(),
    limit: z.number().int().min(1).max(100).default(10),
  }),
  async execute({ query, limit }) {
    const config = await getConfigRuntime();
    const source = await getSource(config);
    const search = createSearchServer(source);

    return await search.searchAsync(query, { limit, merge: true, enrich: true });
  },
});
```
Sources: [packages/preview/src/pages/_api/api/chat.ts:143-156](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/chat.ts#L143-L156)

The system prompt instructs the assistant to invoke the `search` tool to fetch documentation context before formulating answers, ground its responses in raw JSON search payload results, and cite sources as markdown links using the document `url` field [packages/preview/src/pages/_api/api/chat.ts:46-51](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/chat.ts#L46-L51). The completion runner limits execution steps to a maximum count of 5 via `stepCountIs(5)` and serializes responses through `result.toUIMessageStreamResponse()` [packages/preview/src/pages/_api/api/chat.ts:129-141](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/chat.ts#L129-L141).

Sources: [packages/preview/src/pages/_api/api/chat.ts:46-51](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/chat.ts#L46-L51), [packages/preview/src/pages/_api/api/chat.ts:129-141](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/api/chat.ts#L129-L141)

### Client Search Panel and AI Chat State

The client-side UI components in `packages/preview/src/components/ai/search.tsx` manage the interactive chat drawer and message list [[packages/preview/src/components/ai/search.tsx:23-27], [packages/preview/src/components/ai/search.tsx:331-388]]. The `AISearch` wrapper component initializes the chat state using `useChat` from `@ai-sdk/react`, pointing its transport layer to `/api/chat` via `DefaultChatTransport` [packages/preview/src/components/ai/search.tsx:292-299](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/ai/search.tsx#L292-L299).

```tsx
export function AISearch({ children }: { children: ReactNode }) {
  const [open, setOpen] = useState(false);
  const chat = useChat({
    id: 'search',
    transport: new DefaultChatTransport({
      api: '/api/chat',
    }),
  });

```
Sources: [packages/preview/src/components/ai/search.tsx:292-300](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/ai/search.tsx#L292-L300)

Keyboard shortcuts are handled by `useHotKey()`, which listens for `Escape` to close the panel when open, and `Cmd+/` or `Ctrl+/` to toggle the AI search panel open [packages/preview/src/components/ai/search.tsx:420-439](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/ai/search.tsx#L420-L439). The chat message list filters out system messages and displays empty state placeholders or rendered markdown message elements [packages/preview/src/components/ai/search.tsx:390-417](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/ai/search.tsx#L390-L417).

Sources: [packages/preview/src/components/ai/search.tsx:390-417](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/ai/search.tsx#L390-L417), [packages/preview/src/components/ai/search.tsx:420-439](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/ai/search.tsx#L420-L439)

## Dynamic OG Image and Media Handling

### OpenGraph Image Generation

The preview server generates dynamic OpenGraph images using `@takumi-rs/image-response` through the dynamic catch-all route at `packages/preview/src/pages/_api/og/[...slugs]/image.webp.tsx` packages/preview/src/pages/_api/og/%5B...slugs%5D/image.webp.tsx:5-21. The GET handler resolves runtime configuration via `getConfigRuntime()`, retrieves the content source via `getSource(config)`, and looks up the page matching the requested slug parameters [packages/preview/src/pages/_api/og/%5B...slugs%5D/image.webp.tsx:8-9](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/og/%5B...slugs%5D/image.webp.tsx#L8-L9). If no page exists, the endpoint returns a `404` status response [packages/preview/src/pages/_api/og/%5B...slugs%5D/image.webp.tsx:11](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/og/%5B...slugs%5D/image.webp.tsx#L11).

```tsx
export async function GET(_: Request, { params }: ApiContext<'/og/[...slugs]/image.webp'>) {
  const config = await getConfigRuntime();
  const page = (await getSource(config)).getPage(params.slugs);

  if (!page) return new Response(undefined, { status: 404 });

  return new ImageResponse(
    <DefaultImage title={page.data.title} description={page.data.description} site="Fumapress" />,
    {
      width: 1200,
      height: 630,
      format: 'webp',
    },
  );
}
```
Sources: [packages/preview/src/pages/_api/og/...slugs/image.webp.tsx:7-21](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/og/%5B...slugs%5D/image.webp.tsx#L7-L21)

When the page is found, `ImageResponse` renders the default layout component provided by `fumadocs-ui/og/takumi` with a width of 1200 pixels, a height of 630 pixels, and the WebP format [packages/preview/src/pages/_api/og/%5B...slugs%5D/image.webp.tsx:13-20](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/og/%5B...slugs%5D/image.webp.tsx#L13-L20). The underlying generator function from `packages/base-ui/src/og/takumi.tsx` configures a flex column container with a default background color of `#0c0c0c`, a padding of `4rem`, and a primary accent bottom border [packages/base-ui/src/og/takumi.tsx:55-65](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/og/takumi.tsx#L55-L65).

Sources: [packages/base-ui/src/og/takumi.tsx:55-65](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/og/takumi.tsx#L55-L65)

The configuration object exported by `packages/preview/src/pages/_api/og/[...slugs]/image.webp.tsx` sets `render` to `dynamic` [packages/preview/src/pages/_api/og/%5B...slugs%5D/image.webp.tsx:23-26](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/og/%5B...slugs%5D/image.webp.tsx#L23-L26).

```tsx
export async function getConfig() {
  return {
    render: 'dynamic' as const,
  } as const;
```
Sources: [packages/preview/src/pages/_api/og/...slugs/image.webp.tsx:23-26](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/og/%5B...slugs%5D/image.webp.tsx#L23-L26)

### Runtime Image Asset Serving

The endpoint located at `packages/preview/src/pages/_api/img.ts` serves local media files referenced within documentation [packages/preview/src/pages/_api/img.ts:19-66](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/img.ts#L19-L66). The request URL search parameters supply `src`, `page` (the absolute path of the page), and `project` (the project directory identifier) [packages/preview/src/pages/_api/img.ts:20-25](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/img.ts#L20-L25). If any of these parameters are missing or not strings, the handler returns a `404` status response [packages/preview/src/pages/_api/img.ts:27-29](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/img.ts#L27-L29).

```typescript
export async function GET(request: Request) {
  const searchParams = new URL(request.url).searchParams;
  const src = searchParams.get('src');
  // absolute path of page
  const pagePath = searchParams.get('page');
  // project dir
  const projectDir = searchParams.get('project');

  if (typeof src !== 'string' || typeof pagePath !== 'string' || typeof projectDir !== 'string') {
    return new Response(null, { status: 404 });
  }

```
Sources: [packages/preview/src/pages/_api/img.ts:19-30](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/img.ts#L19-L30)

The server validates the project directory against runtime configuration projects, returning a `400` status with `invalid project directory` if the project is unrecognized [packages/preview/src/pages/_api/img.ts:31-34](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/img.ts#L31-L34). It then constructs possible file paths combining the page directory, project asset directories, or fallback root directories defined by `process.env.ROOT_DIR` or `process.cwd()` [packages/preview/src/pages/_api/img.ts:36-45](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/img.ts#L36-L45).

```typescript
const config = await getConfigRuntime();
const projects = normalizeProjects(config.content.projects);
const project = projects.find((item) => item.dir === projectDir);
if (!project) return new Response('invalid project directory', { status: 400 });

const possiblePaths: string[] = [path.join(path.dirname(pagePath), src)];

if (project.assetsDir) {
  for (const dir of project.assetsDir) {
    possiblePaths.push(path.join(dir, src));
  }
} else {
  const rootDir = process.env.ROOT_DIR ?? process.cwd();
  possiblePaths.push(path.join(rootDir, src), path.join(rootDir, 'public', src));
}
```
Sources: [packages/preview/src/pages/_api/img.ts:31-45](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/img.ts#L31-L45)

> [!WARNING]
> File system traversal checks are skipped if `content.unsafe_disableFileSystemChecks` is enabled in configuration, allowing paths outside the project directory [packages/preview/src/pages/_api/img.ts:47-49](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/img.ts#L47-L49).

Supported image formats map specific file extensions to their respective MIME types as defined in `supportedImageFormats` [packages/preview/src/pages/_api/img.ts:7-17](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/img.ts#L7-L17).

| File Extension | MIME Type |
| :--- | :--- |
| `.png` | `image/png` |
| `.jpg` | `image/jpeg` |
| `.jpeg` | `image/jpeg` |
| `.webp` | `image/webp` |
| `.gif` | `image/gif` |
| `.svg` | `image/svg+xml` |
| `.avif` | `image/avif` |
| `.bmp` | `image/bmp` |
| `.ico` | `image/x-icon` |

Sources: [packages/preview/src/pages/_api/img.ts:7-17](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/img.ts#L7-L17)

When a matching file path with a supported extension is successfully read via `fs.readFile`, the handler responds with the file buffer and sets the `Content-Type` header to the corresponding MIME type [packages/preview/src/pages/_api/img.ts:48-63](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/img.ts#L48-L63). If all candidate paths fail, it returns a `404` status response [packages/preview/src/pages/_api/img.ts:65](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/img.ts#L65).

```typescript
  for (const file of possiblePaths) {
    if (!disableChecks && path.relative(project.dir, file).startsWith(`..${path.sep}`)) continue;

    const mimeType = supportedImageFormats[path.extname(file)];
    if (!mimeType) continue;

    try {
      return new Response(await fs.readFile(file), {
        headers: {
          'Content-Type': mimeType,
        },
      });
    } catch {
      continue;
    }
  }

  return new Response(null, { status: 404 });
}
```
Sources: [packages/preview/src/pages/_api/img.ts:48-66](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/_api/img.ts#L48-L66)

## Related

- [Local Markdown Runtime](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/content-sources/local-markdown-runtime)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/llms.txt) for all pages in this wiki.
