Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
6ed79ee
Migrate docs from Mintlify to fumadocs (Next.js, self-hosted)
masnwilliams Jul 21, 2026
9f94fda
Pin Vercel framework preset to Next.js
masnwilliams Jul 22, 2026
4d67246
Default docs assistant to Haiku
masnwilliams Jul 22, 2026
2a8ff17
Address Bugbot review findings
masnwilliams Jul 22, 2026
a5659c1
Match production docs look: notebook layout, logo wordmark
masnwilliams Jul 22, 2026
2219ac8
Sidebar links: text-only hover/active states like navbar links
masnwilliams Jul 22, 2026
9ba98c9
Sidebar folder toggles: same text-only hover as page links
masnwilliams Jul 22, 2026
a1c1731
Pointer cursor on sidebar folder toggles
masnwilliams Jul 22, 2026
d21485a
Match Mintlify homepage and page chrome; disable overscroll
masnwilliams Jul 22, 2026
3b528d5
Group API reference endpoints by tag, like prod
masnwilliams Jul 22, 2026
d0371d9
Render title and description on API reference pages
masnwilliams Jul 22, 2026
3422937
Render API responses with a status-code dropdown
masnwilliams Jul 22, 2026
175b328
Fix AI chat: pass system prompt via instructions option
masnwilliams Jul 22, 2026
f97c250
API sidebar method badges + plain status dropdown
masnwilliams Jul 22, 2026
3a16eee
Drop role labels in AI chat; user turn as a bubble
masnwilliams Jul 22, 2026
87fa555
Address bugbot findings: code tabs, input casing, open-in URL
masnwilliams Jul 22, 2026
72441ee
Harden /api/chat: BotID + rate limiting
masnwilliams Jul 22, 2026
b51b764
BotID Deep Analysis + Vercel Firewall rate limiting
masnwilliams Jul 22, 2026
95c5171
Add Braintrust chat telemetry + input-size guard
masnwilliams Jul 22, 2026
79ea2cc
Group chat turns into one Braintrust trace per conversation
masnwilliams Jul 22, 2026
b9da61d
Tag chat retries in Braintrust
masnwilliams Jul 22, 2026
240ffc7
Enable fumadocs ImageZoom for content images
masnwilliams Jul 22, 2026
0ddfd42
Raise chat input-size cap so real conversations aren't rejected
masnwilliams Jul 22, 2026
e74151f
Soften API method badges to subtle tints
masnwilliams Jul 22, 2026
23385a9
Fix bugbot findings across the docs app
masnwilliams Jul 22, 2026
3fe509e
Merge origin/main into hypeship/fumadocs-migration
masnwilliams Jul 22, 2026
2971558
Address bugbot: robust chat size cap + skip external stubs in index
masnwilliams Jul 23, 2026
1a81b29
Merge origin/main into hypeship/fumadocs-migration
masnwilliams Jul 23, 2026
24a171b
Merge origin/main into hypeship/fumadocs-migration
masnwilliams Jul 25, 2026
8ab7681
Add SEO parity: sitemap, robots, canonical + Open Graph metadata
masnwilliams Jul 25, 2026
5b9f13d
Include shared OG fields per page (Next replaces openGraph per segment)
masnwilliams Jul 25, 2026
8ba6a1a
Generate per-page OG images for API-reference pages
masnwilliams Jul 25, 2026
4ccfdcc
Address bugbot: 400 on bad chat JSON; skip external stubs in llms end…
masnwilliams Jul 25, 2026
1d3f4a4
Merge origin/main into fumadocs-migration
masnwilliams Jul 30, 2026
6bfb91e
docs chat: route through Vercel AI Gateway on gpt-5.6-luna
masnwilliams Jul 30, 2026
8f795ac
Merge origin/main into fumadocs-migration
masnwilliams Aug 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
*/.DS_Store
# deps
/node_modules

# generated content
.source

# test & build
/coverage
/.next/
/out/
/build
*.tsbuildinfo

# misc
.DS_Store
.env
*.pem
/.pnp
.pnp.js
npm-debug.log*
yarn-debug.log*
yarn-error.log*

