From 1d648a9391f86ce26671e7ddccee0d39bb7dc615 Mon Sep 17 00:00:00 2001 From: taranvohra Date: Thu, 17 Sep 2026 20:42:32 +0530 Subject: [PATCH] Resolve repository page URLs within published sites --- .../DocumentView/BlockContentRef.tsx | 1 + .../components/DocumentView/InlineButton.tsx | 4 +- .../DocumentView/InlineLink/InlineLink.tsx | 7 +- .../src/components/DocumentView/Mention.tsx | 1 + .../DocumentView/Table/RecordCard.tsx | 4 +- .../DocumentView/Table/RecordColumnValue.tsx | 1 + packages/gitbook/src/lib/context.ts | 2 + packages/gitbook/src/lib/data/api.ts | 12 +- packages/gitbook/src/lib/data/types.ts | 1 + packages/gitbook/src/lib/gitPageURL.test.ts | 132 +++++++++++++++ packages/gitbook/src/lib/gitPageURL.ts | 117 +++++++++++++ packages/gitbook/src/lib/references.test.ts | 158 +++++++++++++++++- packages/gitbook/src/lib/references.tsx | 70 +++++++- 13 files changed, 501 insertions(+), 9 deletions(-) create mode 100644 packages/gitbook/src/lib/gitPageURL.test.ts create mode 100644 packages/gitbook/src/lib/gitPageURL.ts diff --git a/packages/gitbook/src/components/DocumentView/BlockContentRef.tsx b/packages/gitbook/src/components/DocumentView/BlockContentRef.tsx index cdbc07978..27d1a79c2 100644 --- a/packages/gitbook/src/components/DocumentView/BlockContentRef.tsx +++ b/packages/gitbook/src/components/DocumentView/BlockContentRef.tsx @@ -18,6 +18,7 @@ export async function BlockContentRef(props: BlockProps const resolved = context.contentContext ? await resolveContentRefInDocument(document, block.data.ref, context.contentContext, { + resolveGitPageURLs: true, resolveAnchorText: true, iconStyle: ['text-xl', 'text-tint'], }) diff --git a/packages/gitbook/src/components/DocumentView/InlineButton.tsx b/packages/gitbook/src/components/DocumentView/InlineButton.tsx index 40ba19c41..ae699232c 100644 --- a/packages/gitbook/src/components/DocumentView/InlineButton.tsx +++ b/packages/gitbook/src/components/DocumentView/InlineButton.tsx @@ -76,7 +76,9 @@ export async function InlineLinkButton( const resolved = context.contentContext && inline.data.ref - ? await resolveContentRefInDocument(document, inline.data.ref, context.contentContext) + ? await resolveContentRefInDocument(document, inline.data.ref, context.contentContext, { + resolveGitPageURLs: true, + }) : null; const href = diff --git a/packages/gitbook/src/components/DocumentView/InlineLink/InlineLink.tsx b/packages/gitbook/src/components/DocumentView/InlineLink/InlineLink.tsx index c9b8588ce..d53ad41e4 100644 --- a/packages/gitbook/src/components/DocumentView/InlineLink/InlineLink.tsx +++ b/packages/gitbook/src/components/DocumentView/InlineLink/InlineLink.tsx @@ -19,6 +19,7 @@ export async function InlineLink(props: InlineProps) { const resolved = context.contentContext ? await resolveContentRefInDocument(document, inline.data.ref, context.contentContext, { + resolveGitPageURLs: true, // We don't want to resolve the anchor text here, as it can be very expensive and will block rendering if there is a lot of anchors link. resolveAnchorText: false, }) @@ -52,8 +53,8 @@ export async function InlineLink(props: InlineProps) { const anchorElement = ( {inlinesElement} @@ -121,7 +122,7 @@ function InlineLinkTooltipWrapper(props: { let breadcrumbs = resolved.ancestors ?? []; const isMailto = resolved.href.startsWith('mailto:'); - const isExternal = inline.data.ref.kind === 'url'; + const isExternal = (resolved.resolvedRef ?? inline.data.ref).kind === 'url'; const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined; if (isMailto) { diff --git a/packages/gitbook/src/components/DocumentView/Mention.tsx b/packages/gitbook/src/components/DocumentView/Mention.tsx index 7802f6ccf..3979a7e71 100644 --- a/packages/gitbook/src/components/DocumentView/Mention.tsx +++ b/packages/gitbook/src/components/DocumentView/Mention.tsx @@ -9,6 +9,7 @@ export async function Mention(props: InlineProps) { const resolved = context.contentContext ? await resolveContentRefInDocument(document, inline.data.ref, context.contentContext, { + resolveGitPageURLs: true, resolveAnchorText: true, }) : null; diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx index ddedaf3bb..e3b1020de 100644 --- a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx +++ b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx @@ -41,7 +41,9 @@ export async function RecordCard( ? resolveContentRefInDocument(document, dark.contentRef, context.contentContext) : null, targetRef && context.contentContext - ? resolveContentRefInDocument(document, targetRef, context.contentContext) + ? resolveContentRefInDocument(document, targetRef, context.contentContext, { + resolveGitPageURLs: true, + }) : null, ]); diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx index 7c16b951a..e2a3f3a8b 100644 --- a/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx +++ b/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx @@ -253,6 +253,7 @@ export async function RecordColumnValue( const resolved = value && context.contentContext ? await resolveContentRefInDocument(document, value, context.contentContext, { + resolveGitPageURLs: true, resolveAnchorText: true, iconStyle: ['mr-2', 'text-tint-subtle'], }) diff --git a/packages/gitbook/src/lib/context.ts b/packages/gitbook/src/lib/context.ts index 97cd2adec..6e969f2ae 100644 --- a/packages/gitbook/src/lib/context.ts +++ b/packages/gitbook/src/lib/context.ts @@ -519,6 +519,7 @@ export async function fetchSpaceContextByIds( shareKey: string | undefined; changeRequest: string | undefined; revision: string | undefined; + revisionMetadata?: boolean; } ): Promise { const { dataFetcher } = baseContext; @@ -552,6 +553,7 @@ export async function fetchSpaceContextByIds( dataFetcher.getRevision({ spaceId: ids.space, revisionId, + ...(ids.revisionMetadata ? { metadata: true } : {}), }), // When trying to render a revision with an invalid / non-existing ID, diff --git a/packages/gitbook/src/lib/data/api.ts b/packages/gitbook/src/lib/data/api.ts index 1c96e6fb6..ebacb9bdf 100644 --- a/packages/gitbook/src/lib/data/api.ts +++ b/packages/gitbook/src/lib/data/api.ts @@ -80,6 +80,7 @@ export function createDataFetcher( return getRevision(input, { spaceId: params.spaceId, revisionId: params.revisionId, + metadata: params.metadata ?? false, }); }, getRevisionPageByPath(params) { @@ -319,8 +320,15 @@ const getChangeRequest = cache( // We don't use remote cache on vercel because of the 2Mb limit on cache size that makes some route crash const getRevision = cache( - async (input: DataFetcherInput, params: { spaceId: string; revisionId: string }) => { + async ( + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; metadata: boolean } + ) => { 'use cache'; + if (params.metadata) { + // Git paths can change without changing the content revision. + cacheTag(getCacheTag({ tag: 'space', space: params.spaceId })); + } return wrapDataFetcherError(async () => { return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => { const api = apiClient(input); @@ -328,7 +336,7 @@ const getRevision = cache( params.spaceId, params.revisionId, { - metadata: false, + metadata: params.metadata, }, { ...noCacheFetchOptions, diff --git a/packages/gitbook/src/lib/data/types.ts b/packages/gitbook/src/lib/data/types.ts index 62bc92405..203b3cbbd 100644 --- a/packages/gitbook/src/lib/data/types.ts +++ b/packages/gitbook/src/lib/data/types.ts @@ -79,6 +79,7 @@ export interface GitBookDataFetcher { getRevision(params: { spaceId: string; revisionId: string; + metadata?: boolean; }): Promise>; /** diff --git a/packages/gitbook/src/lib/gitPageURL.test.ts b/packages/gitbook/src/lib/gitPageURL.test.ts new file mode 100644 index 000000000..dcec7b89f --- /dev/null +++ b/packages/gitbook/src/lib/gitPageURL.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, it } from 'bun:test'; + +import { findGitPageURLTarget, findPageByGitPath } from './gitPageURL'; + +const SPACES = [ + { + id: 'a', + gitSync: { + url: 'https://github.com/acme/docs/tree/main', + installationProjectDirectory: 'guides', + }, + }, + { + id: 'b', + gitSync: { + url: 'https://github.com/acme/docs/tree/main', + installationProjectDirectory: '/api/', + }, + }, +]; + +describe('findGitPageURLTarget', () => { + it('matches the repository, ref and directory and preserves anchors', () => { + expect( + findGitPageURLTarget( + 'https://github.com/acme/docs/tree/main/api/auth.md#tokens', + SPACES + ) + ).toEqual({ space: 'b', path: 'api/auth.md', anchor: 'tokens' }); + }); + + it.each([ + 'https://github.com/other/docs/tree/main/api/auth.md', + 'https://github.com/acme/docs/tree/preview/api/auth.md', + 'https://github.com.evil.test/acme/docs/tree/main/api/auth.md', + 'https://github.com/acme/docs/tree/main/api-other/auth.md', + 'https://github.com/acme/docs/tree/main/api/auth.md?raw=1', + 'https://github.com/acme/docs/tree/main/api/%ZZ.md', + 'https://github.com/acme/docs/tree/main/api/%2Fsecret.md', + ])('does not reinterpret %s', (url) => { + expect(findGitPageURLTarget(url, SPACES)).toBeNull(); + }); + + it('supports blob URLs and encoded file names', () => { + expect( + findGitPageURLTarget( + 'https://github.com/acme/docs/blob/main/api/hello%20world.md', + SPACES + ) + ).toEqual({ space: 'b', path: 'api/hello world.md', anchor: undefined }); + }); + + it('matches self-hosted GitLab with nested groups and a slash in the branch', () => { + const spaces = [ + { + id: 'b', + gitSync: { + url: 'https://git.example.com/group/sub/docs/-/tree/release/v2', + installationProjectDirectory: 'api', + }, + }, + ]; + expect( + findGitPageURLTarget( + 'https://git.example.com/group/sub/docs/-/blob/release/v2/api/auth.md', + spaces + )?.space + ).toBe('b'); + }); + + it('rejects ambiguous owners and ambiguous branch prefixes', () => { + expect( + findGitPageURLTarget('https://github.com/acme/docs/tree/main/api/auth.md', [ + ...SPACES, + { ...SPACES[1]!, id: 'duplicate' }, + ]) + ).toBeNull(); + expect( + findGitPageURLTarget('https://github.com/acme/docs/tree/main/api/auth.md', [ + ...SPACES, + { + id: 'other-ref', + gitSync: { + url: 'https://github.com/acme/docs/tree/main/api', + installationProjectDirectory: '', + }, + }, + ]) + ).toBeNull(); + }); + + it('prefers the most specific directory and deduplicates site placements', () => { + expect( + findGitPageURLTarget('https://github.com/acme/docs/tree/main/api/auth.md', [ + ...SPACES, + SPACES[1]!, + { + id: 'root', + gitSync: { url: SPACES[0]!.gitSync.url, installationProjectDirectory: '' }, + }, + ])?.space + ).toBe('b'); + }); + + it('requires directory metadata, including an explicit empty root directory', () => { + expect( + findGitPageURLTarget('https://github.com/acme/docs/tree/main/api/auth.md', [ + { id: 'old', gitSync: { url: SPACES[0]!.gitSync.url } }, + ]) + ).toBeNull(); + }); +}); + +describe('findPageByGitPath', () => { + const pages = [ + { id: 'auth', git: { path: 'api/auth.md' }, pages: [] }, + { id: 'group', pages: [{ id: 'readme', git: { path: 'api/11.8/README.md' }, pages: [] }] }, + ]; + it('finds nested pages and directory README links', () => { + expect(findPageByGitPath(pages, 'api/auth.md')?.id).toBe('auth'); + expect(findPageByGitPath(pages, 'api/11.8/')?.id).toBe('readme'); + expect(findPageByGitPath(pages, 'api/missing.md')).toBeNull(); + }); + it('does not select between duplicate paths', () => { + expect( + findPageByGitPath( + [...pages, { id: 'copy', git: { path: 'api/auth.md' }, pages: [] }], + 'api/auth.md' + ) + ).toBeNull(); + }); +}); diff --git a/packages/gitbook/src/lib/gitPageURL.ts b/packages/gitbook/src/lib/gitPageURL.ts new file mode 100644 index 000000000..ff13b9d2f --- /dev/null +++ b/packages/gitbook/src/lib/gitPageURL.ts @@ -0,0 +1,117 @@ +export interface GitPageURLSpace { + id: string; + gitSync?: { + url?: string; + installationProjectDirectory?: string; + }; +} + +export interface GitPageURLTarget { + space: string; + path: string; + anchor?: string; +} + +/** Locate a unique owning space without fetching any revisions. */ +export function findGitPageURLTarget( + href: string, + spaces: readonly GitPageURLSpace[] +): GitPageURLTarget | null { + const url = parseURL(href); + if (!url || url.search) { + return null; + } + + const matches = new Map(); + for (const space of spaces) { + const { url: treeURL, installationProjectDirectory } = space.gitSync ?? {}; + if (!treeURL || installationProjectDirectory === undefined) { + continue; + } + const tree = parseURL(treeURL); + if (!tree || tree.host !== url.host) { + continue; + } + const prefix = tree.pathname.replace(/\/$/, ''); + const blobPrefix = prefix + .replace('/-/tree/', '/-/blob/') + .replace(/^(\/[^/]+\/[^/]+)\/tree\//, '$1/blob/'); + const matchedPrefix = [prefix, blobPrefix].find((candidate) => + url.pathname.startsWith(`${candidate}/`) + ); + if (!matchedPrefix) { + continue; + } + + let filePath: string; + try { + const encoded = url.pathname.slice(matchedPrefix.length + 1); + // Encoded separators make repository/ref boundaries ambiguous. + if (/%2f|%5c/i.test(encoded)) { + continue; + } + filePath = decodeURIComponent(encoded); + } catch { + continue; + } + const root = installationProjectDirectory.replace(/^\.\//, '').replace(/^\/+|\/+$/g, ''); + if ( + filePath.split('/').some((part) => part === '.' || part === '..') || + filePath.includes('\\') + ) { + continue; + } + if (root && filePath !== root && !filePath.startsWith(`${root}/`)) { + continue; + } + matches.set(space.id, { + space: space.id, + path: filePath, + anchor: url.hash.slice(1) || undefined, + root, + tree: `${tree.host}${prefix}`, + }); + } + + const candidates = [...matches.values()]; + if (new Set(candidates.map((candidate) => candidate.tree)).size !== 1) { + return null; + } + const longestRoot = Math.max(...candidates.map((candidate) => candidate.root.length)); + const owners = candidates.filter((candidate) => candidate.root.length === longestRoot); + if (owners.length !== 1) { + return null; + } + const owner = owners[0]!; + return { space: owner.space, path: owner.path, anchor: owner.anchor }; +} + +/** Match an API revision's nested page tree, including directory README links. */ +export function findPageByGitPath( + pages: readonly T[], + filePath: string +): T | null { + const paths = [filePath, `${filePath.replace(/\/$/, '')}/README.md`]; + const matches: T[] = []; + const visit = (children: readonly T[]) => { + for (const page of children) { + if (page.git && paths.includes(page.git.path)) { + matches.push(page); + } + visit(page.pages ?? []); + } + }; + visit(pages); + return matches.length === 1 ? matches[0]! : null; +} + +function parseURL(href: string): URL | null { + try { + const url = new URL(href); + return ['https:', 'http:'].includes(url.protocol) && !url.username && !url.password + ? url + : null; + } catch { + return null; + } +} diff --git a/packages/gitbook/src/lib/references.test.ts b/packages/gitbook/src/lib/references.test.ts index 4342f5a78..7befe167d 100644 --- a/packages/gitbook/src/lib/references.test.ts +++ b/packages/gitbook/src/lib/references.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'bun:test'; +import { describe, expect, it, mock } from 'bun:test'; import type { Revision, RevisionPageDocument, SiteSpace, Space } from '@gitbook/api'; @@ -738,3 +738,159 @@ describe('resolveContentRef for direct space links', () => { ]); }); }); + +describe('repository page links', () => { + function fixture(options: { denied?: boolean; missing?: boolean; draft?: boolean } = {}) { + const page = { + id: 'target-page', + type: 'document', + title: 'Authentication', + path: 'authentication', + slug: 'authentication', + pages: [], + git: { path: 'api/auth.md', oid: 'blob' }, + } as unknown as RevisionPageDocument; + const targetSpace = { + id: 'target', + title: 'API', + organization: 'org', + revision: 'target-main', + gitSync: { + url: 'https://github.com/acme/docs/tree/main', + installationProjectDirectory: 'api', + }, + urls: { + app: 'https://app.gitbook.com/s/target', + published: 'https://docs.example.com/api/', + }, + } as unknown as Space; + const targetSiteSpace = { + id: 'site-target', + title: 'API', + space: targetSpace, + path: 'api', + draft: options.draft ?? false, + urls: { published: 'https://docs.example.com/api/' }, + } as unknown as SiteSpace; + const getSpace = mock(async () => + options.denied ? { error: { code: 403, message: 'Forbidden' } } : { data: targetSpace } + ); + const getRevision = mock(async () => ({ + data: { + id: 'target-main', + pages: options.missing + ? [] + : [{ ...page, id: 'home', path: '', slug: '', git: undefined }, page], + files: [], + reusableContents: [], + }, + })); + const context = { + organizationId: 'org', + site: { id: 'site' }, + space: { id: 'source', revision: 'source-main' }, + revision: { pages: [] }, + revisionId: 'source-main', + changeRequest: null, + structure: { type: 'siteSpaces', structure: [targetSiteSpace] }, + linker: createLinker({ + host: 'docs.example.com', + siteBasePath: '/', + spaceBasePath: '/source/', + }), + dataFetcher: { getSpace, getRevision }, + } as unknown as GitBookAnyContext; + return { context, getSpace, getRevision }; + } + + const ref = { + kind: 'url' as const, + url: 'https://github.com/acme/docs/tree/main/api/auth.md#tokens', + }; + const options = { resolveGitPageURLs: true }; + + it('renders a matching repository URL as a site page link with its anchor', async () => { + const { context, getRevision } = fixture(); + const result = await resolveContentRef(ref, context, options); + expect(result?.href).toBe('/api/authentication#tokens'); + expect(result?.text).toBe('Authentication'); + expect(result?.ancestors?.[0]?.label).toBe('API'); + expect(result?.resolvedRef).toEqual({ + kind: 'anchor', + space: 'target', + page: 'target-page', + anchor: 'tokens', + }); + expect(getRevision).toHaveBeenCalledWith({ + spaceId: 'target', + revisionId: 'target-main', + metadata: true, + }); + expect(ref.kind).toBe('url'); + }); + + it('keeps media/resource URL resolution unchanged unless enabled', async () => { + const { context, getSpace } = fixture(); + expect((await resolveContentRef(ref, context))?.href).toBe(ref.url); + expect(getSpace).not.toHaveBeenCalled(); + }); + + it('reads only the matching space in a 500-space site', async () => { + const { context, getSpace, getRevision } = fixture(); + if (!('site' in context) || context.structure.type !== 'siteSpaces') { + throw new Error('Expected a site fixture'); + } + const target = context.structure.structure[0]!; + context.structure.structure.push( + ...Array.from({ length: 499 }, (_, index) => ({ + ...target, + id: `site-${index}`, + space: { + ...target.space, + id: `space-${index}`, + gitSync: { + ...target.space.gitSync!, + installationProjectDirectory: `other-${index}`, + }, + }, + })) + ); + expect((await resolveContentRef(ref, context, options))?.href).toBe( + '/api/authentication#tokens' + ); + expect(getSpace).toHaveBeenCalledTimes(1); + expect(getRevision).toHaveBeenCalledTimes(1); + }); + + it.each([{ denied: true }, { missing: true }, { draft: true }])( + 'preserves the fallback for unavailable content: %j', + async (state) => { + const { context } = fixture(state); + const result = await resolveContentRef(ref, context, options); + expect(result).toEqual({ href: ref.url, text: ref.url, active: false }); + } + ); + + it('does not fetch revisions for a different repository or branch', async () => { + const { context, getRevision } = fixture(); + for (const url of [ + ref.url.replace('/main/', '/preview/'), + ref.url.replace('/acme/', '/other/'), + ]) { + expect((await resolveContentRef({ kind: 'url', url }, context, options))?.href).toBe( + url + ); + } + expect(getRevision).not.toHaveBeenCalled(); + }); + + it('resolves the unchanged stored URL when the target becomes available', async () => { + const state = { missing: true }; + const { context } = fixture(state); + expect((await resolveContentRef(ref, context, options))?.href).toBe(ref.url); + state.missing = false; + expect((await resolveContentRef(ref, context, options))?.href).toBe( + '/api/authentication#tokens' + ); + }); +}); diff --git a/packages/gitbook/src/lib/references.tsx b/packages/gitbook/src/lib/references.tsx index aede45c39..10ff273a1 100644 --- a/packages/gitbook/src/lib/references.tsx +++ b/packages/gitbook/src/lib/references.tsx @@ -16,12 +16,14 @@ import type { Filesystem } from '@gitbook/openapi-parser'; import { getGitBookAppHref } from './app'; import { getBlockById, getBlockTitle } from './document'; +import { findGitPageURLTarget, findPageByGitPath } from './gitPageURL'; import { resolvePageId } from './pages'; import { findSiteSpaceBy, getFallbackSiteSpacePath, getLinkerForSiteSpace, getLocalizedTitle, + listAllSiteSpaces, } from './sites'; import { getRevisionTags, resolveTag } from './tags'; import type { ClassValue } from './tailwind'; @@ -43,6 +45,8 @@ import { import { type GitBookLinker, createLinker, linkerWithAbsoluteURLs } from '@/lib/links'; export interface ResolvedContentRef { + /** Effective destination when a repository URL resolves to a site page. */ + resolvedRef?: ContentRef; /** Text to render in the content ref */ text: string; /** Additional sub text to render in the content ref */ @@ -80,6 +84,8 @@ export interface ResolvedContentRef { } export interface ResolveContentRefOptions { + /** Enable repository-page lookup for navigational links. */ + resolveGitPageURLs?: boolean; /** * Should the content ref be rendered as text. * @default false @@ -143,6 +149,66 @@ export async function resolveContentRef( switch (contentRef.kind) { case 'url': { + if (options.resolveGitPageURLs && 'site' in context) { + const target = findGitPageURLTarget( + contentRef.url, + listAllSiteSpaces(context.structure) + .filter((entry) => !entry.draft) + .map((entry) => entry.space) + ); + if (target) { + try { + // Site CRs must select the target member's revision here instead of main. + const targetContext = await createContextForSpace( + target.space, + context, + true + ); + const page = + targetContext && + findPageByGitPath( + targetContext.spaceContext.revision.pages, + target.path + ); + if (page?.type === 'document' && targetContext) { + const resolvedRef: ContentRef = target.anchor + ? { + kind: 'anchor', + space: target.space, + page: page.id, + anchor: target.anchor, + } + : { kind: 'page', space: target.space, page: page.id }; + const resolved = await resolveContentRef( + resolvedRef, + targetContext.spaceContext, + options + ); + if (resolved) { + const foundSiteSpace = findSiteSpaceBy( + context.structure, + (entry) => entry.space.id === target.space + ); + return { + ...resolved, + resolvedRef, + ancestors: [ + ...resolvePageAncestors( + context, + resolvedRef, + foundSiteSpace, + targetContext + ), + ...(resolved.ancestors ?? []), + ], + }; + } + } + } catch { + // An unavailable or forbidden target must not prevent rendering the source page. + } + } + } return { href: contentRef.url, text: contentRef.url, @@ -627,7 +693,8 @@ async function resolveContentRefInSpace( */ async function createContextForSpace( spaceId: string, - context: GitBookAnyContext + context: GitBookAnyContext, + revisionMetadata = false ): Promise<{ spaceContext: GitBookSpaceContext; baseURL: URL; @@ -639,6 +706,7 @@ async function createContextForSpace( shareKey: context?.shareKey, changeRequest: undefined, revision: undefined, + revisionMetadata, }) ), getBestTargetSpace(context, spaceId),