OpenAPI: improve description rendering

This commit is contained in:
Fuma Nama
2025-11-23 21:01:48 +08:00
parent 116bc7a853
commit 60e3324f45
10 changed files with 59 additions and 58 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---
improve description rendering
+2 -2
View File
@@ -55,9 +55,9 @@ export default async function Page(props: PageProps<'/docs/[...slug]'>) {
if (page.data.type === 'openapi') {
const { APIPage } = await import('@/components/api-page');
return (
<DocsPage>
<DocsPage full>
<h1 className="text-[1.75em] font-semibold">{page.data.title}</h1>
<p className="text-fd-muted-foreground mb-6">{page.data.description}</p>
<DocsBody>
<APIPage {...page.data.getAPIPageProps()} />
</DocsBody>
+1 -1
View File
@@ -382,7 +382,7 @@ export function AISearchPanel() {
<Presence present={open}>
<div
data-state={open ? 'open' : 'closed'}
className="fixed inset-0 z-20 backdrop-blur-xs bg-fd-overlay data-[state=open]:animate-fd-fade-in data-[state=closed]:animate-fd-fade-out lg:hidden"
className="fixed inset-0 z-30 backdrop-blur-xs bg-fd-overlay data-[state=open]:animate-fd-fade-in data-[state=closed]:animate-fd-fade-out lg:hidden"
onClick={() => setOpen(false)}
/>
</Presence>
+5 -1
View File
@@ -93,7 +93,11 @@ export async function openapiSource(
path: `${baseDir}/${entry.path}`,
data: {
...entry.info,
getAPIPageProps: () => toBody(entry),
getAPIPageProps: () => {
const props = toBody(entry);
props.showDescription ??= true;
return props;
},
_openapi: {
method:
entry.type === 'operation' || entry.type === 'webhook'
+10 -4
View File
@@ -123,6 +123,7 @@ export interface CreateAPIPageOptions {
renderOperationLayout?: (
slots: {
header: ReactNode;
description: ReactNode;
apiExample: ReactNode;
apiPlayground: ReactNode;
@@ -138,6 +139,7 @@ export interface CreateAPIPageOptions {
renderWebhookLayout?: (slots: {
header: ReactNode;
description: ReactNode;
authSchemes: ReactNode;
paremeters: ReactNode;
body: ReactNode;
@@ -188,7 +190,8 @@ export interface CreateAPIPageOptions {
export interface ApiPageProps {
document: Promise<ProcessedDocument> | string | ProcessedDocument;
hasHead?: boolean;
showTitle?: boolean;
showDescription?: boolean;
/**
* An array of operations
@@ -307,7 +310,8 @@ export function createAPIPage(
}
async function APIPage({
hasHead = false,
showTitle: hasHead = false,
showDescription,
operations,
webhooks,
ctx,
@@ -348,7 +352,8 @@ async function APIPage({
method={method}
path={item.path}
ctx={ctx}
hasHead={hasHead}
showTitle={hasHead}
showDescription={showDescription}
/>
),
};
@@ -375,7 +380,8 @@ async function APIPage({
method={createMethod(item.method, webhook, hook)}
ctx={ctx}
path={`/${item.name}`}
hasHead={hasHead}
showTitle={hasHead}
showDescription={showDescription}
/>
),
};
@@ -141,7 +141,7 @@ export async function APIExample({
renderAPIExampleLayout ??= (slots) => {
return (
<div className="prose-no-margin md:sticky md:top-(--fd-api-info-top) xl:w-[400px]">
<div className="prose-no-margin md:sticky md:py-4 md:top-(--fd-docs-toc-top) xl:w-[400px]">
{slots.selector}
{slots.usageTabs}
{slots.responseTabs}
+15 -18
View File
@@ -46,7 +46,8 @@ export async function Operation({
path,
method,
ctx,
hasHead,
showTitle,
showDescription,
headingLevel = 2,
}: {
type?: 'webhook' | 'operation';
@@ -54,7 +55,8 @@ export async function Operation({
method: MethodInformation;
ctx: RenderContext;
hasHead?: boolean;
showTitle?: boolean;
showDescription?: boolean;
headingLevel?: number;
}) {
const {
@@ -62,22 +64,21 @@ export async function Operation({
} = ctx;
const body = method.requestBody;
let headNode: ReactNode = null;
const descriptionNode =
showDescription &&
method.description &&
ctx.renderMarkdown(method.description);
let bodyNode: ReactNode = null;
let authNode: ReactNode = null;
let responseNode: ReactNode = null;
let callbacksNode: ReactNode = null;
if (hasHead) {
if (showTitle) {
const title =
method.summary ??
(method.operationId ? idToTitle(method.operationId) : path);
headNode = (
<>
{ctx.renderHeading(headingLevel, title)}
{method.description && ctx.renderMarkdown(method.description)}
</>
);
headNode = ctx.renderHeading(headingLevel, title);
headingLevel++;
}
@@ -262,18 +263,11 @@ export async function Operation({
if (type === 'operation') {
renderOperationLayout ??= (slots) => {
return (
<div
className="flex flex-col gap-x-6 gap-y-4 xl:flex-row xl:items-start"
style={
{
'--fd-api-info-top':
'calc(12px + var(--fd-nav-height) + var(--fd-banner-height) + var(--fd-tocnav-height, 0px))',
} as object
}
>
<div className="flex flex-col gap-x-6 gap-y-4 xl:flex-row xl:items-start">
<div className="min-w-0 flex-1">
{slots.header}
{slots.apiPlayground}
{slots.description}
{slots.authSchemes}
{slots.paremeters}
{slots.body}
@@ -289,6 +283,7 @@ export async function Operation({
const content = await renderOperationLayout(
{
header: headNode,
description: descriptionNode,
authSchemes: authNode,
body: bodyNode,
callbacks: callbacksNode,
@@ -325,6 +320,7 @@ export async function Operation({
renderWebhookLayout ??= (slots) => (
<div>
{slots.header}
{slots.description}
{slots.authSchemes}
{slots.paremeters}
{slots.body}
@@ -334,6 +330,7 @@ export async function Operation({
);
return renderWebhookLayout({
header: headNode,
description: descriptionNode,
authSchemes: authNode,
body: bodyNode,
callbacks: callbacksNode,
+14 -22
View File
@@ -176,9 +176,20 @@ function SchemaUIContent({ $type }: { $type: string }) {
child = (
<>
{child}
<ObjectCollapsible name="Array item">
<SchemaUIContent $type={schema.item.$type} />
</ObjectCollapsible>
<Collapsible className="my-2">
<CollapsibleTrigger
className={cn(
buttonVariants({ color: 'secondary', size: 'sm' }),
'group px-3 py-2 data-[state=open]:rounded-b-none',
)}
>
Array Item
<ChevronDown className="size-4 text-fd-muted-foreground group-data-[state=open]:rotate-180" />
</CollapsibleTrigger>
<CollapsibleContent className="-mt-px bg-fd-card px-3 rounded-lg rounded-tl-none border shadow-sm">
<SchemaUIContent $type={schema.item.$type} />
</CollapsibleContent>
</Collapsible>
</>
);
}
@@ -387,25 +398,6 @@ function LinkRef({
);
}
function ObjectCollapsible(props: { name: string; children: ReactNode }) {
return (
<Collapsible className="my-2" {...props}>
<CollapsibleTrigger
className={cn(
buttonVariants({ color: 'secondary', size: 'sm' }),
'group px-3 py-2 data-[state=open]:rounded-b-none',
)}
>
{props.name}
<ChevronDown className="size-4 text-fd-muted-foreground group-data-[state=open]:rotate-180" />
</CollapsibleTrigger>
<CollapsibleContent className="-mt-px *:bg-fd-card">
{props.children}
</CollapsibleContent>
</Collapsible>
);
}
interface PropertyProps {
name: ReactNode;
type: ReactNode;
+2 -3
View File
@@ -4,19 +4,18 @@ import type { OutputEntry } from '@/utils/pages/builder';
export function toBody(entry: OutputEntry): ApiPageProps {
if (entry.type === 'operation')
return {
hasHead: false,
document: entry.schemaId,
operations: [entry.item],
};
if (entry.type === 'webhook')
return {
hasHead: false,
document: entry.schemaId,
webhooks: [entry.item],
};
return {
hasHead: true,
showTitle: true,
showDescription: true,
document: entry.schemaId,
operations: entry.operations,
webhooks: entry.webhooks,
+4 -6
View File
@@ -62,7 +62,6 @@ export function toText(
processed,
{
operations: [entry.item],
hasHead: false,
},
{
...options,
@@ -79,7 +78,7 @@ export function toText(
{
operations: entry.operations,
webhooks: entry.webhooks,
hasHead: true,
showTitle: true,
},
{
...options,
@@ -96,7 +95,7 @@ export function toText(
{
operations: entry.operations,
webhooks: entry.webhooks,
hasHead: true,
showTitle: true,
},
{
...options,
@@ -113,7 +112,6 @@ export function toText(
processed,
{
webhooks: [entry.item],
hasHead: false,
},
{
...options,
@@ -243,7 +241,7 @@ function generateStaticData(
const operation = dereferenced.paths?.[item.path]?.[item.method];
if (!operation) continue;
if (props.hasHead && operation.operationId) {
if (props.showTitle && operation.operationId) {
const title =
operation.summary ??
(operation.operationId ? idToTitle(operation.operationId) : item.path);
@@ -281,5 +279,5 @@ function pageContent(props: ApiPageProps): string {
method: item.method,
}));
return `<APIPage document={${JSON.stringify(props.document)}} operations={${JSON.stringify(operations)}} webhooks={${JSON.stringify(webhooks)}} hasHead={${JSON.stringify(props.hasHead)}} />`;
return `<APIPage document={${JSON.stringify(props.document)}} operations={${JSON.stringify(operations)}} webhooks={${JSON.stringify(webhooks)}} hasHead={${JSON.stringify(props.showTitle)}} />`;
}