node_modules/
venv
__pycache__/**
test.py
./kernel/**
# others
.env*.local
.vercel
next-env.d.ts
Comment thread
cursor[bot] marked this conversation as resolved.
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@

## Cursor Cloud specific instructions

This is a Mintlify documentation site (no `package.json`). The only service is the docs preview server.
This is a fumadocs (Next.js) documentation site. Content lives in `content/docs/` as MDX.

- **Run the docs site:** `mintlify dev` (serves on `http://localhost:3000` by default; auto-increments if the port is busy)
- **Lint/build:** Mintlify handles rendering; there's no separate lint or build step. Validate by running `mintlify dev` and checking pages load.
- **Config:** `docs.json` is the main Mintlify config (navigation, theme, OpenAPI spec URL, redirects).
- **Run the docs site:** `bun dev` (serves on `http://localhost:3000`)
- **Lint/build:** `bun run build` compiles every page; `bun run lint` runs Biome. Validate changes with a build.
- **Config:** `docs.json` remains the navigation source of truth (tabs/groups/redirects); `lib/tree.ts` converts it to the sidebar at build time. Mintlify components (`<Info>`, `<CodeGroup>`, `<Steps>`, ...) still work — they're aliased in `components/mdx.tsx`.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ When you add Go examples:
To run the docs locally, you can use the following command:

```bash
mintlify dev
bun install
bun dev
```

## Contributing
Expand Down
67 changes: 67 additions & 0 deletions app/(docs)/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
DocsBody,
DocsDescription,
DocsPage,
DocsTitle,
MarkdownCopyButton,
ViewOptionsPopover,
} from "fumadocs-ui/layouts/docs/page";
import { createRelativeLink } from "fumadocs-ui/mdx";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getMDXComponents } from "@/components/mdx";
import { gitConfig } from "@/lib/shared";
import { getPageImage, getPageMarkdownUrl, source } from "@/lib/source";

export default async function Page(props: PageProps<"/[[...slug]]">) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();

const MDX = page.data.body;
const markdownUrl = getPageMarkdownUrl(page).url;

return (
<DocsPage toc={page.data.toc} full={page.data.full}>
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription className="mb-0">
{page.data.description}
</DocsDescription>
<div className="flex flex-row gap-2 items-center border-b pb-6">
<MarkdownCopyButton markdownUrl={markdownUrl} />
<ViewOptionsPopover
markdownUrl={markdownUrl}
githubUrl={`https://github.com/${gitConfig.user}/${gitConfig.repo}/blob/${gitConfig.branch}/content/docs/${page.path}`}
/>
</div>
<DocsBody>
<MDX
components={getMDXComponents({
// this allows you to link to other pages with relative file paths
a: createRelativeLink(source, page),
})}
/>
</DocsBody>
</DocsPage>
);
Comment thread
cursor[bot] marked this conversation as resolved.
}

export async function generateStaticParams() {
return source.generateParams();
}

export async function generateMetadata(
props: PageProps<"/[[...slug]]">,
): Promise<Metadata> {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();

return {
title: page.data.title,
description: page.data.description,
openGraph: {
images: getPageImage(page).url,
},
Comment thread
cursor[bot] marked this conversation as resolved.
};
}
35 changes: 35 additions & 0 deletions app/(docs)/api-reference/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { DocsBody, DocsPage } from "fumadocs-ui/layouts/docs/page";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { OpenAPIPage } from "@/components/api-page";
import { apiSource } from "@/lib/source";

export default async function Page(
props: PageProps<"/api-reference/[[...slug]]">,
) {
const params = await props.params;
const page = apiSource.getPage(params.slug);
if (!page) notFound();

return (
<DocsPage toc={page.data.toc}>
<DocsBody>
<OpenAPIPage {...page.data.getOpenAPIPageProps()} />
</DocsBody>
</DocsPage>
);
}

export function generateStaticParams() {
return apiSource.generateParams();
}

export async function generateMetadata(
props: PageProps<"/api-reference/[[...slug]]">,
): Promise<Metadata> {
const params = await props.params;
const page = apiSource.getPage(params.slug);
if (!page) notFound();

return { title: page.data.title, description: page.data.description };
}
35 changes: 35 additions & 0 deletions app/(docs)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { buttonVariants } from "fumadocs-ui/components/ui/button";
import { DocsLayout } from "fumadocs-ui/layouts/docs";
import { MessageCircleIcon } from "lucide-react";
import {
AISearch,
AISearchPanel,
AISearchTrigger,
} from "@/components/ai/search";
import { cn } from "@/lib/cn";
import { baseOptions } from "@/lib/layout.shared";
import { buildTree } from "@/lib/tree";

export default function Layout({ children }: LayoutProps<"/">) {
return (
<DocsLayout tree={buildTree()} {...baseOptions()}>
<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>
);
}
116 changes: 116 additions & 0 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { createAnthropic } from "@ai-sdk/anthropic";
import {
convertToModelMessages,
createUIMessageStreamResponse,
stepCountIs,
streamText,
tool,
toUIMessageStream,
} from "ai";
import { Document, type DocumentData } from "flexsearch";
import { z } from "zod";
import { source } from "@/lib/source";
import type { ChatUIMessage, SearchTool } from "../../../components/ai/search";

interface CustomDocument extends DocumentData {
url: string;
title: string;
description: string;
content: string;
}
const searchServer = createSearchServer();

async function createSearchServer() {
const search = new Document<CustomDocument>({
document: {
id: "url",
index: ["title", "description", "content"],
store: true,
},
});

const docs = await chunkedAll(
source.getPages().map(async (page) => {
if (!("getText" in page.data)) return null;

return {
title: page.data.title,
description: page.data.description,
url: page.url,
content: await page.data.getText("processed"),
} as CustomDocument;
}),
Comment thread
cursor[bot] marked this conversation as resolved.
);

for (const doc of docs) {
if (doc) search.add(doc);
}
Comment thread
cursor[bot] marked this conversation as resolved.

return search;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chat rebuilds search every cold start

Medium Severity

Each serverless instance starts createSearchServer() at module load and awaits getText("processed") for every docs page before flexsearch is usable. First Ask AI requests on a cold worker can block on hundreds of page extractions with no shared or build-time index.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 240ffc7. Configure here.

}

async function chunkedAll<O>(promises: Promise<O>[]): Promise<O[]> {
const SIZE = 50;
const out: O[] = [];
for (let i = 0; i < promises.length; i += SIZE) {
out.push(...(await Promise.all(promises.slice(i, i + SIZE))));
}
return out;
}

const anthropic = createAnthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});

/** System prompt, you can update it to provide more specific information */
const systemPrompt = [
"You are an AI assistant for the Kernel documentation site (Kernel provides sandboxed cloud browsers for automations and web agents).",
"Use the `search` tool to retrieve relevant docs context before answering when needed.",
"The `search` tool returns raw JSON results from documentation. Use those results to ground your answer and cite sources as markdown links using the document `url` field when available.",
"If you cannot find the answer in search results, say you do not know and suggest a better search query.",
].join("\n");

