From f91b24080200bd205c29013d196208f5311f4d3c Mon Sep 17 00:00:00 2001 From: Tim <135014430+nagisa77@users.noreply.github.com> Date: Fri, 19 Sep 2025 17:57:44 +0800 Subject: [PATCH] feat(docs): show API routes on overview --- docs/components/api-overview.tsx | 65 +++++++++++++++++++++++++++ docs/content/docs/openapi/index.mdx | 8 ++++ docs/lib/openapi-operations.ts | 68 +++++++++++++++++++++++++++++ docs/scripts/generate-docs.ts | 10 ++++- 4 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 docs/components/api-overview.tsx create mode 100644 docs/lib/openapi-operations.ts diff --git a/docs/components/api-overview.tsx b/docs/components/api-overview.tsx new file mode 100644 index 000000000..61de32413 --- /dev/null +++ b/docs/components/api-overview.tsx @@ -0,0 +1,65 @@ +import Link from "next/link"; + +import { getOpenAPIOperations } from "@/lib/openapi-operations"; + +const methodColors: Record = { + GET: "bg-emerald-100 text-emerald-700", + POST: "bg-blue-100 text-blue-700", + PUT: "bg-amber-100 text-amber-700", + PATCH: "bg-purple-100 text-purple-700", + DELETE: "bg-rose-100 text-rose-700", +}; + +function MethodBadge({ method }: { method: string }) { + const color = methodColors[method] ?? "bg-slate-100 text-slate-700"; + + return ( + + {method} + + ); +} + +export function APIOverviewTable() { + const operations = getOpenAPIOperations(); + + if (operations.length === 0) { + return null; + } + + return ( +
+ + + + + + + + + + {operations.map((operation) => ( + + + + + + ))} + +
路径方法摘要
+ + {operation.route} + + + + + {operation.summary || "—"} +
+
+ ); +} diff --git a/docs/content/docs/openapi/index.mdx b/docs/content/docs/openapi/index.mdx index dbba8c47e..f77d37a6a 100644 --- a/docs/content/docs/openapi/index.mdx +++ b/docs/content/docs/openapi/index.mdx @@ -2,3 +2,11 @@ title: API 概览 description: Open API 接口文档 --- + +import { APIOverviewTable } from "@/components/api-overview"; + +# 接口列表 + +以下列表聚合了所有已生成的接口页面,展示对应的路径、请求方法以及摘要,便于快速检索和跳转。 + + diff --git a/docs/lib/openapi-operations.ts b/docs/lib/openapi-operations.ts new file mode 100644 index 000000000..83fa96970 --- /dev/null +++ b/docs/lib/openapi-operations.ts @@ -0,0 +1,68 @@ +import matter from "gray-matter"; + +import { source } from "@/lib/source"; + +interface OperationFrontmatter { + title?: string; + description?: string; + _openapi?: { + method?: string; + route?: string; + }; +} + +export interface OpenAPIOperation { + href: string; + method: string; + route: string; + summary: string; +} + +function parseFrontmatter(content: string): OperationFrontmatter { + const result = matter(content); + + return result.data as OperationFrontmatter; +} + +function normalizeSummary(frontmatter: OperationFrontmatter): string { + return frontmatter.title ?? frontmatter.description ?? ""; +} + +export function getOpenAPIOperations(): OpenAPIOperation[] { + return source + .getPages() + .filter((page) => + page.url.startsWith("/openapi/") && page.url !== "/openapi" + ) + .map((page) => { + if (typeof page.data.content !== "string") { + return undefined; + } + + const frontmatter = parseFrontmatter(page.data.content); + + const method = frontmatter._openapi?.method?.toUpperCase(); + const route = frontmatter._openapi?.route; + const summary = normalizeSummary(frontmatter); + + if (!method || !route) { + return undefined; + } + + return { + href: page.url, + method, + route, + summary, + } satisfies OpenAPIOperation; + }) + .filter((operation): operation is OpenAPIOperation => Boolean(operation)) + .sort((a, b) => { + const routeCompare = a.route.localeCompare(b.route); + if (routeCompare !== 0) { + return routeCompare; + } + + return a.method.localeCompare(b.method); + }); +} diff --git a/docs/scripts/generate-docs.ts b/docs/scripts/generate-docs.ts index 449e28d15..858909115 100644 --- a/docs/scripts/generate-docs.ts +++ b/docs/scripts/generate-docs.ts @@ -1,10 +1,18 @@ +import { rmSync } from "node:fs"; + import { generateFiles } from "fumadocs-openapi"; import { openapi } from "@/lib/openapi"; +const outputDir = "./content/docs/openapi/(generated)"; + +rmSync(outputDir, { recursive: true, force: true }); + void generateFiles({ input: openapi, - output: "./content/docs/openapi/(generated)", + output: outputDir, // we recommend to enable it // make sure your endpoint description doesn't break MDX syntax. includeDescription: true, + per: "operation", + groupBy: "route", });