-
Notifications
You must be signed in to change notification settings - Fork 9
Migrate docs from Mintlify to fumadocs #457
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
6ed79ee
9f94fda
4d67246
2a8ff17
a5659c1
2219ac8
9ba98c9
a1c1731
d21485a
3b528d5
d0371d9
3422937
175b328
f97c250
3a16eee
87fa555
72441ee
b51b764
95c5171
79ea2cc
b9da61d
240ffc7
0ddfd42
e74151f
23385a9
3fe509e
2971558
1a81b29
24a171b
8ab7681
5b9f13d
8ba6a1a
4ccfdcc
1d3f4a4
6bfb91e
8f795ac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| 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}> | ||
|
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> | ||
| ); | ||
|
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, | ||
| }, | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| }; | ||
| } | ||
| 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 }; | ||
| } |
| 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> | ||
| ); | ||
| } |
| 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; | ||
| }), | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| ); | ||
|
|
||
| for (const doc of docs) { | ||
| if (doc) search.add(doc); | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| return search; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Chat rebuilds search every cold startMedium Severity Each serverless instance starts 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(); | ||
|
cursor[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| const result = streamText({ | ||
| model: anthropic(process.env.ANTHROPIC_MODEL ?? "claude-sonnet-5"), | ||
| 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 }), | ||
| }); | ||
|
cursor[bot] marked this conversation as resolved.
Outdated
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; | ||
| 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", | ||
| }); |
| 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; | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| 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" }, | ||
| }; | ||
|
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> | ||
| ); | ||
| } | ||
| 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")); | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| } | ||


Uh oh!
There was an error while loading. Please reload this page.