From bdc6cd9aba3f7ec97c1315f01481841455d725a6 Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:21:14 +0100 Subject: [PATCH] Use OpenAPI publicURL as Scalar specUrl (#4093) --- .changeset/eleven-loops-pull.md | 6 +++++ .../DocumentView/OpenAPI/OpenAPIOperation.tsx | 8 +++++-- .../DocumentView/OpenAPI/OpenAPISchemas.tsx | 8 +++++-- .../DocumentView/OpenAPI/OpenAPIWebhook.tsx | 8 +++++-- .../DocumentView/OpenAPI/context.tsx | 2 +- packages/gitbook/src/lib/openapi/fetch.ts | 22 +++++++++++++++---- .../openapi/resolveOpenAPIOperationBlock.ts | 8 +++---- .../lib/openapi/resolveOpenAPISchemasBlock.ts | 8 +++---- .../lib/openapi/resolveOpenAPIWebhookBlock.ts | 8 +++---- packages/gitbook/src/lib/openapi/types.ts | 6 ++++- packages/gitbook/src/lib/references.tsx | 19 ++++++++++------ .../react-openapi/src/OpenAPICodeSample.tsx | 2 +- packages/react-openapi/src/context.ts | 5 +++-- 13 files changed, 76 insertions(+), 34 deletions(-) create mode 100644 .changeset/eleven-loops-pull.md diff --git a/.changeset/eleven-loops-pull.md b/.changeset/eleven-loops-pull.md new file mode 100644 index 000000000..8926b1a9e --- /dev/null +++ b/.changeset/eleven-loops-pull.md @@ -0,0 +1,6 @@ +--- +"@gitbook/react-openapi": patch +"gitbook": patch +--- + +Use OpenAPI publicURL as Scalar specUrl diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIOperation.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIOperation.tsx index 04966122c..7694f94dc 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIOperation.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIOperation.tsx @@ -26,7 +26,7 @@ async function OpenAPIOperationBody(props: BlockProps return null; } - const { data, specUrl, error } = await resolveOpenAPIOperationBlock({ + const { data, specUrl, publicURL, error } = await resolveOpenAPIOperationBlock({ block, context: context.contentContext, }); @@ -46,7 +46,11 @@ async function OpenAPIOperationBody(props: BlockProps return ( ); diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPISchemas.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPISchemas.tsx index 0d5034dda..ec90eead5 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPISchemas.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPISchemas.tsx @@ -25,7 +25,7 @@ async function OpenAPISchemasBody(props: BlockProps) { return null; } - const { data, specUrl, error } = await resolveOpenAPISchemasBlock({ + const { data, specUrl, publicURL, error } = await resolveOpenAPISchemasBlock({ block, context: context.contentContext, }); @@ -48,7 +48,11 @@ async function OpenAPISchemasBody(props: BlockProps) { ); diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIWebhook.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIWebhook.tsx index ba61b6bd2..3225ee1da 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIWebhook.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIWebhook.tsx @@ -26,7 +26,7 @@ async function OpenAPIWebhookBody(props: BlockProps) { return null; } - const { data, specUrl, error } = await resolveOpenAPIWebhookBlock({ + const { data, specUrl, publicURL, error } = await resolveOpenAPIWebhookBlock({ block, context: context.contentContext, }); @@ -48,7 +48,11 @@ async function OpenAPIWebhookBody(props: BlockProps) { return ( ); diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx index 4a96be843..489d1004d 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx @@ -24,7 +24,7 @@ import type { */ export function getOpenAPIContext(args: { props: BlockProps; - specUrl: string; + specUrl: string | null; context: GitBookAnyContext | undefined; }): OpenAPIContextInput { const { props, specUrl, context } = args; diff --git a/packages/gitbook/src/lib/openapi/fetch.ts b/packages/gitbook/src/lib/openapi/fetch.ts index 366584e07..46b3e46ec 100644 --- a/packages/gitbook/src/lib/openapi/fetch.ts +++ b/packages/gitbook/src/lib/openapi/fetch.ts @@ -33,14 +33,14 @@ export async function fetchOpenAPIFilesystem( const resolved = ref ? await resolveContentRef(ref, context) : null; if (!resolved) { - return { filesystem: null, specUrl: null }; + return { filesystem: null, specUrl: null, publicURL: null }; } const result = await (() => { // If the reference is a new OpenAPI reference, we return it. if (ref.kind === 'openapi') { - assert(resolved.openAPIFilesystem); - return resolved.openAPIFilesystem; + assert(resolved.openapi?.filesystem); + return resolved.openapi.filesystem; } // For legacy blocks ("swagger"), we need to fetch the file system. return fetchFilesystem(resolved.href, context.space.id); @@ -50,7 +50,21 @@ export async function fetchOpenAPIFilesystem( throw new OpenAPIParseError(result.error.message, { code: result.error.code }); } - return { filesystem: result, specUrl: resolved.href }; + const publicURL = (() => { + // For new OpenAPI refs, use the explicit publicURL (null when spec is private). + if (ref.kind === 'openapi' && resolved.openapi) { + return resolved.openapi.publicURL; + } + + // For legacy "swagger" refs, the href is the public spec URL itself. + return resolved.href; + })(); + + return { + filesystem: result, + specUrl: resolved.href, + publicURL, + }; } /** diff --git a/packages/gitbook/src/lib/openapi/resolveOpenAPIOperationBlock.ts b/packages/gitbook/src/lib/openapi/resolveOpenAPIOperationBlock.ts index e508a634c..3764de89e 100644 --- a/packages/gitbook/src/lib/openapi/resolveOpenAPIOperationBlock.ts +++ b/packages/gitbook/src/lib/openapi/resolveOpenAPIOperationBlock.ts @@ -37,14 +37,14 @@ async function resolveOpenAPIOperationBlockNoCache( const { context, block } = args; if (!block.data.path || !block.data.method) { - return { data: null, specUrl: null }; + return { data: null, specUrl: null, publicURL: null }; } try { - const { filesystem, specUrl } = await fetchOpenAPIFilesystem({ block, context }); + const { filesystem, specUrl, publicURL } = await fetchOpenAPIFilesystem({ block, context }); if (!filesystem) { - return { data: null, specUrl: null }; + return { data: null, specUrl: null, publicURL: null }; } const data = await resolveOpenAPIOperation(filesystem, { @@ -52,7 +52,7 @@ async function resolveOpenAPIOperationBlockNoCache( method: block.data.method, }); - return { data, specUrl }; + return { data, specUrl, publicURL }; } catch (error) { if (error instanceof OpenAPIParseError) { return { error }; diff --git a/packages/gitbook/src/lib/openapi/resolveOpenAPISchemasBlock.ts b/packages/gitbook/src/lib/openapi/resolveOpenAPISchemasBlock.ts index 82c10d60b..1e3dd1131 100644 --- a/packages/gitbook/src/lib/openapi/resolveOpenAPISchemasBlock.ts +++ b/packages/gitbook/src/lib/openapi/resolveOpenAPISchemasBlock.ts @@ -37,21 +37,21 @@ async function baseResolveOpenAPISchemasBlock( ): Promise { const { context, block } = args; if (!block.data.schemas || !block.data.schemas.length) { - return { data: null, specUrl: null }; + return { data: null, specUrl: null, publicURL: null }; } try { - const { filesystem, specUrl } = await fetchOpenAPIFilesystem({ block, context }); + const { filesystem, specUrl, publicURL } = await fetchOpenAPIFilesystem({ block, context }); if (!filesystem || !specUrl) { - return { data: null, specUrl: null }; + return { data: null, specUrl: null, publicURL: null }; } const data = await resolveOpenAPISchemas(filesystem, { schemas: block.data.schemas, }); - return { data, specUrl }; + return { data, specUrl, publicURL }; } catch (error) { if (error instanceof OpenAPIParseError) { return { error }; diff --git a/packages/gitbook/src/lib/openapi/resolveOpenAPIWebhookBlock.ts b/packages/gitbook/src/lib/openapi/resolveOpenAPIWebhookBlock.ts index 817e080c3..691cca850 100644 --- a/packages/gitbook/src/lib/openapi/resolveOpenAPIWebhookBlock.ts +++ b/packages/gitbook/src/lib/openapi/resolveOpenAPIWebhookBlock.ts @@ -35,14 +35,14 @@ async function baseResolveOpenAPIWebhookBlock( ): Promise { const { context, block } = args; if (!block.data.name || !block.data.method) { - return { data: null, specUrl: null }; + return { data: null, specUrl: null, publicURL: null }; } try { - const { filesystem, specUrl } = await fetchOpenAPIFilesystem({ block, context }); + const { filesystem, specUrl, publicURL } = await fetchOpenAPIFilesystem({ block, context }); if (!filesystem) { - return { data: null, specUrl: null }; + return { data: null, specUrl: null, publicURL: null }; } const data = await resolveOpenAPIWebhook(filesystem, { @@ -50,7 +50,7 @@ async function baseResolveOpenAPIWebhookBlock( method: block.data.method, }); - return { data, specUrl }; + return { data, specUrl, publicURL }; } catch (error) { if (error instanceof OpenAPIParseError) { return { error }; diff --git a/packages/gitbook/src/lib/openapi/types.ts b/packages/gitbook/src/lib/openapi/types.ts index a712b0b32..c364b050c 100644 --- a/packages/gitbook/src/lib/openapi/types.ts +++ b/packages/gitbook/src/lib/openapi/types.ts @@ -38,6 +38,8 @@ export type FetchOpenAPIFilesystemResult = error?: undefined; filesystem: Filesystem | null; specUrl: string | null; + /** Public URL of the OpenAPI spec, used as the spec URL for Scalar's "Test it" modal */ + publicURL: string | null; } | FetchOpenAPIFilesystemError; @@ -48,13 +50,14 @@ type FetchOpenAPIFilesystemError = { error: OpenAPIParseError; filesystem?: undefined; specUrl?: undefined; + publicURL?: undefined; }; /** * Resolved OpenAPI block result. */ export type ResolveOpenAPIBlockResult = - | { error?: undefined; data: T | null; specUrl: string | null } + | { error?: undefined; data: T | null; specUrl: string | null; publicURL: string | null } | ResolveOpenAPIBlockError; /** @@ -64,4 +67,5 @@ type ResolveOpenAPIBlockError = { error: OpenAPIParseError; data?: undefined; specUrl?: undefined; + publicURL?: undefined; }; diff --git a/packages/gitbook/src/lib/references.tsx b/packages/gitbook/src/lib/references.tsx index 4286bec8d..d7a9728c8 100644 --- a/packages/gitbook/src/lib/references.tsx +++ b/packages/gitbook/src/lib/references.tsx @@ -58,12 +58,17 @@ export interface ResolvedContentRef { context: GitBookSpaceContext; revisionReusableContent: RevisionReusableContent; }; - /** Resolve OpenAPI spec filesystem. */ - openAPIFilesystem?: Filesystem; /** * Space that the content ref belongs to (if applicable). */ space?: Space; + /** Resolved OpenAPI spec, if the reference is an OpenAPI spec. */ + openapi?: { + /** OpenAPI spec filesystem. */ + filesystem: Filesystem; + /** Public URL of the OpenAPI spec */ + publicURL: string | null; + }; } export interface ResolveContentRefOptions { @@ -340,13 +345,13 @@ export async function resolveContentRef( return null; } return { - // @ts-expect-error - Backward compatibility: `urls.source` replaces `url` in the next API version - href: openAPISpecVersionContent.urls?.source ?? openAPISpecVersionContent.url, + href: openAPISpecVersionContent.urls.source, text: contentRef.spec, active: false, - openAPIFilesystem: openAPISpecVersionContent.filesystem as Filesystem, - // @ts-expect-error - Public URL is not yet supported in the API - publicURL: openAPISpecVersionContent.urls?.public, + openapi: { + filesystem: openAPISpecVersionContent.filesystem as Filesystem, + publicURL: openAPISpecVersionContent.urls.public, + }, }; } diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index 2eb0f1e50..8b35688f2 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -245,7 +245,7 @@ function OpenAPICodeSampleFooter(props: { ) : ( )} - {!hideTryItPanel && hasValidHost && ( + {!hideTryItPanel && hasValidHost && specUrl && ( React.ReactNode; /** - * Specification URL. + * Public specification URL, used by Scalar's "Test it" modal. + * When null, the "Test it" button is hidden. */ - specUrl: string; + specUrl: string | null; /** * Build a signed proxy URL that restricts the proxy to specific origins.