feat(docs): reorganize documentation structure and update navigation links

This commit is contained in:
HouYunFei
2026-06-01 17:50:53 +08:00
parent 5f59b4bed3
commit b16251c1ed
65 changed files with 2491 additions and 281 deletions
+6
View File
@@ -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>;
}
+171
View File
@@ -0,0 +1,171 @@
import Link from 'next/link';
import { ArrowUpRight, BookOpen, Rocket } from 'lucide-react';
import { appName, gitConfig } from '@/lib/shared';
const githubUrl = `https://github.com/${gitConfig.user}/${gitConfig.repo}`;
const demoUrl = 'https://infinite-canvas-cpco.onrender.com/';
const starHistoryUrl = `https://www.star-history.com/?repos=${gitConfig.user}%2F${gitConfig.repo}&type=date`;
const starHistoryChart = `https://api.star-history.com/chart?repos=${gitConfig.user}/${gitConfig.repo}&type=date&transparent=true`;
const darkStarHistoryChart = `${starHistoryChart}&theme=dark`;
const previewImages = [
{
src: 'https://i.ibb.co/TDFvGWDT/image.png',
title: '画布编排',
},
{
src: 'https://i.ibb.co/zVwJq3YS/image.png',
title: '图片生成',
},
{
src: 'https://i.ibb.co/PvY3qhhK/image.png',
title: '参考图编辑',
},
{
src: 'https://i.ibb.co/7D04LwN/image.png',
title: '节点工作流',
},
];
export default function HomePage() {
return (
<main className="mx-auto flex w-full max-w-6xl flex-1 flex-col px-5 pb-16 pt-8 md:px-10 md:pt-14">
<section className="grid min-h-[520px] items-center gap-10 border-b border-zinc-200 pb-12 dark:border-zinc-800 lg:grid-cols-[0.88fr_1.12fr]">
<div>
<div className="inline-flex items-center gap-2 text-xs font-medium text-zinc-500 dark:text-zinc-400">
<Rocket className="size-3.5 text-emerald-600 dark:text-emerald-400" />
AI
</div>
<h1 className="mt-6 max-w-3xl text-4xl font-semibold leading-tight text-zinc-950 dark:text-zinc-50 md:text-6xl [font-family:var(--font-display)]">
{appName}
<span className="block text-zinc-500 dark:text-zinc-400"></span>
</h1>
<p className="mt-6 max-w-2xl text-base leading-8 text-zinc-600 dark:text-zinc-400">
AI
</p>
<div className="mt-8 flex flex-wrap gap-3">
<Link
href="/docs/overview/quick-start"
className="inline-flex items-center justify-center gap-2 rounded-full bg-zinc-950 px-5 py-3 text-sm font-medium text-white transition hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-950 dark:hover:bg-zinc-200"
>
<BookOpen className="size-4" />
</Link>
<a
href={githubUrl}
target="_blank"
rel="noreferrer noopener"
className="inline-flex items-center justify-center gap-2 rounded-full border border-zinc-300 px-5 py-3 text-sm font-medium text-zinc-900 transition hover:border-zinc-900 hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-100 dark:hover:border-zinc-500 dark:hover:bg-zinc-900"
>
<img src="/github.svg" alt="" className="size-4" />
GitHub
</a>
<a
href={demoUrl}
target="_blank"
rel="noreferrer noopener"
className="inline-flex items-center justify-center gap-2 rounded-full border border-zinc-300 px-5 py-3 text-sm font-medium text-zinc-900 transition hover:border-zinc-900 hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-100 dark:hover:border-zinc-500 dark:hover:bg-zinc-900"
>
线
<ArrowUpRight className="size-4" />
</a>
</div>
</div>
<div className="overflow-hidden rounded-2xl lg:w-[108%] lg:max-w-none">
<img
src={previewImages[3].src}
alt="无限画布效果图"
className="aspect-[16/10] w-full rounded-xl object-cover"
/>
</div>
</section>
<section className="mt-14">
<div className="flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
<div>
<h2 className="text-2xl font-semibold text-zinc-950 dark:text-zinc-50 md:text-3xl">
</h2>
</div>
<Link
href="/docs/overview/features"
className="inline-flex w-fit items-center gap-1.5 text-sm font-medium text-zinc-800 transition hover:text-zinc-950 dark:text-zinc-200 dark:hover:text-white"
>
<ArrowUpRight className="size-4" />
</Link>
</div>
<div className="mt-6 grid gap-4 md:grid-cols-2">
{previewImages.map((item) => (
<img
key={item.src}
src={item.src}
alt={`${item.title}效果图`}
loading="lazy"
decoding="async"
className="aspect-[16/10] w-full rounded-2xl object-cover"
/>
))}
</div>
</section>
<section className="mx-auto mt-16 w-full max-w-4xl text-center">
<h2 className="text-2xl font-semibold text-zinc-950 dark:text-zinc-50 md:text-3xl">
</h2>
<p className="mt-2 text-sm text-zinc-500 dark:text-zinc-400">
</p>
<div className="mt-7 flex justify-center">
<a
href={`${githubUrl}/graphs/contributors`}
target="_blank"
rel="noreferrer noopener"
className="inline-flex max-w-full"
>
<img
src={`https://contrib.rocks/image?repo=${gitConfig.user}/${gitConfig.repo}`}
alt="开发贡献者头像"
loading="lazy"
decoding="async"
className="max-w-full"
/>
</a>
</div>
</section>
<section className="mx-auto mt-16 w-full max-w-5xl text-center">
<h2 className="text-2xl font-semibold text-zinc-950 dark:text-zinc-50 md:text-3xl">
Star History
</h2>
<div className="mt-7 flex justify-center">
<a
href={starHistoryUrl}
target="_blank"
rel="noreferrer noopener"
className="block w-full max-w-4xl"
>
<picture>
<source
media="(prefers-color-scheme: dark)"
srcSet={darkStarHistoryChart}
/>
<source
media="(prefers-color-scheme: light)"
srcSet={starHistoryChart}
/>
<img
src={starHistoryChart}
alt="Star History Chart"
loading="lazy"
decoding="async"
className="mx-auto w-full"
/>
</picture>
</a>
</div>
</section>
</main>
);
}
+9
View File
@@ -0,0 +1,9 @@
import { source } from '@/lib/source';
import { createFromSource } from 'fumadocs-core/search/server';
export const revalidate = false;
export const { staticGET: GET } = createFromSource(source, {
// https://docs.orama.com/docs/orama-js/supported-languages
language: 'english',
});
+20
View File
@@ -0,0 +1,20 @@
import { DocPageContent, getDocPageMetadata } from '@/lib/doc-page';
import { source } from '@/lib/source';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
export default async function Page(props: PageProps<'/docs/[...slug]'>) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
return <DocPageContent page={page} />;
}
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 getDocPageMetadata(page);
}
+13
View File
@@ -0,0 +1,13 @@
import { source } from '@/lib/source';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { baseOptions } from '@/lib/layout.shared';
import { DocsTopTabs } from '@/components/docs-top-tabs';
export default function Layout({ children }: LayoutProps<'/docs'>) {
return (
<DocsLayout {...baseOptions()} tree={source.getPageTree()} tabs={false}>
<DocsTopTabs />
{children}
</DocsLayout>
);
}
+39
View File
@@ -0,0 +1,39 @@
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
import { Markdown } from 'fumadocs-core/content/md';
import { getTableOfContents } from 'fumadocs-core/content/toc';
import { remarkHeading } from 'fumadocs-core/mdx-plugins';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import type { Metadata } from 'next';
import { getMDXComponents } from '@/components/mdx';
const title = '无限画布文档';
const description = '功能说明、操作手册、部署方式、开发文档、商务合作与赞助支持';
async function readDocsIndex() {
return readFile(join(process.cwd(), 'index.md'), 'utf8');
}
export default async function Page() {
const content = await readDocsIndex();
const toc = getTableOfContents(content);
return (
<DocsPage toc={toc}>
<DocsTitle>{title}</DocsTitle>
<DocsDescription>{description}</DocsDescription>
<DocsBody>
<Markdown components={getMDXComponents()} remarkPlugins={[remarkHeading]}>
{content}
</Markdown>
</DocsBody>
</DocsPage>
);
}
export function generateMetadata(): Metadata {
return {
title,
description,
};
}
@@ -0,0 +1,39 @@
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
import { Markdown } from 'fumadocs-core/content/md';
import { getTableOfContents } from 'fumadocs-core/content/toc';
import { remarkHeading } from 'fumadocs-core/mdx-plugins';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import type { Metadata } from 'next';
import { getMDXComponents } from '@/components/mdx';
const title = '更新日志';
const description = '项目版本变更记录';
async function readChangelog() {
return readFile(join(process.cwd(), '..', 'CHANGELOG.md'), 'utf8');
}
export default async function ChangelogPage() {
const changelog = await readChangelog();
const toc = getTableOfContents(changelog);
return (
<DocsPage toc={toc}>
<DocsTitle>{title}</DocsTitle>
<DocsDescription>{description}</DocsDescription>
<DocsBody>
<Markdown components={getMDXComponents()} remarkPlugins={[remarkHeading]}>
{changelog}
</Markdown>
</DocsBody>
</DocsPage>
);
}
export function generateMetadata(): Metadata {
return {
title,
description,
};
}
+12
View File
@@ -0,0 +1,12 @@
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
html {
scrollbar-gutter: stable;
}
html > body[data-scroll-locked] {
margin-right: 0px !important;
--removed-body-scroll-bar-size: 0px !important;
}
+4
View File
@@ -0,0 +1,4 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M32 8L58 54H46L32 29L18 54H6L32 8Z" fill="currentColor"/>
<path d="M32 40L40 54H24L32 40Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 229 B

