From f4469b2df3824741e83479d1ccdcaafdf95cf92b Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:35:09 +0200 Subject: [PATCH] Support OpenAPI one-operation-per-page mode (#4314) --- .changeset/rich-chefs-dig.md | 6 ++++ .../DocumentView/OpenAPI/OpenAPIOperation.tsx | 1 + .../DocumentView/OpenAPI/OpenAPIWebhook.tsx | 1 + .../DocumentView/OpenAPI/context.tsx | 4 ++- .../components/DocumentView/OpenAPI/style.css | 25 +++++++++-------- .../PageAside/ScrollSectionsList.tsx | 11 ++++---- .../TableOfContents/PageDocumentItem.tsx | 14 +++++++++- .../encodeClientTableOfContents.ts | 3 ++ .../src/components/TableOfContents/styles.ts | 2 +- .../src/lib/openapi/computedSourceProps.ts | 28 +++++++++++++++++++ .../src/OpenAPICodeSampleSelector.tsx | 8 +++++- .../react-openapi/src/OpenAPIMethodBadge.tsx | 28 +++++++++++++++++++ packages/react-openapi/src/OpenAPIPath.tsx | 5 ++++ .../react-openapi/src/OpenAPIPathItem.tsx | 13 +++++++-- .../src/common/OpenAPIColumnSpec.tsx | 4 ++- .../src/common/OpenAPISummary.tsx | 7 +++-- packages/react-openapi/src/context.ts | 5 ++++ .../react-openapi/src/formatOpenAPIMethod.ts | 14 ++++++++++ packages/react-openapi/src/index.ts | 2 ++ 19 files changed, 156 insertions(+), 25 deletions(-) create mode 100644 .changeset/rich-chefs-dig.md create mode 100644 packages/gitbook/src/lib/openapi/computedSourceProps.ts create mode 100644 packages/react-openapi/src/OpenAPIMethodBadge.tsx create mode 100644 packages/react-openapi/src/formatOpenAPIMethod.ts diff --git a/.changeset/rich-chefs-dig.md b/.changeset/rich-chefs-dig.md new file mode 100644 index 000000000..1490e5859 --- /dev/null +++ b/.changeset/rich-chefs-dig.md @@ -0,0 +1,6 @@ +--- +"@gitbook/react-openapi": patch +"gitbook": patch +--- + +Support OpenAPI one-operation-per-page mode diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIOperation.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIOperation.tsx index 634b59b10..16011a322 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIOperation.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIOperation.tsx @@ -54,6 +54,7 @@ async function OpenAPIOperationBody(props: BlockProps data.operation['x-expandAllResponses'] ?? data['x-expandAllResponses'], expandAllModelSections: data.operation['x-expandAllModelSections'] ?? data['x-expandAllModelSections'], + headless: block.type === 'openapi-operation' ? block.data.headless : undefined, })} className="openapi-block" /> diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIWebhook.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIWebhook.tsx index 2c6b21946..00b6b25cc 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIWebhook.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIWebhook.tsx @@ -56,6 +56,7 @@ async function OpenAPIWebhookBody(props: BlockProps) { data.operation['x-expandAllResponses'] ?? data['x-expandAllResponses'], expandAllModelSections: data.operation['x-expandAllModelSections'] ?? data['x-expandAllModelSections'], + headless: block.data.headless, })} className="openapi-block" /> diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx index 035f0fc3d..ba44e5d31 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx @@ -26,8 +26,9 @@ export function getOpenAPIContext(args: { context: GitBookAnyContext | undefined; expandAllResponses?: boolean; expandAllModelSections?: boolean; + headless?: boolean; }): OpenAPIContextInput { - const { props, specUrl, context, expandAllResponses, expandAllModelSections } = args; + const { props, specUrl, context, expandAllResponses, expandAllModelSections, headless } = args; const { block } = props; const customizationLocale = context ? getSpaceLocale(context) : DEFAULT_LOCALE; @@ -96,6 +97,7 @@ export function getOpenAPIContext(args: { ), expandAllResponses: expandAllResponses || props.context.mode === 'print', expandAllModelSections: expandAllModelSections || props.context.mode === 'print', + headless, id: block.meta?.id, blockKey: block.key, locale, diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index 7969e0741..2c35eaedd 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -101,7 +101,17 @@ /* Method Tags */ .openapi-method, .openapi-statuscode { - @apply rounded-md straight-corners:rounded-none circular-corners:rounded-lg uppercase font-mono items-center shrink-0 font-semibold text-[0.813rem] px-1 py-0.5 mr-2 text-tint-12/8 leading-tight align-middle inline-flex ring-1 ring-inset ring-tint-12/1 dark:ring-tint-1/1 whitespace-nowrap; + @apply m-0 h-5 min-w-9 justify-center rounded-md text-xs straight-corners:rounded-none circular-corners:rounded-lg uppercase font-mono items-center shrink-0 font-semibold px-1.5 py-0.5 mr-2 text-tint-12/8 leading-tight align-middle inline-flex whitespace-nowrap; +} + +.openapi-method-small {} + +.openapi-method-medium { + @apply m-0 px-2.5 py-1 h-6 text-[0.813rem]; +} + +.toclink .openapi-method { + @apply text-[0.625rem] flex items-center justify-center; } .openapi-method-get, @@ -130,7 +140,8 @@ .openapi-method-head, .openapi-method-options, -.openapi-method-trace { +.openapi-method-trace, +.openapi-method-hook { @apply bg-tint; } @@ -450,16 +461,12 @@ @apply max-w-full overflow-hidden shrink pl-0.5 py-0.5; } -.openapi-response-examples-header .openapi-select > button .openapi-statuscode { - @apply h-full; -} - .openapi-codesample-header-content { @apply flex flex-row items-center justify-between h-fit p-2.5; } .openapi-codesample-header-content .openapi-path { - @apply flex items-center font-mono text-[0.813rem] gap-1 h-fit *:truncate overflow-x-auto min-w-0 max-w-full font-normal text-tint-strong; + @apply flex items-center font-mono text-[0.813rem] gap-2 h-fit *:truncate overflow-x-auto min-w-0 max-w-full font-normal text-tint-strong; } .openapi-codesample-header-content .openapi-path-title { @@ -493,10 +500,6 @@ @apply text-tint inline; } -.openapi-summary .openapi-path .openapi-method { - @apply m-0 items-center flex px-2 py-1 h-6; -} - .openapi-path-title { @apply flex-1 relative font-normal items-center gap-y-1 flex flex-wrap text-left overflow-x-auto font-mono text-tint-strong/10; @apply whitespace-nowrap md:whitespace-normal; diff --git a/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx b/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx index 8f4278a40..d69472e87 100644 --- a/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx +++ b/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx @@ -7,6 +7,7 @@ import type { DocumentSection } from '@/lib/document-sections'; import { tcls } from '@/lib/tailwind'; import { useBodyLoaded } from '@/components/primitives'; +import { OpenAPIMethodBadge } from '@gitbook/react-openapi'; import { HEADER_HEIGHT_DESKTOP } from '../layout'; /** @@ -141,11 +142,11 @@ export function ScrollSectionsList({ sections }: { sections: DocumentSection[] } )} > {section.tag ? ( - - {section.tag} - + ) : null} } tag={page.primaryTag ? : null} > - {page.title} + {page.openAPIOperation ? ( + + + + ) : null} + + {page.title} + ); diff --git a/packages/gitbook/src/components/TableOfContents/encodeClientTableOfContents.ts b/packages/gitbook/src/components/TableOfContents/encodeClientTableOfContents.ts index df8667a68..db633204f 100644 --- a/packages/gitbook/src/components/TableOfContents/encodeClientTableOfContents.ts +++ b/packages/gitbook/src/components/TableOfContents/encodeClientTableOfContents.ts @@ -1,4 +1,5 @@ import type { GitBookSiteContext } from '@/lib/context'; +import { getOpenAPIOperationPageProps } from '@/lib/openapi/computedSourceProps'; import { getPagePaths, hasPageVisibleDescendant } from '@/lib/pages'; import { resolveContentRef } from '@/lib/references'; import { getRevisionTags, resolveTag } from '@/lib/tags'; @@ -26,6 +27,7 @@ export type ClientTOCPageDocument = { pathnames: string[]; descendants?: ClientTOCPage[]; primaryTag?: RevisionTag; + openAPIOperation?: { method: string; deprecated: boolean }; }; export type ClientTOCPageGroup = { @@ -83,6 +85,7 @@ export async function encodeClientTableOfContents( pathnames: getPagePaths(rootPages, page), descendants, primaryTag, + openAPIOperation: getOpenAPIOperationPageProps(page), type: 'document', }) ); diff --git a/packages/gitbook/src/components/TableOfContents/styles.ts b/packages/gitbook/src/components/TableOfContents/styles.ts index c61ab9645..001fdf5e9 100644 --- a/packages/gitbook/src/components/TableOfContents/styles.ts +++ b/packages/gitbook/src/components/TableOfContents/styles.ts @@ -3,7 +3,7 @@ export const ToCItemBaseStyles = [ 'circular-corners:rounded-2xl rounded-md straight-corners:rounded-none p-1.5 pl-3', 'focus-visible:-outline-offset-2', 'before:contents[] before:-left-px before:absolute before:inset-y-0', - 'sidebar-list-line:rounded-l-none! sidebar-list-line:before:w-px [&+div_a]:sidebar-list-default:rounded-l-none [&+div_a]:pl-5 [&+div_a]:sidebar-list-default:before:w-px', + 'sidebar-list-line:rounded-l-none! sidebar-list-line:before:w-px [&+div_a]:sidebar-list-default:rounded-l-none has-[.openapi-method]:pl-3 [&+div_a]:pl-5 [&+div_a]:sidebar-list-default:before:w-px', ]; export const ToCLinkItemStyles = [ diff --git a/packages/gitbook/src/lib/openapi/computedSourceProps.ts b/packages/gitbook/src/lib/openapi/computedSourceProps.ts new file mode 100644 index 000000000..4c9b83086 --- /dev/null +++ b/packages/gitbook/src/lib/openapi/computedSourceProps.ts @@ -0,0 +1,28 @@ +import type { ComputedContentSourceDocumentOpenAPI, RevisionPageDocument } from '@gitbook/api'; + +/** + * Get the method and deprecated state of a page generated by the OpenAPI + * computed source in one-operation-per-page mode. + */ +export function getOpenAPIOperationPageProps( + page: RevisionPageDocument +): { method: string; deprecated: boolean } | undefined { + if (!('computed' in page)) { + return; + } + + const source = page.computed; + if (source.type !== 'builtin:openapi') { + return; + } + + const { props } = source as ComputedContentSourceDocumentOpenAPI; + if (props.doc !== 'operation') { + return; + } + + return { + method: props.method, + deprecated: props.deprecated === true, + }; +} diff --git a/packages/react-openapi/src/OpenAPICodeSampleSelector.tsx b/packages/react-openapi/src/OpenAPICodeSampleSelector.tsx index d8b80a9f7..18292495c 100644 --- a/packages/react-openapi/src/OpenAPICodeSampleSelector.tsx +++ b/packages/react-openapi/src/OpenAPICodeSampleSelector.tsx @@ -33,7 +33,13 @@ function OpenAPICodeSampleHeader(props: { return ( <> - + {items.length > 1 ? ( + {short ? formatOpenAPIMethod(normalized) : method} + + ); +} diff --git a/packages/react-openapi/src/OpenAPIPath.tsx b/packages/react-openapi/src/OpenAPIPath.tsx index 53c31c6c6..e0a20ef03 100644 --- a/packages/react-openapi/src/OpenAPIPath.tsx +++ b/packages/react-openapi/src/OpenAPIPath.tsx @@ -16,6 +16,11 @@ export type OpenAPIPathProps = { * @default true */ canCopy?: boolean; + /** + * The size of the HTTP method badge. + * @default 'medium' + */ + methodBadgeSize?: 'small' | 'medium'; }; /** diff --git a/packages/react-openapi/src/OpenAPIPathItem.tsx b/packages/react-openapi/src/OpenAPIPathItem.tsx index 9d500bc0a..d597a4582 100644 --- a/packages/react-openapi/src/OpenAPIPathItem.tsx +++ b/packages/react-openapi/src/OpenAPIPathItem.tsx @@ -1,4 +1,5 @@ import { OpenAPICopyButton } from './OpenAPICopyButton'; +import { OpenAPIMethodBadge } from './OpenAPIMethodBadge'; import type { OpenAPIPathProps } from './OpenAPIPath'; import type { OpenAPIClientContext } from './context'; @@ -10,14 +11,22 @@ export function OpenAPIPathItem( context: OpenAPIClientContext; } ) { - const { value, canCopy = true, context, children, data, copyType = 'children' } = props; + const { + value, + canCopy = true, + context, + children, + data, + copyType = 'children', + methodBadgeSize = 'medium', + } = props; const { operation, method } = data; const title = {children}; return (
-
{method}
+ {canCopy && value ? ( copyType === 'children' ? ( ) : null} - + {!context.headless ? ( + + ) : null}
); diff --git a/packages/react-openapi/src/common/OpenAPISummary.tsx b/packages/react-openapi/src/common/OpenAPISummary.tsx index 8c81ae2b4..78e8b289d 100644 --- a/packages/react-openapi/src/common/OpenAPISummary.tsx +++ b/packages/react-openapi/src/common/OpenAPISummary.tsx @@ -23,7 +23,10 @@ export function OpenAPISummary(props: { })(); return ( -
+
{(operation.deprecated || operation['x-stability']) && (
{operation.deprecated &&
Deprecated
} @@ -32,7 +35,7 @@ export function OpenAPISummary(props: { )}
)} - {title + {!context.headless && title ? context.renderHeading({ deprecated: operation.deprecated ?? false, stability: operation['x-stability'], diff --git a/packages/react-openapi/src/context.ts b/packages/react-openapi/src/context.ts index b480fe32e..5933f6b46 100644 --- a/packages/react-openapi/src/context.ts +++ b/packages/react-openapi/src/context.ts @@ -72,6 +72,11 @@ export interface OpenAPIContext */ renderDocument: (props: { document: object }) => React.ReactNode; + /** + * If `true`, the block omits its own title and description (the page renders them instead). + */ + headless?: boolean; + /** * Public specification URL, used by Scalar's "Test it" modal. * When null, the "Test it" button is hidden. diff --git a/packages/react-openapi/src/formatOpenAPIMethod.ts b/packages/react-openapi/src/formatOpenAPIMethod.ts new file mode 100644 index 000000000..36b70055c --- /dev/null +++ b/packages/react-openapi/src/formatOpenAPIMethod.ts @@ -0,0 +1,14 @@ +/** + * Format an HTTP method for display in a method badge. + * Uppercasing is left to the `.openapi-method` CSS. + */ +export function formatOpenAPIMethod(method: string): string { + switch (method) { + case 'delete': + return 'DEL'; + case 'options': + return 'OPTS'; + default: + return method; + } +} diff --git a/packages/react-openapi/src/index.ts b/packages/react-openapi/src/index.ts index bc7a1648c..2a119dffb 100644 --- a/packages/react-openapi/src/index.ts +++ b/packages/react-openapi/src/index.ts @@ -1,4 +1,6 @@ export * from './schemas'; +export * from './formatOpenAPIMethod'; +export * from './OpenAPIMethodBadge'; export * from './OpenAPIOperation'; export * from './OpenAPIWebhook'; export * from './OpenAPIOperationContext';