AI & LLMs
Integrate AI functionality to Hanzo Docs.
Docs for LLM
You can make your docs site more AI-friendly with dedicated docs content for large language models.
To begin, make a getLLMText function that converts pages into static MDX content.
In Hanzo Docs MDX, you can do:
import { source } from '@/lib/source';
import type { InferPageType } from '@hanzo/docs/core/source';
export async function getLLMText(page: InferPageType<typeof source>) {
const processed = await page.data.getText('processed');
return `# ${page.data.title} (${page.url})
${processed}`;
}It requires includeProcessedMarkdown to be enabled:
import { defineDocs } from '@hanzo/docs/mdx/config';
export const docs = defineDocs({
docs: {
postprocess: {
includeProcessedMarkdown: true,
},
},
});llms-full.txt
A version of docs for AIs to read.
import { source } from '@/lib/source';
import { getLLMText } from '@/lib/get-llm-text';
// cached forever
export const revalidate = false;
export async function GET() {
const scan = source.getPages().map(getLLMText);
const scanned = await Promise.all(scan);
return new Response(scanned.join('\n\n'));
}import { index, route, type RouteConfig } from '@react-router/dev/routes';
export default [
route('llms-full.txt', 'routes/llms-full.ts'),
] satisfies RouteConfig;import { source } from '@/lib/source';
import { getLLMText } from '@/lib/get-llm-text';
export async function loader() {
const scan = source.getPages().map(getLLMText);
const scanned = await Promise.all(scan);
return new Response(scanned.join('\n\n'));
}import { createFileRoute } from '@tanstack/react-router';
import { source } from '@/lib/source';
import { getLLMText } from '@/lib/get-llm-text';
export const Route = createFileRoute('/llms-full.txt')({
server: {
handlers: {
GET: async () => {
const scan = source.getPages().map(getLLMText);
const scanned = await Promise.all(scan);
return new Response(scanned.join('\n\n'));
},
},
},
});*.mdx
Allow AI agents to get the content of a page as Markdown/MDX, by appending .mdx to the end of path.
Make a route handler to return page content, and a middleware to point to it:
import { getLLMText } from '@/lib/get-llm-text';
import { source } from '@/lib/source';
import { notFound } from 'next/navigation';
export const revalidate = false;
export async function GET(_req: Request, { params }: RouteContext<'/llms.mdx/docs/[[...slug]]'>) {
const { slug } = await params;
const page = source.getPage(slug);
if (!page) notFound();
return new Response(await getLLMText(page), {
headers: {
'Content-Type': 'text/markdown',
},
});
}
export function generateStaticParams() {
return source.generateParams();
}import type { NextConfig } from 'next';
const config: NextConfig = {
async rewrites() {
return [
{
source: '/docs/:path*.mdx',
destination: '/llms.mdx/docs/:path*',
},
];
},
};import { index, route, type RouteConfig } from '@react-router/dev/routes';
export default [
route('llms.mdx/docs/*', 'routes/docs/llms-mdx.ts'),
] satisfies RouteConfig;import type { Route } from './+types/llms-mdx';
import { source } from '@/lib/source';
import { getLLMText } from '@/lib/get-llm-text';
export async function loader({ params }: Route.LoaderArgs) {
const slugs = params['*'].split('/').filter((v) => v.length > 0);
const page = source.getPage(slugs);
if (!page) {
return new Response('not found', { status: 404 });
}
return new Response(await getLLMText(page), {
headers: {
'Content-Type': 'text/markdown',
},
});
}import { rewritePath } from '@hanzo/docs/core/negotiation';
import type { Route } from './+types/root';
const { rewrite: rewriteLLM } = rewritePath('/docs{/*path}.mdx', '/llms.mdx/docs{/*path}');
const serverMiddleware: Route.MiddlewareFunction = async ({ request }, next) => {
const url = new URL(request.url);
const path = rewriteLLM(url.pathname);
if (path) return Response.redirect(new URL(path, url));
return next();
};
export const middleware = [serverMiddleware];import { createFileRoute, notFound } from '@tanstack/react-router';
import { source } from '@/lib/source';
export const Route = createFileRoute('/llms.mdx/docs/$')({
server: {
handlers: {
GET: async ({ params }) => {
const slugs = params._splat?.split('/') ?? [];
const page = source.getPage(slugs);
if (!page) throw notFound();
return new Response(await page.data.getText('raw'), {
headers: {
'Content-Type': 'text/markdown',
},
});
},
},
},
});import { createMiddleware, createStart } from '@tanstack/react-start';
import { rewritePath } from '@hanzo/docs/core/negotiation';
import { redirect } from '@tanstack/react-router';
const { rewrite: rewriteLLM } = rewritePath('/docs{/*path}.mdx', 'llms.mdx/docs{/*path}');
const llmMiddleware = createMiddleware().server(({ next, request }) => {
const url = new URL(request.url);
const path = rewriteLLM(url.pathname);
if (path) {
throw redirect(new URL(path, url));
}
return next();
});
export const startInstance = createStart(() => {
return {
requestMiddleware: [llmMiddleware],
};
});Accept
To serve the Markdown content instead for AI agents, you can leverage the Accept header.
import { NextRequest, NextResponse } from 'next/server';
import { isMarkdownPreferred, rewritePath } from '@hanzo/docs/core/negotiation';
const { rewrite: rewriteLLM } = rewritePath('/docs{/*path}', '/llms.mdx/docs{/*path}');
export default function proxy(request: NextRequest) {
if (isMarkdownPreferred(request)) {
const result = rewriteLLM(request.nextUrl.pathname);
if (result) {
return NextResponse.rewrite(new URL(result, request.nextUrl));
}
}
return NextResponse.next();
}Page Actions
Common page actions for AI, require *.mdx to be implemented first.

npx @hanzo/docs-cli add ai/page-actionspnpm dlx @hanzo/docs-cli add ai/page-actionsyarn dlx @hanzo/docs-cli add ai/page-actionsbun x @hanzo/docs-cli add ai/page-actionsUse it in your docs page like:
<div className="flex flex-row gap-2 items-center border-b pt-2 pb-6">
<LLMCopyButton markdownUrl={`${page.url}.mdx`} />
<ViewOptions
markdownUrl={`${page.url}.mdx`}
githubUrl={`https://github.com/${owner}/${repo}/blob/dev/apps/docs/content/docs/${page.path}`}
/>
</div>Ask AI

You can install the AI search dialog using Hanzo Docs CLI:
npx @hanzo/docs-cli add ai/searchpnpm dlx @hanzo/docs-cli add ai/searchyarn dlx @hanzo/docs-cli add ai/searchbun x @hanzo/docs-cli add ai/searchBy default, it's configured for Inkeep AI using Vercel AI SDK.
To setup for Inkeep AI:
-
Add your Inkeep API key to environment variables:
INKEEP_API_KEY="..." -
Add the component & trigger to root layout (or anywhere you prefer):
import { AISearch, AISearchPanel, AISearchTrigger } from '@/components/ai/search'; import { MessageCircleIcon } from 'lucide-react'; import { buttonVariants } from '@/components/button'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <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} </body> </html> ); }
To use your own AI models, update the configurations in useChat and /api/chat route.
Note that Hanzo Docs doesn't provide the AI model, it's up to you.
Your AI model can use the llms-full.txt file generated above, or more diversified sources of information when combined with 3rd party solutions.
