feat(docs): enhance documentation site with internationalization and layout updates

- Updated the demo API to support locale-based content rendering.
- Refactored the main layout to accommodate language-specific metadata and routing.
- Introduced new components for blog and documentation pages, ensuring proper language handling.
- Added styles for improved UI consistency across different sections.
- Removed deprecated layout files and streamlined the structure for better maintainability.
This commit is contained in:
leookun
2026-08-28 00:35:31 +08:00
parent 6c758f2b44
commit eca063e424
55 changed files with 6700 additions and 407 deletions
-44
View File
@@ -1,44 +0,0 @@
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>
);
}
-6
View File
@@ -1,6 +0,0 @@
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { baseOptions } from '@/lib/layout.shared';
export default function Layout({ children }: LayoutProps<'/'>) {
return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
}
-135
View File
@@ -1,135 +0,0 @@
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>
);
}
@@ -6,30 +6,42 @@ 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';
import { i18n, isLanguage, type Language } from '@/lib/i18n';
export default async function BlogPostPage(props: PageProps<'/blog/[slug]'>) {
const { slug } = await props.params;
const page = blogSource.getPage([slug]);
const copy: Record<Language, { back: string; team: string }> = {
zh: { back: '返回开发者博客', team: 'cursor-byok 开发团队' },
en: { back: 'Back to Developer Blog', team: 'The cursor-byok team' },
};
export default async function BlogPostPage(props: PageProps<'/[lang]/blog/[slug]'>) {
const { lang, slug } = await props.params;
if (!isLanguage(lang)) notFound();
const page = blogSource.getPage([slug], lang);
if (!page) notFound();
const t = copy[lang];
const prefix = lang === i18n.defaultLanguage ? '' : `/${lang}`;
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"
href={`${prefix}/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" />
返回开发者博客
{t.back}
</Link>
<header className="border-b pb-8">
<time className="text-sm text-fd-muted-foreground">{formatBlogDate(page.path)}</time>
<time className="text-sm text-fd-muted-foreground">
{formatBlogDate(page.path, lang)}
</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>
<p className="mt-5 text-sm font-medium">{t.team}</p>
</header>
<div className="prose mt-10 min-w-0">
@@ -46,12 +58,18 @@ export default async function BlogPostPage(props: PageProps<'/blog/[slug]'>) {
}
export function generateStaticParams() {
return blogSource.getPages().map((page) => ({ slug: page.slugs[0] }));
return i18n.languages.flatMap((lang) =>
blogSource.getPages(lang).map((page) => ({ lang, slug: page.slugs[0] })),
);
}
export async function generateMetadata(props: PageProps<'/blog/[slug]'>): Promise<Metadata> {
const { slug } = await props.params;
const page = blogSource.getPage([slug]);
export async function generateMetadata(
props: PageProps<'/[lang]/blog/[slug]'>,
): Promise<Metadata> {
const { lang, slug } = await props.params;
if (!isLanguage(lang)) notFound();
const page = blogSource.getPage([slug], lang);
if (!page) notFound();
return {
+68
View File
@@ -0,0 +1,68 @@
import Link from 'next/link';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { ArrowRight } from 'lucide-react';
import { blogSource, formatBlogDate, sortBlogPages } from '@/lib/blog';
import { isLanguage, type Language } from '@/lib/i18n';
const copy: Record<Language, { title: string; description: string; intro: string }> = {
zh: {
title: '开发者博客',
description: 'cursor-byok 的架构决策、协议实现与开发进展。',
intro: '记录 cursor-byok 的架构决策、协议实现和开发进展。',
},
en: {
title: 'Developer Blog',
description: 'Architecture decisions, protocol work, and development progress of cursor-byok.',
intro: 'Notes on architecture decisions, protocol work, and development progress of cursor-byok.',
},
};
export async function generateMetadata(props: PageProps<'/[lang]/blog'>): Promise<Metadata> {
const { lang } = await props.params;
if (!isLanguage(lang)) notFound();
return {
title: copy[lang].title,
description: copy[lang].description,
};
}
export default async function BlogPage(props: PageProps<'/[lang]/blog'>) {
const { lang } = await props.params;
if (!isLanguage(lang)) notFound();
const t = copy[lang];
const posts = sortBlogPages(blogSource.getPages(lang));
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">{t.title}</h1>
<p className="mt-4 text-lg leading-8 text-fd-muted-foreground">{t.intro}</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, lang)}
</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>
);
}
+18
View File
@@ -0,0 +1,18 @@
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { baseOptions } from '@/lib/layout.shared';
import { isLanguage, i18n } from '@/lib/i18n';
export default async function Layout({ params, children }: LayoutProps<'/[lang]'>) {
const { lang } = await params;
const base = baseOptions(isLanguage(lang) ? lang : i18n.defaultLanguage);
return (
<HomeLayout
{...base}
nav={{ ...base.nav, transparentMode: 'always' }}
className="home-fullbleed-nav"
>
{children}
</HomeLayout>
);
}
+252
View File
@@ -0,0 +1,252 @@
import Link from 'next/link';
import {
ArrowRight,
BookOpen,
Settings2,
Star,
Wrench,
} from 'lucide-react';
import { notFound } from 'next/navigation';
import { DesktopDemo } from '@/components/hero/DesktopDemo';
import { DownloadButton } from '@/components/hero/DownloadButton';
import { blogSource, formatBlogDate, sortBlogPages } from '@/lib/blog';
import { formatStars, getRepoStats } from '@/lib/github';
import { i18n, isLanguage, type Language } from '@/lib/i18n';
import { releaseUrl, repositoryUrl } from '@/lib/shared';
const copy: Record<
Language,
{
pillReleased: (version: string) => string;
pillFallback: string;
title: string;
subtitle: string;
readDocs: string;
flow: [string, string, string];
docsEyebrow: string;
docsTitle: string;
viewAll: string;
docs: { icon: typeof BookOpen; title: string; description: string; href: string }[];
blogEyebrow: string;
blogTitle: string;
}
> = {
zh: {
pillReleased: (version) => `${version} 已发布 · 开源 · 本地运行`,
pillFallback: '开源 · 本地运行 · 自由接入',
title: 'Cursor 服务端的开源替代',
subtitle:
'在本机运行自己的模型网关,用自己的 API Key 接入 OpenAI、Anthropic 兼容服务或自定义端点,完整保留 Cursor Agent 的工具调用、Skills 和 MCP。',
readDocs: '阅读文档',
flow: ['Cursor 客户端', 'cursor-byok 本地服务', '你的模型 API(OpenAI / Anthropic 兼容)'],
docsEyebrow: 'DOCUMENTATION',
docsTitle: '文档',
viewAll: '查看全部',
docs: [
{
icon: BookOpen,
title: '快速开始',
description: '完成安装、初始化和第一次模型调用。',
href: '/docs',
},
{
icon: Settings2,
title: '模型配置',
description: '配置协议、服务地址、凭据和生成参数。',
href: '/docs/model-configuration',
},
{
icon: Wrench,
title: '故障排查',
description: '解决证书、连接和模型测试问题。',
href: '/docs/troubleshooting',
},
],
blogEyebrow: 'DEVELOPER BLOG',
blogTitle: '开发者博客',
},
en: {
pillReleased: (version) => `${version} released · Open source · Runs locally`,
pillFallback: 'Open source · Runs locally · Any provider',
title: "The open-source alternative to Cursor's backend",
subtitle:
'Run your own model gateway locally, connect OpenAI- and Anthropic-compatible services or custom endpoints with your own API keys, and keep Cursor Agent tool calling, Skills, and MCP intact.',
readDocs: 'Read the docs',
flow: ['Cursor client', 'cursor-byok local service', 'Your model API (OpenAI / Anthropic compatible)'],
docsEyebrow: 'DOCUMENTATION',
docsTitle: 'Documentation',
viewAll: 'View all',
docs: [
{
icon: BookOpen,
title: 'Quick Start',
description: 'Install, initialize, and make your first model call.',
href: '/docs',
},
{
icon: Settings2,
title: 'Model Configuration',
description: 'Configure protocols, server addresses, credentials, and parameters.',
href: '/docs/model-configuration',
},
{
icon: Wrench,
title: 'Troubleshooting',
description: 'Resolve certificate, connection, and model test issues.',
href: '/docs/troubleshooting',
},
],
blogEyebrow: 'DEVELOPER BLOG',
blogTitle: 'Developer Blog',
},
};
export default async function HomePage(props: PageProps<'/[lang]'>) {
const { lang } = await props.params;
if (!isLanguage(lang)) notFound();
const t = copy[lang];
const prefix = lang === i18n.defaultLanguage ? '' : `/${lang}`;
const posts = sortBlogPages(blogSource.getPages(lang)).slice(0, 3);
const { stars, version } = await getRepoStats();
return (
<main className="flex flex-1 flex-col">
<section className="relative -mt-14 overflow-hidden border-b px-4 pb-16 pt-34 sm:px-6 sm:pb-24 sm:pt-42">
<div aria-hidden className="pointer-events-none absolute inset-0 -z-10">
<div className="absolute inset-0 bg-[linear-gradient(to_right,var(--color-fd-border)_1px,transparent_1px),linear-gradient(to_bottom,var(--color-fd-border)_1px,transparent_1px)] bg-[size:56px_56px] opacity-40 [mask-image:radial-gradient(ellipse_70%_60%_at_50%_0%,#000_20%,transparent_75%)]" />
<div className="absolute left-1/2 top-[-14rem] h-[26rem] w-[44rem] -translate-x-1/2 rounded-full bg-fd-primary/10 blur-3xl" />
</div>
<div className="mx-auto max-w-6xl">
<div className="mx-auto max-w-3xl text-center">
<a
href={releaseUrl}
className="inline-flex items-center gap-2 rounded-full border bg-fd-card px-4 py-1.5 text-sm text-fd-muted-foreground transition-colors hover:text-fd-foreground"
>
<span className="relative flex size-2">
<span className="absolute inline-flex size-full animate-ping rounded-full bg-fd-primary opacity-50" />
<span className="relative inline-flex size-2 rounded-full bg-fd-primary" />
</span>
{version ? t.pillReleased(version) : t.pillFallback}
<ArrowRight className="size-3.5" />
</a>
<h1 className="mt-6 text-4xl font-bold tracking-tight sm:text-6xl">{t.title}</h1>
<p className="mx-auto mt-6 max-w-2xl text-lg leading-8 text-fd-muted-foreground">
{t.subtitle}
</p>
<div className="mt-10 flex flex-wrap items-center justify-center gap-3">
<DownloadButton lang={lang} />
<Link
href={`${prefix}/docs`}
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"
>
{t.readDocs}
<ArrowRight className="size-4" />
</Link>
<a
href={repositoryUrl}
target="_blank"
rel="noreferrer"
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"
>
<GitHubIcon className="size-4" />
GitHub
{stars !== null ? (
<span className="flex items-center gap-1 border-l pl-2.5 text-sm text-fd-muted-foreground">
<Star className="size-3.5" />
{formatStars(stars)}
</span>
) : null}
</a>
</div>
</div>
<DesktopDemo lang={lang} />
<div className="mx-auto mt-10 flex flex-wrap items-center justify-center gap-x-3 gap-y-2 font-mono text-xs text-fd-muted-foreground sm:text-sm">
<span>{t.flow[0]}</span>
<ArrowRight className="size-3.5 shrink-0" />
<span className="font-medium text-fd-foreground">{t.flow[1]}</span>
<ArrowRight className="size-3.5 shrink-0" />
<span>{t.flow[2]}</span>
</div>
</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">{t.docsEyebrow}</p>
<h2 className="mt-3 text-3xl font-bold tracking-tight">{t.docsTitle}</h2>
</div>
<Link
href={`${prefix}/docs`}
className="hidden items-center gap-2 text-sm font-medium sm:flex"
>
{t.viewAll}
<ArrowRight className="size-4" />
</Link>
</div>
<div className="mt-10 grid gap-4 md:grid-cols-3">
{t.docs.map(({ icon: Icon, title, description, href }) => (
<Link
key={href}
href={`${prefix}${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">{t.blogEyebrow}</p>
<h2 className="mt-3 text-3xl font-bold tracking-tight">{t.blogTitle}</h2>
</div>
<Link href={`${prefix}/blog`} className="flex items-center gap-2 text-sm font-medium">
{t.viewAll}
<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, lang)}
</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>
);
}
function GitHubIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className={className}>
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2Z" />
</svg>
);
}
@@ -13,16 +13,16 @@ 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]]'>) {
export default async function Page(props: PageProps<'/[lang]/docs/[[...slug]]'>) {
const params = await props.params;
const page = source.getPage(params.slug);
const page = source.getPage(params.slug, params.lang);
if (!page) notFound();
const MDX = page.data.body;
const markdownUrl = getPageMarkdownUrl(page).url;
return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<DocsPage toc={page.data.toc} full={page.data.full} tableOfContent={{ style: 'clerk' }}>
<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">
@@ -48,9 +48,11 @@ export async function generateStaticParams() {
return source.generateParams();
}
export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): Promise<Metadata> {
export async function generateMetadata(
props: PageProps<'/[lang]/docs/[[...slug]]'>,
): Promise<Metadata> {
const params = await props.params;
const page = source.getPage(params.slug);
const page = source.getPage(params.slug, params.lang);
if (!page) notFound();
return {
+15
View File
@@ -0,0 +1,15 @@
import { source } from '@/lib/source';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { baseOptions } from '@/lib/layout.shared';
import { i18n, isLanguage } from '@/lib/i18n';
export default async function Layout({ params, children }: LayoutProps<'/[lang]/docs'>) {
const { lang } = await params;
const language = isLanguage(lang) ? lang : i18n.defaultLanguage;
return (
<DocsLayout tree={source.getPageTree(language)} {...baseOptions(language)}>
{children}
</DocsLayout>
);
}
+50
View File
@@ -0,0 +1,50 @@
import { RootProvider } from 'fumadocs-ui/provider/next';
import { i18nProvider } from 'fumadocs-ui/i18n';
import { notFound } from 'next/navigation';
import type { Metadata } from 'next';
import { htmlLang, i18n, isLanguage } from '@/lib/i18n';
import { translations } from '@/lib/layout.shared';
import '../global.css';
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://docs.leokun.cn';
export async function generateMetadata(props: LayoutProps<'/[lang]'>): Promise<Metadata> {
const { lang } = await props.params;
if (lang === 'en') {
return {
metadataBase: new URL(siteUrl),
title: {
default: 'cursor-byok Docs',
template: '%s | cursor-byok',
},
description: 'Installation, model configuration, and troubleshooting guide for cursor-byok.',
};
}
return {
metadataBase: new URL(siteUrl),
title: {
default: 'cursor-byok 文档',
template: '%s | cursor-byok',
},
description: 'cursor-byok 的安装、模型配置与故障排查指南。',
};
}
export function generateStaticParams() {
return i18n.languages.map((lang) => ({ lang }));
}
export default async function Layout({ params, children }: LayoutProps<'/[lang]'>) {
const { lang } = await params;
if (!isLanguage(lang)) notFound();
return (
<html lang={htmlLang[lang]} suppressHydrationWarning>
<body className="flex min-h-screen flex-col">
<RootProvider i18n={i18nProvider(translations, lang)}>{children}</RootProvider>
</body>
</html>
);
}
@@ -0,0 +1,29 @@
import { getLLMText, getPageMarkdownUrl, source } from '@/lib/source';
import { i18n } from '@/lib/i18n';
import { notFound } from 'next/navigation';
export const revalidate = false;
export async function GET(
_req: Request,
{ params }: RouteContext<'/[lang]/llms.mdx/docs/[[...slug]]'>,
) {
const { lang, slug } = await params;
const page = source.getPage(slug?.slice(0, -1), lang);
if (!page) notFound();
return new Response(await getLLMText(page), {
headers: {
'Content-Type': 'text/markdown',
},
});
}
export function generateStaticParams() {
return i18n.languages.flatMap((lang) =>
source.getPages(lang).map((page) => ({
lang,
slug: getPageMarkdownUrl(page).segments,
})),
);
}
@@ -1,4 +1,5 @@
import { getPageImageUrl, source } from '@/lib/source';
import { i18n } from '@/lib/i18n';
import { notFound } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { generate as DefaultImage } from 'fumadocs-ui/og';
@@ -6,9 +7,9 @@ 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));
export async function GET(_req: Request, { params }: RouteContext<'/[lang]/og/docs/[...slug]'>) {
const { lang, slug } = await params;
const page = source.getPage(slug.slice(0, -1), lang);
if (!page) notFound();
return new ImageResponse(
@@ -21,8 +22,10 @@ export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...
}
export function generateStaticParams() {
return source.getPages().map((page) => ({
lang: page.locale,
slug: getPageImageUrl(page).segments,
}));
return i18n.languages.flatMap((lang) =>
source.getPages(lang).map((page) => ({
lang,
slug: getPageImageUrl(page).segments,
})),
);
}
-11
View File
@@ -1,11 +0,0 @@
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>
);
}
+24
View File
@@ -14,3 +14,27 @@ html > body[data-scroll-locked] {
margin-right: 0 !important;
--removed-body-scroll-bar-size: 0px !important;
}
/* 文档页目录:隐藏默认折线,改用单条直线导轨 */
#nd-toc a[href^='#'] > svg,
#nd-toc div.absolute:has(> svg) {
display: none;
}
#nd-toc a[href^='#'] {
border-inline-start: 1px solid var(--color-fd-border);
}
#nd-toc a[href^='#'][data-active='true'] {
border-inline-start-color: var(--color-fd-primary);
}
/* 首页导航:通铺整行、无背景无分隔线 */
.home-fullbleed-nav {
--fd-layout-width: 100%;
}
.home-fullbleed-nav #nd-nav > * {
border-bottom-color: transparent;
backdrop-filter: none;
}
-26
View File
@@ -1,26 +0,0 @@
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>
);
}
@@ -1,23 +0,0 @@
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,
}));
}