Next.js and React
In a Next.js app, or any React app with a server, ondocs is three pieces: an index built by the CLI, a route handler made with createAskHandler, and the <AskDialog /> component. The ondocs website is built exactly this way.
Install for Next.js
Section titled “Install for Next.js”npm i ondocs ai @ai-sdk/openai @radix-ui/react-dialog cmdk@radix-ui/react-dialog and cmdk work with React 18.3 and 19.
Index your content
Section titled “Index your content”Build the index from the folder that holds your pages, and commit ask-index.json. Running it before every build keeps it current, and only changed chunks are embedded again:
{ "scripts": { "index": "ondocs index content --base-url /docs -e openai:text-embedding-3-small --dimensions 512 --llms-txt public", "prebuild": "npm run index" }}--base-url /docs makes content/setup.md cite /docs/setup. The CLI detects a Next.js site and leaves (group) folders out of URLs. Fumadocs and Nextra have guides of their own: Fumadocs and Nextra.
Citations link to those URLs, so the app must serve the pages there. A new create-next-app project has no content/ folder and no /docs route: add your pages and a route under app/docs/ that renders them, or set --base-url to where your pages already are.
--llms-txt public also writes llms.txt, llms-full.txt and a Markdown copy of each page into public/, which Next.js serves from the site’s root: /docs/setup.md for /docs/setup. See llms.txt and Markdown pages.
Mount the route handler
Section titled “Mount the route handler”To add the endpoint to Next.js, export the handler as POST from a route handler. It is a plain (request: Request) => Promise<Response> function:
import { openai } from '@ai-sdk/openai';import { createAskHandler, memoryRateLimit } from 'ondocs/server';import index from '../../../ask-index.json';
export const POST = createAskHandler({ index, model: openai('gpt-5.4-mini'), embeddingModel: openai.embedding('text-embedding-3-small'), siteName: 'Acme Docs', rateLimit: memoryRateLimit({ limit: 10, windowMs: 60_000 }),});Importing the JSON bundles the index with the function, and it is loaded into memory once per server instance. Set OPENAI_API_KEY in your deployment’s environment.
Serve agents over MCP
Section titled “Serve agents over MCP”The same index answers coding agents from a second route, with createMcpHandler. Without an embeddingModel, it calls no model:
import { createMcpHandler } from 'ondocs/server';import index from '../../../ask-index.json';
const handler = createMcpHandler({ index, siteName: 'Acme Docs' });
export const POST = handler;export const GET = handler;export const DELETE = handler;export const OPTIONS = handler;Agents connect to /api/mcp, as in claude mcp add --transport http acme-docs https://docs.acme.dev/api/mcp. See MCP server.
Render the dialog
Section titled “Render the dialog”Render the dialog once, near the root, from a client component. onNavigate sends citations through the Next.js router instead of a full page load:
'use client';import { AskDialog } from 'ondocs/react';import 'ondocs/react/styles.css';import 'ondocs/embed/launcher.css';import { useRouter } from 'next/navigation';
export function Ask() { const router = useRouter(); return ( <AskDialog launcher suggestions={['How do I install it?', 'Which runtimes are supported?']} onNavigate={(url, event) => { event.preventDefault(); router.push(url); }} /> );}Then render <Ask /> in app/layout.tsx. launcher shows a floating “Ask AI” button; the dialog also opens with ⌘I or Ctrl+I.
Open it from your own button
Section titled “Open it from your own button”Leave launcher out and pass trigger to open the dialog from an element of your own, such as a search box in your header: <AskDialog trigger={<button type="button">Ask the docs</button>} />. Change the shortcut with shortcut="k", for ⌘K on a site whose only search is this dialog, or turn it off with shortcut={false}.
Other React frameworks and runtimes
Section titled “Other React frameworks and runtimes”createAskHandler returns a Web-standard handler, so it mounts anywhere that speaks Request and Response: app.post('/api/ask', (c) => handler(c.req.raw)) in Hono, Bun.serve({ fetch: handler }), or export default { fetch: handler } in a Cloudflare Worker. React Router, Remix, SvelteKit and Astro endpoints also receive a Request, which you pass to the handler as it is. The dialog only needs React and the endpoint’s URL.
Build your own interface
Section titled “Build your own interface”useAsk() from ondocs/react is the hook behind the dialog. It returns ask, stop and reset, plus the streaming answer, its sources and a status, so you can render answers inline on a page instead of in a dialog. AskAnswer renders an answer with clickable citations. See The ask dialog.
Keep the index fresh in CI
Section titled “Keep the index fresh in CI”Add the check to CI so a pull request that changes content without rebuilding the index fails:
npx ondocs index content --base-url /docs --check # exit 1 if staleThe check never calls a model and needs no API key.