Skip to main content
doc0blade47/fumadocsdocumentation home
Sign in

Preview Server

Last updated Oct 12, 2026100% grounded
Relevant source files

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

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, packages/preview/src/waku.server.tsx:1-38, packages/preview/src/pages/...slugs.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/...slugs/image.webp.tsx:7-21

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. When executed, runStart loads environment variables and overrides NODE_ENV to production packages/preview/src/cli/commands.ts:16-17.

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

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. 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.

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

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. 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, packages/preview/src/lib/source/watcher.ts:29-48.

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

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. 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.

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

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. 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.

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

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

Event TypeCache ActionSource RevalidationWebSocket Payload Sent
changeDeletes entry from filesCache if presentTriggers getSource.revalidate(false)Clears cache path, then sends revalidate
unlinkBypasses cache eviction if unparsedTriggers getSource.revalidate(false)Sends revalidate
addNoneTriggers getSource.revalidate(false)Sends revalidate

Sources: packages/preview/src/waku.server.tsx:66-92

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.

Sources: packages/preview/src/waku.server.tsx:70-72

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. 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.

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

Upon receiving incoming messages, the browser runtime decodes the event payload using decodeEvent packages/preview/src/components/hot-reload.tsx:13-14. 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.

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. 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. 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.

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

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.

Sources: packages/preview/src/lib/source/index.ts:7

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. 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. 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. When HOT_RELOAD === '1', the HotReload component is additionally injected into the document body packages/preview/src/pages/_root.tsx:13. 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]].

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

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. 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.

Layout PresetLayout Import PathPage Component Import Path
docsfumadocs-ui/layouts/docsfumadocs-ui/layouts/docs/page
Other (Flux)fumadocs-ui/layouts/fluxfumadocs-ui/layouts/flux/page

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

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.

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

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

Plugin CategoryPlugin NameConfiguration Options / Behavior
RemarkremarkGfmGitHub Flavored Markdown support
RemarkremarkMathMathematical notation parsing
RemarkremarkMdxMermaidMermaid diagram embedding in MDX
RemarkremarkHeadingHeading processing (generateToc: false)
RemarkremarkNpmPackage manager tab persistence (persist: { id: 'package-manager' })
RemarkremarkCodeTabCode block tab grouping
RehyperehypeKatexKaTeX math rendering
RehyperehypeCodeSyntax highlighting (lazy: true, fallbackLanguage: 'text')
RehyperehypeTocTable of contents extraction (exportToc: { as: 'data' })

Sources: packages/preview/src/pages/...slugs.tsx:30-45

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.

Sources: packages/preview/src/pages/...slugs.tsx:170-194

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. It relies on a cached server instance created through the revalidable wrapper packages/preview/src/pages/_api/api/search.ts:7-21. The server compiles the content source using flexsearchFromSource, building index entries for every page packages/preview/src/pages/_api/api/search.ts:8-19.

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

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. 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

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. 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.

Rate Limit Setting / VariableDefault Value / BehaviorSource Reference
rateLimitWindowMs60 * 1000 (60 seconds)packages/preview/src/pages/_api/api/chat.ts:55
rateLimitMaxRequests20 requests per windowpackages/preview/src/pages/_api/api/chat.ts:56
IP Headers Checkedx-forwarded-for, cf-connecting-ip, x-real-ip, x-client-ippackages/preview/src/pages/_api/api/chat.ts:63-77

Sources: packages/preview/src/pages/_api/api/chat.ts:55-77, packages/preview/src/pages/_api/api/chat.ts:110-125

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, packages/preview/src/pages/_api/api/chat.ts:118-124

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.

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

Sources: packages/preview/src/lib/ai.ts:13-16

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.

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

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. 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.

Sources: packages/preview/src/pages/_api/api/chat.ts:46-51, packages/preview/src/pages/_api/api/chat.ts:129-141

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.

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

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. 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.

Sources: packages/preview/src/components/ai/search.tsx:390-417, packages/preview/src/components/ai/search.tsx:420-439

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. If no page exists, the endpoint returns a 404 status response packages/preview/src/pages/_api/og/%5B...slugs%5D/image.webp.tsx:11.

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

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. 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.

Sources: packages/base-ui/src/og/takumi.tsx:55-65

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.

export async function getConfig() {
  return {
    render: 'dynamic' as const,
  } as const;

Sources: packages/preview/src/pages/_api/og/...slugs/image.webp.tsx:23-26

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. 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. 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.

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

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. 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.

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

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.

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.

File ExtensionMIME Type
.pngimage/png
.jpgimage/jpeg
.jpegimage/jpeg
.webpimage/webp
.gifimage/gif
.svgimage/svg+xml
.avifimage/avif
.bmpimage/bmp
.icoimage/x-icon

Sources: packages/preview/src/pages/_api/img.ts:7-17

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. If all candidate paths fail, it returns a 404 status response packages/preview/src/pages/_api/img.ts:65.

  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

Was this page helpful?