+29
View File
@@ -0,0 +1,29 @@
import { Noto_Sans_SC, Sora } from 'next/font/google';
import { Provider } from '@/components/provider';
import './global.css';
const bodyFont = Noto_Sans_SC({
subsets: ['latin'],
weight: ['400', '500', '600'],
variable: '--font-body',
});
const displayFont = Sora({
subsets: ['latin'],
weight: ['500', '600', '700'],
variable: '--font-display',
});
export default function Layout({ children }: LayoutProps<'/'>) {
return (
<html
lang="zh-CN"
className={`${bodyFont.className} ${bodyFont.variable} ${displayFont.variable}`}
suppressHydrationWarning
>
<body className="flex flex-col min-h-screen">
<Provider>{children}</Provider>
</body>
</html>
);
}
+13
View File
@@ -0,0 +1,13 @@
import { getLLMText, source } from '@/lib/source';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
export const revalidate = false;
export async function GET() {
const docsIndex = await readFile(join(process.cwd(), 'index.md'), 'utf8');
const scan = source.getPages().map(getLLMText);
const scanned = await Promise.all(scan);
return new Response([docsIndex, ...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;
// remove the appended "content.md"
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) => ({
slug: getPageMarkdownUrl(page).segments,
}));
}
+11
View File
@@ -0,0 +1,11 @@
import { source } from '@/lib/source';
import { llms } from 'fumadocs-core/source';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
export const revalidate = false;
export async function GET() {
const docsIndex = await readFile(join(process.cwd(), 'index.md'), 'utf8');
return new Response([docsIndex, llms(source).index()].join('\n\n'));
}
+28
View File
@@ -0,0 +1,28 @@
import { getPageImage, 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: getPageImage(page).segments,
}));
}
+41
View File
@@ -0,0 +1,41 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { cn } from '@/lib/cn';
const tabs = [
{ title: '项目介绍', href: '/docs/overview/quick-start', prefix: '/docs/overview' },
{ title: '操作手册', href: '/docs/canvas/canvas-node-manual', prefix: '/docs/canvas' },
{ title: '开发文档', href: '/docs/backend/local-development', prefix: '/docs/backend' },
{ title: '项目进度', href: '/docs/progress/changelog', prefix: '/docs/progress' },
{ title: '商务合作', href: '/docs/business/business', prefix: '/docs/business' },
{ title: '赞助支持', href: '/docs/support/donate', prefix: '/docs/support' },
];
export function DocsTopTabs() {
const pathname = usePathname();
return (
<nav className="sticky top-0 z-30 hidden h-12 self-start overflow-x-auto border-b bg-fd-background/95 px-6 pt-3 backdrop-blur [grid-area:main] md:flex xl:px-8">
<div className="flex flex-row items-end gap-6">
{tabs.map((tab) => {
const active = tab.prefix ? pathname === tab.href || pathname.startsWith(`${tab.prefix}/`) : pathname === tab.href;
return (
<Link
key={tab.href}
href={tab.href}
className={cn(
'inline-flex border-b-2 border-transparent pb-1.5 text-sm font-medium text-nowrap text-fd-muted-foreground transition-colors hover:text-fd-accent-foreground',
active && 'border-fd-primary text-fd-primary',
)}
>
{tab.title}
</Link>
);
})}
</div>
</nav>
);
}
+15
View File
@@ -0,0 +1,15 @@
import defaultMdxComponents from 'fumadocs-ui/mdx';
import type { MDXComponents } from 'mdx/types';
export function getMDXComponents(components?: MDXComponents) {
return {
...defaultMdxComponents,
...components,
} satisfies MDXComponents;
}
export const useMDXComponents = getMDXComponents;
declare global {
type MDXProvidedComponents = ReturnType<typeof getMDXComponents>;
}
+8
View File
@@ -0,0 +1,8 @@
'use client';
import SearchDialog from '@/components/search';
import { RootProvider } from 'fumadocs-ui/provider/next';
import { type ReactNode } from 'react';
export function Provider({ children }: { children: ReactNode }) {
return <RootProvider search={{ SearchDialog }}>{children}</RootProvider>;
}
+46
View File
@@ -0,0 +1,46 @@
'use client';
import {
SearchDialog,
SearchDialogClose,
SearchDialogContent,
SearchDialogHeader,
SearchDialogIcon,
SearchDialogInput,
SearchDialogList,
SearchDialogOverlay,
type SharedProps,
} from 'fumadocs-ui/components/dialog/search';
import { useDocsSearch } from 'fumadocs-core/search/client';
import { create } from '@orama/orama';
import { useI18n } from 'fumadocs-ui/contexts/i18n';
function initOrama() {
return create({
schema: { _: 'string' },
// https://docs.orama.com/docs/orama-js/supported-languages
language: 'english',
});
}
export default function DefaultSearchDialog(props: SharedProps) {
const { locale } = useI18n(); // (optional) for i18n
const { search, setSearch, query } = useDocsSearch({
type: 'static',
initOrama,
locale,
});
return (
<SearchDialog search={search} onSearchChange={setSearch} isLoading={query.isLoading} {...props}>
<SearchDialogOverlay />
<SearchDialogContent>
<SearchDialogHeader>
<SearchDialogIcon />
<SearchDialogInput />
<SearchDialogClose />
</SearchDialogHeader>
<SearchDialogList items={query.data !== 'empty' ? query.data : null} />
</SearchDialogContent>
</SearchDialog>
);
}
+1
View File
@@ -0,0 +1 @@
export { twMerge as cn } from 'tailwind-merge';
+48
View File
@@ -0,0 +1,48 @@
import { getMDXComponents } from '@/components/mdx';
import { gitConfig } from '@/lib/shared';
import { getPageImage, getPageMarkdownUrl, source } from '@/lib/source';
import type { Metadata } from 'next';
import { createRelativeLink } from 'fumadocs-ui/mdx';
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page';
import {
MarkdownCopyButton,
ViewOptionsPopover,
} from 'fumadocs-ui/layouts/docs/page';
export type DocPageData = (typeof source)['$inferPage'];
export function DocPageContent({ page }: { page: DocPageData }) {
const MDX = page.data.body;
const markdownUrl = getPageMarkdownUrl(page).url;
return (
<DocsPage toc={page.data.toc} full={page.data.full} className="md:pt-20 xl:pt-24">
<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}/${gitConfig.docsContentDir}/${page.path}`}
/>
</div>
<DocsBody>
<MDX
components={getMDXComponents({
a: createRelativeLink(source, page),
})}
/>
</DocsBody>
</DocsPage>
);
}
export function getDocPageMetadata(page: DocPageData): Metadata {
return {
title: page.data.title,
description: page.data.description,
openGraph: {
images: getPageImage(page).url,
},
};
}
+55
View File
@@ -0,0 +1,55 @@
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
import { appName, gitConfig } from './shared';
import { ArrowUpRight } from 'lucide-react';
const githubUrl = `https://github.com/${gitConfig.user}/${gitConfig.repo}`;
const qqUrl = 'https://qm.qq.com/q/DFnKzZ807u';
export function baseOptions(): BaseLayoutProps {
return {
nav: {
title: (
<span className="inline-flex items-center gap-2 font-semibold">
<img src="/logo.svg" alt={appName} className="h-6 w-6" />
<span>{appName}</span>
</span>
),
},
links: [
{
text: '文档导航',
url: '/docs/overview/quick-start',
on: 'nav',
},
{
text: (
<span className="inline-flex items-center gap-1.5">
<span>线</span>
<ArrowUpRight className="size-4" />
</span>
),
url: 'https://infinite-canvas-cpco.onrender.com/',
external: true,
on: 'nav',
},
{
type: 'icon',
text: 'GitHub',
label: 'GitHub',
url: githubUrl,
external: true,
on: 'menu',
icon: <img src="/github.svg" alt="" className="size-4" />,
},
{
type: 'icon',
text: 'QQ',
label: 'QQ',
url: qqUrl,
external: true,
on: 'menu',
icon: <img src="/qq.svg" alt="" className="size-4" />,
},
],
};
}
+12
View File
@@ -0,0 +1,12 @@
export const appName = '无限画布';
export const docsRoute = '/docs';
export const docsImageRoute = '/og/docs';
export const docsContentRoute = '/llms.mdx/docs';
// fill this with your actual GitHub info, for example:
export const gitConfig = {
user: 'basketikun',
repo: 'infinite-canvas',
branch: 'main',
docsContentDir: 'docs/content/docs',
};
+36
View File
@@ -0,0 +1,36 @@
import { docs } from 'collections/server';
import { loader } from 'fumadocs-core/source';
import { docsContentRoute, docsImageRoute, docsRoute } from './shared';
// See https://fumadocs.dev/docs/headless/source-api for more info
export const source = loader({
baseUrl: docsRoute,
source: docs.toFumadocsSource(),
plugins: [],
});
export function getPageImage(page: (typeof source)['$inferPage']) {
const segments = [...page.slugs, 'image.png'];
return {
segments,
url: `${docsImageRoute}/${segments.join('/')}`,
};
}
export function getPageMarkdownUrl(page: (typeof source)['$inferPage']) {
const segments = [...page.slugs, 'content.md'];
return {
segments,
url: `${docsContentRoute}/${segments.join('/')}`,
};
}
export async function getLLMText(page: (typeof source)['$inferPage']) {
const processed = await page.data.getText('processed');
return `# ${page.data.title} (${page.url})
${processed}`;
}