export async function POST(req: Request, _ctx: RouteContext<"/api/chat">) {
const reqJson = await req.json();
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

const result = streamText({
model: anthropic(process.env.ANTHROPIC_MODEL ?? "claude-haiku-4-5-20251001"),
stopWhen: stepCountIs(5),
tools: {
search: searchTool,
},
messages: [
{ role: "system", content: systemPrompt },
...(await convertToModelMessages<ChatUIMessage>(reqJson.messages ?? [], {
convertDataPart(part) {
if (part.type === "data-client")
return {
type: "text",
text: `[Client Context: ${JSON.stringify(part.data)}]`,
};
},
})),
],
toolChoice: "auto",
});

return createUIMessageStreamResponse({
stream: toUIMessageStream({ stream: result.stream }),
});
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
}

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 search = await searchServer;
return await search.searchAsync(query, {
limit,
merge: true,
enrich: true,
});
},
}) satisfies SearchTool;
7 changes: 7 additions & 0 deletions app/api/search/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createFromSource } from "fumadocs-core/search/server";
import { source } from "@/lib/source";

export const { GET } = createFromSource(source, {
// https://docs.orama.com/docs/orama-js/supported-languages
language: "english",
});
23 changes: 23 additions & 0 deletions app/global.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@import "tailwindcss";
@import "fumadocs-ui/css/neutral.css";
@import "fumadocs-ui/css/preset.css";

/* Kernel brand (ported from Mintlify docs.json colors) */
:root {
--color-fd-background: hsl(240 10% 95%); /* #F0F0F3 */
--color-fd-primary: hsl(228 6% 14%); /* #212225 */
}

.dark {
--color-fd-background: hsl(228 6% 14%); /* #212225 */
--color-fd-primary: hsl(48 47% 60%); /* #CAB168 */
}

html {
scrollbar-gutter: stable;
}

html > body[data-scroll-locked] {
margin-right: 0px;
--removed-body-scroll-bar-size: 0px;
}
Comment thread
cursor[bot] marked this conversation as resolved.
37 changes: 37 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { RootProvider } from "fumadocs-ui/provider/next";
import "./global.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import Script from "next/script";

const inter = Inter({
subsets: ["latin"],
});

export const metadata: Metadata = {
title: {
template: "%s - Kernel",
default: "Kernel Documentation",
},
icons: { icon: "/favicon.svg" },
};
Comment thread
cursor[bot] marked this conversation as resolved.

export default function Layout({ children }: LayoutProps<"/">) {
return (
<html lang="en" className={inter.className} suppressHydrationWarning>
<body className="flex flex-col min-h-screen">
<RootProvider>{children}</RootProvider>
<Script
src="https://www.googletagmanager.com/gtag/js?id=G-GVEXRLYSN4"
strategy="afterInteractive"
/>
<Script id="ga4" strategy="afterInteractive">
{`window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-GVEXRLYSN4');`}
</Script>
</body>
</html>
);
}
10 changes: 10 additions & 0 deletions app/llms-full.txt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { getLLMText, source } from "@/lib/source";

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"));
Comment thread
cursor[bot] marked this conversation as resolved.
}
Loading
Loading