mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-07 23:03:43 +08:00
feat(docs): add documentation site and demo features
- Introduced a new documentation site for Cursor BYOK using Next.js and Fumadocs. - Added a product demo page with a corresponding Vite configuration. - Implemented a demo API to simulate LLM calls and responses. - Enhanced the Makefile to include new build and development commands for the documentation. - Updated package.json scripts for building and running the documentation site. - Created various components and layouts for the documentation structure, including blog and user documentation sections. - Added styling for the new components and layouts to ensure a cohesive design. - Included a README and other necessary files for local development and deployment.
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import Link from 'next/link';
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { InlineTOC } from 'fumadocs-ui/components/inline-toc';
|
||||
import { createRelativeLink } from 'fumadocs-ui/mdx';
|
||||
import { getMDXComponents } from '@/components/mdx';
|
||||
import { blogSource, formatBlogDate } from '@/lib/blog';
|
||||
|
||||
export default async function BlogPostPage(props: PageProps<'/blog/[slug]'>) {
|
||||
const { slug } = await props.params;
|
||||
const page = blogSource.getPage([slug]);
|
||||
if (!page) notFound();
|
||||
|
||||
const MDX = page.data.body;
|
||||
|
||||
return (
|
||||
<main className="mx-auto w-full max-w-3xl px-6 py-12 sm:py-20">
|
||||
<article>
|
||||
<Link
|
||||
href="/blog"
|
||||
className="mb-10 inline-flex items-center gap-2 text-sm text-fd-muted-foreground transition-colors hover:text-fd-foreground"
|
||||
>
|
||||
<ArrowLeft className="size-4" />
|
||||
返回开发者博客
|
||||
</Link>
|
||||
|
||||
<header className="border-b pb-8">
|
||||
<time className="text-sm text-fd-muted-foreground">{formatBlogDate(page.path)}</time>
|
||||
<h1 className="mt-4 text-3xl font-bold tracking-tight sm:text-4xl">{page.data.title}</h1>
|
||||
<p className="mt-4 text-lg leading-8 text-fd-muted-foreground">{page.data.description}</p>
|
||||
<p className="mt-5 text-sm font-medium">cursor-byok 开发团队</p>
|
||||
</header>
|
||||
|
||||
<div className="prose mt-10 min-w-0">
|
||||
<InlineTOC items={page.data.toc} />
|
||||
<MDX
|
||||
components={getMDXComponents({
|
||||
a: createRelativeLink(blogSource, page),
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return blogSource.getPages().map((page) => ({ slug: page.slugs[0] }));
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: PageProps<'/blog/[slug]'>): Promise<Metadata> {
|
||||
const { slug } = await props.params;
|
||||
const page = blogSource.getPage([slug]);
|
||||
if (!page) notFound();
|
||||
|
||||
return {
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import Link from 'next/link';
|
||||
import type { Metadata } from 'next';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { blogSource, formatBlogDate, sortBlogPages } from '@/lib/blog';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '开发者博客',
|
||||
description: 'cursor-byok 的架构决策、协议实现与开发进展。',
|
||||
};
|
||||
|
||||
export default function BlogPage() {
|
||||
const posts = sortBlogPages(blogSource.getPages());
|
||||
|
||||
return (
|
||||
<main className="mx-auto w-full max-w-5xl px-6 py-16 sm:py-24">
|
||||
<div className="max-w-2xl">
|
||||
<p className="font-mono text-sm font-medium text-fd-primary">DEVELOPER BLOG</p>
|
||||
<h1 className="mt-4 text-4xl font-bold tracking-tight">开发者博客</h1>
|
||||
<p className="mt-4 text-lg leading-8 text-fd-muted-foreground">
|
||||
记录 cursor-byok 的架构决策、协议实现和开发进展。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 divide-y border-y">
|
||||
{posts.map((post) => (
|
||||
<Link
|
||||
key={post.url}
|
||||
href={post.url}
|
||||
className="group grid gap-3 py-7 transition-colors hover:text-fd-primary sm:grid-cols-[10rem_1fr_auto] sm:items-center"
|
||||
>
|
||||
<time className="text-sm text-fd-muted-foreground">{formatBlogDate(post.path)}</time>
|
||||
<div>
|
||||
<h2 className="font-semibold">{post.data.title}</h2>
|
||||
<p className="mt-1 text-sm leading-6 text-fd-muted-foreground">
|
||||
{post.data.description}
|
||||
</p>
|
||||
</div>
|
||||
<ArrowRight className="hidden size-4 transition-transform group-hover:translate-x-1 sm:block" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
|
||||
export default function Layout({ children }: LayoutProps<'/'>) {
|
||||
return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
ArrowRight,
|
||||
BookOpen,
|
||||
Download,
|
||||
Settings2,
|
||||
Wrench,
|
||||
} from 'lucide-react';
|
||||
import { DesktopDemo } from '@/components/hero/DesktopDemo';
|
||||
import { blogSource, formatBlogDate, sortBlogPages } from '@/lib/blog';
|
||||
import { appDescription, releaseUrl } from '@/lib/shared';
|
||||
|
||||
const docs = [
|
||||
{
|
||||
icon: BookOpen,
|
||||
title: '快速开始',
|
||||
description: '完成安装、初始化和第一次模型调用。',
|
||||
href: '/docs',
|
||||
},
|
||||
{
|
||||
icon: Settings2,
|
||||
title: '模型配置',
|
||||
description: '配置协议、服务地址、凭据和生成参数。',
|
||||
href: '/docs/model-configuration',
|
||||
},
|
||||
{
|
||||
icon: Wrench,
|
||||
title: '故障排查',
|
||||
description: '解决证书、连接和模型测试问题。',
|
||||
href: '/docs/troubleshooting',
|
||||
},
|
||||
];
|
||||
|
||||
export default function HomePage() {
|
||||
const posts = sortBlogPages(blogSource.getPages()).slice(0, 3);
|
||||
|
||||
return (
|
||||
<main className="flex flex-1 flex-col">
|
||||
<section className="border-b px-4 pb-16 pt-20 sm:px-6 sm:pb-24 sm:pt-28">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="mx-auto max-w-3xl text-center">
|
||||
<p className="font-mono text-sm font-medium text-fd-primary">开源 · 本地运行 · 自由接入</p>
|
||||
<h1 className="mt-5 text-4xl font-bold tracking-tight sm:text-6xl">
|
||||
{appDescription}
|
||||
</h1>
|
||||
<p className="mx-auto mt-6 max-w-2xl text-lg leading-8 text-fd-muted-foreground">
|
||||
在本机运行自己的模型网关,接入 OpenAI、Anthropic 等兼容服务,继续使用 Cursor Agent 的工具调用、Skills 和 MCP。
|
||||
</p>
|
||||
<div className="mt-10 flex flex-wrap justify-center gap-3">
|
||||
<Link
|
||||
href="/docs"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-fd-primary px-5 py-3 font-medium text-fd-primary-foreground transition-opacity hover:opacity-90"
|
||||
>
|
||||
阅读文档
|
||||
<ArrowRight className="size-4" />
|
||||
</Link>
|
||||
<a
|
||||
href={releaseUrl}
|
||||
className="inline-flex items-center gap-2 rounded-lg border bg-fd-card px-5 py-3 font-medium transition-colors hover:bg-fd-accent"
|
||||
>
|
||||
<Download className="size-4" />
|
||||
下载最新版
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<DesktopDemo />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border-b px-6 py-16 sm:py-20">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<div className="flex items-end justify-between gap-6">
|
||||
<div>
|
||||
<p className="font-mono text-sm font-medium text-fd-primary">DOCUMENTATION</p>
|
||||
<h2 className="mt-3 text-3xl font-bold tracking-tight">文档</h2>
|
||||
</div>
|
||||
<Link href="/docs" className="hidden items-center gap-2 text-sm font-medium sm:flex">
|
||||
查看全部
|
||||
<ArrowRight className="size-4" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 grid gap-4 md:grid-cols-3">
|
||||
{docs.map(({ icon: Icon, title, description, href }) => (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
className="group rounded-xl border bg-fd-card p-6 transition-colors hover:bg-fd-accent"
|
||||
>
|
||||
<Icon className="mb-5 size-5 text-fd-primary" />
|
||||
<h3 className="flex items-center justify-between font-semibold">
|
||||
{title}
|
||||
<ArrowRight className="size-4 transition-transform group-hover:translate-x-1" />
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-6 text-fd-muted-foreground">{description}</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="px-6 py-16 sm:py-20">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<div className="flex items-end justify-between gap-6">
|
||||
<div>
|
||||
<p className="font-mono text-sm font-medium text-fd-primary">DEVELOPER BLOG</p>
|
||||
<h2 className="mt-3 text-3xl font-bold tracking-tight">开发者博客</h2>
|
||||
</div>
|
||||
<Link href="/blog" className="flex items-center gap-2 text-sm font-medium">
|
||||
查看全部
|
||||
<ArrowRight className="size-4" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 divide-y border-y">
|
||||
{posts.map((post) => (
|
||||
<Link
|
||||
key={post.url}
|
||||
href={post.url}
|
||||
className="group grid gap-3 py-6 transition-colors hover:text-fd-primary sm:grid-cols-[10rem_1fr_auto] sm:items-center"
|
||||
>
|
||||
<time className="text-sm text-fd-muted-foreground">{formatBlogDate(post.path)}</time>
|
||||
<div>
|
||||
<h3 className="font-semibold">{post.data.title}</h3>
|
||||
<p className="mt-1 text-sm text-fd-muted-foreground">{post.data.description}</p>
|
||||
</div>
|
||||
<ArrowRight className="hidden size-4 transition-transform group-hover:translate-x-1 sm:block" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { source } from '@/lib/source';
|
||||
import { createFromSource } from 'fumadocs-core/search/server';
|
||||
|
||||
export const { GET } = createFromSource(source);
|
||||
@@ -0,0 +1,63 @@
|
||||
import { getPageImageUrl, getPageMarkdownUrl, source } from '@/lib/source';
|
||||
import {
|
||||
DocsBody,
|
||||
DocsDescription,
|
||||
DocsPage,
|
||||
DocsTitle,
|
||||
MarkdownCopyButton,
|
||||
ViewOptionsPopover,
|
||||
} from 'fumadocs-ui/layouts/docs/page';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { getMDXComponents } from '@/components/mdx';
|
||||
import type { Metadata } from 'next';
|
||||
import { createRelativeLink } from 'fumadocs-ui/mdx';
|
||||
import { gitConfig } from '@/lib/shared';
|
||||
|
||||
export default async function Page(props: PageProps<'/docs/[[...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}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return source.generateParams();
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: PageProps<'/docs/[[...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: getPageImageUrl(page).url,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { source } from '@/lib/source';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
|
||||
export default function Layout({ children }: LayoutProps<'/docs'>) {
|
||||
return (
|
||||
<DocsLayout tree={source.getPageTree()} {...baseOptions()}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'fumadocs-ui/css/neutral.css';
|
||||
@import 'fumadocs-ui/css/preset.css';
|
||||
|
||||
:root {
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
html > body[data-scroll-locked] {
|
||||
margin-right: 0 !important;
|
||||
--removed-body-scroll-bar-size: 0px !important;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { RootProvider } from 'fumadocs-ui/provider/next';
|
||||
import { i18nProvider } from 'fumadocs-ui/i18n';
|
||||
import type { Metadata } from 'next';
|
||||
import { translations } from '@/lib/layout.shared';
|
||||
import './global.css';
|
||||
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://docs.leokun.cn';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(siteUrl),
|
||||
title: {
|
||||
default: 'cursor-byok 文档',
|
||||
template: '%s | cursor-byok',
|
||||
},
|
||||
description: 'cursor-byok 的安装、模型配置与故障排查指南。',
|
||||
};
|
||||
|
||||
export default function Layout({ children }: LayoutProps<'/'>) {
|
||||
return (
|
||||
<html lang="zh-CN" suppressHydrationWarning>
|
||||
<body className="flex min-h-screen flex-col">
|
||||
<RootProvider i18n={i18nProvider(translations)}>{children}</RootProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { getLLMText, getPageMarkdownUrl, 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?.slice(0, -1));
|
||||
if (!page) notFound();
|
||||
|
||||
return new Response(await getLLMText(page), {
|
||||
headers: {
|
||||
'Content-Type': 'text/markdown',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return source.getPages().map((page) => ({
|
||||
lang: page.locale,
|
||||
slug: getPageMarkdownUrl(page).segments,
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { source } from '@/lib/source';
|
||||
import { llms } from 'fumadocs-core/source';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
export function GET() {
|
||||
return new Response(llms(source).index());
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { getPageImageUrl, source } from '@/lib/source';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { generate as DefaultImage } from 'fumadocs-ui/og';
|
||||
import { appName } from '@/lib/shared';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...slug]'>) {
|
||||
const { slug } = await params;
|
||||
const page = source.getPage(slug.slice(0, -1));
|
||||
if (!page) notFound();
|
||||
|
||||
return new ImageResponse(
|
||||
<DefaultImage title={page.data.title} description={page.data.description} site={appName} />,
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return source.getPages().map((page) => ({
|
||||
lang: page.locale,
|
||||
slug: getPageImageUrl(page).segments,
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user