diff --git a/.changeset/polite-falcons-agree.md b/.changeset/polite-falcons-agree.md
new file mode 100644
index 0000000000..7743ab7095
--- /dev/null
+++ b/.changeset/polite-falcons-agree.md
@@ -0,0 +1,6 @@
+---
+"gitbook-v2": minor
+"gitbook": minor
+---
+
+Add support for reusable content across spaces.
diff --git a/bun.lock b/bun.lock
index 5476639847..ff44480802 100644
--- a/bun.lock
+++ b/bun.lock
@@ -259,7 +259,7 @@
},
"overrides": {
"@codemirror/state": "6.4.1",
- "@gitbook/api": "0.111.0",
+ "@gitbook/api": "0.113.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
},
@@ -624,7 +624,7 @@
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
- "@gitbook/api": ["@gitbook/api@0.111.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-E5Pk28kPD4p6XNWdwFM9pgDijdByseIZQqcFK+/hoW5tEZa5Yw/plRKJyN1hmwfPL6SKq6Maf0fbIzTQiVXyQQ=="],
+ "@gitbook/api": ["@gitbook/api@0.113.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-PWMeAkdm4bHSl3b5OmtcmskZ6qRkkDhauCPybo8sGnjS03O14YAUtubAQiNCKX/uwbs+yiQ8KRPyeIwn+g42yw=="],
"@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
diff --git a/package.json b/package.json
index fba8d79576..00f323e2f4 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
"packageManager": "bun@1.2.8",
"overrides": {
"@codemirror/state": "6.4.1",
- "@gitbook/api": "0.111.0",
+ "@gitbook/api": "0.113.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
diff --git a/packages/gitbook/src/components/DocumentView/Integration/contentkit.tsx b/packages/gitbook/src/components/DocumentView/Integration/contentkit.tsx
index fb692c6824..155077ca25 100644
--- a/packages/gitbook/src/components/DocumentView/Integration/contentkit.tsx
+++ b/packages/gitbook/src/components/DocumentView/Integration/contentkit.tsx
@@ -20,6 +20,8 @@ export const contentKitServerContext: ContentKitServerContext = {
'link-external': (props) => ,
eye: (props) => ,
lock: (props) => ,
+ check: (props) => ,
+ 'check-circle': (props) => ,
},
codeBlock: (props) => {
return ;
diff --git a/packages/gitbook/src/components/DocumentView/ReusableContent.tsx b/packages/gitbook/src/components/DocumentView/ReusableContent.tsx
index e95ffc7545..7e2d28d8d2 100644
--- a/packages/gitbook/src/components/DocumentView/ReusableContent.tsx
+++ b/packages/gitbook/src/components/DocumentView/ReusableContent.tsx
@@ -13,15 +13,28 @@ export async function ReusableContent(props: BlockProps
face.weight === 400 || face.weight === 700
diff --git a/packages/gitbook/src/lib/api.ts b/packages/gitbook/src/lib/api.ts
index 2bb2039bc3..ca3ce3b1f8 100644
--- a/packages/gitbook/src/lib/api.ts
+++ b/packages/gitbook/src/lib/api.ts
@@ -267,6 +267,7 @@ export const getPublishedContentByUrl = cache({
const parsed = parseCacheResponse(response);
+ // biome-ignore lint/suspicious/noConsole: log the ttl of the token
console.log(
`Parsed ttl: ${parsed.ttl} at ${Date.now()}, for ${'apiToken' in response.data ? response.data.apiToken : ''}`
);
diff --git a/packages/gitbook/src/lib/references.tsx b/packages/gitbook/src/lib/references.tsx
index ab941e43c5..2ef8c48a6a 100644
--- a/packages/gitbook/src/lib/references.tsx
+++ b/packages/gitbook/src/lib/references.tsx
@@ -41,8 +41,12 @@ export interface ResolvedContentRef {
file?: RevisionFile;
/** Page document resolved from the content ref */
page?: RevisionPageDocument;
- /** Resolved reusable content, if the ref points to reusable content on a revision. */
- reusableContent?: RevisionReusableContent;
+ /** Resolved reusable content, if the ref points to reusable content on a revision. Also contains the space and revision used for resolution. */
+ reusableContent?: {
+ revisionReusableContent: RevisionReusableContent;
+ space: string;
+ revision: string;
+ };
/** Resolve OpenAPI spec filesystem. */
openAPIFilesystem?: Filesystem;
}
@@ -231,21 +235,52 @@ export async function resolveContentRef(
}
case 'reusable-content': {
+ // Figure out which space and revision the reusable content is in.
+ const container: { space: string; revision: string } | null = await (async () => {
+ // without a space on the content ref, or if the space is the same as the current one, we can use the current revision.
+ if (!contentRef.space || contentRef.space === context.space.id) {
+ return { space: context.space.id, revision: revisionId };
+ }
+
+ const space = await getDataOrNull(
+ dataFetcher.getSpace({
+ spaceId: contentRef.space,
+ shareKey: undefined,
+ })
+ );
+
+ if (!space) {
+ return null;
+ }
+
+ return { space: space.id, revision: space.revision };
+ })();
+
+ if (!container) {
+ return null;
+ }
+
const reusableContent = await getDataOrNull(
dataFetcher.getReusableContent({
- spaceId: space.id,
- revisionId,
+ spaceId: container.space,
+ revisionId: container.revision,
reusableContentId: contentRef.reusableContent,
})
);
+
if (!reusableContent) {
return null;
}
+
return {
- href: getGitBookAppHref(`/s/${space.id}`),
+ href: getGitBookAppHref(`/s/${container.space}/~/reusable/${reusableContent.id}`),
text: reusableContent.title,
active: false,
- reusableContent,
+ reusableContent: {
+ revisionReusableContent: reusableContent,
+ space: container.space,
+ revision: container.revision,
+ },
};
}
diff --git a/packages/gitbook/src/lib/v1.ts b/packages/gitbook/src/lib/v1.ts
index ea5219816b..eb2accdc24 100644
--- a/packages/gitbook/src/lib/v1.ts
+++ b/packages/gitbook/src/lib/v1.ts
@@ -8,6 +8,7 @@ import type { GitBookDataFetcher } from '@v2/lib/data/types';
import { createImageResizer } from '@v2/lib/images';
import { createLinker } from '@v2/lib/links';
+import { GitBookAPI } from '@gitbook/api';
import { DataFetcherError, wrapDataFetcherError } from '@v2/lib/data';
import { headers } from 'next/headers';
import {
@@ -30,6 +31,7 @@ import {
getUserById,
renderIntegrationUi,
searchSiteContent,
+ withAPI as withAPIV1,
} from './api';
import { getDynamicCustomizationSettings } from './customization';
import { withLeadingSlash, withTrailingSlash } from './paths';
@@ -58,7 +60,7 @@ export async function getV1BaseContext(): Promise {
return url;
};
- const dataFetcher = await getDataFetcherV1();
+ const dataFetcher = getDataFetcherV1();
const imageResizer = createImageResizer({
imagesContextId: host,
@@ -82,77 +84,121 @@ export async function getV1BaseContext(): Promise {
* Try not to use this as much as possible, and instead take the data fetcher from the props.
* This data fetcher should only be used at the top of the tree.
*/
-async function getDataFetcherV1(): Promise {
+function getDataFetcherV1(apiTokenOverride?: string): GitBookDataFetcher {
+ let apiClient: GitBookAPI | undefined;
+
+ /**
+ * Run a function with the correct API client. If an API token is provided, we
+ * create a new API client with the token. Otherwise, we use the default API client.
+ */
+ async function withAPI(fn: () => Promise): Promise {
+ // No token override - we can use the default API client.
+ if (!apiTokenOverride) {
+ return fn();
+ }
+
+ const client = await api();
+
+ if (!apiClient) {
+ // New client uses same endpoint and user agent as the default client.
+ apiClient = new GitBookAPI({
+ endpoint: client.client.endpoint,
+ authToken: apiTokenOverride,
+ userAgent: client.client.userAgent,
+ });
+ }
+
+ return withAPIV1(
+ {
+ client: apiClient,
+ contextId: client.contextId,
+ },
+ fn
+ );
+ }
+
const dataFetcher: GitBookDataFetcher = {
async api() {
- const result = await api();
- return result.client;
+ return withAPI(async () => {
+ const result = await api();
+ return result.client;
+ });
},
- withToken() {
- // In v1, the token is global and controlled by the middleware.
- // We don't need to do anything special here.
- return dataFetcher;
+ withToken({ apiToken }) {
+ return getDataFetcherV1(apiToken);
},
getUserById(userId) {
- return wrapDataFetcherError(async () => {
- const user = await getUserById(userId);
- if (!user) {
- throw new DataFetcherError('User not found', 404);
- }
-
- return user;
- });
+ return withAPI(() =>
+ wrapDataFetcherError(async () => {
+ const user = await getUserById(userId);
+ if (!user) {
+ throw new DataFetcherError('User not found', 404);
+ }
+
+ return user;
+ })
+ );
},
getPublishedContentSite(params) {
- return wrapDataFetcherError(async () => {
- return getPublishedContentSite(params);
- });
+ return withAPI(() =>
+ wrapDataFetcherError(async () => {
+ return getPublishedContentSite(params);
+ })
+ );
},
getSpace(params) {
- return wrapDataFetcherError(async () => {
- return getSpace(params.spaceId, params.shareKey);
- });
+ return withAPI(() =>
+ wrapDataFetcherError(async () => {
+ return getSpace(params.spaceId, params.shareKey);
+ })
+ );
},
getChangeRequest(params) {
- return wrapDataFetcherError(async () => {
- const changeRequest = await getChangeRequest(
- params.spaceId,
- params.changeRequestId
- );
- if (!changeRequest) {
- throw new DataFetcherError('Change request not found', 404);
- }
-
- return changeRequest;
- });
+ return withAPI(() =>
+ wrapDataFetcherError(async () => {
+ const changeRequest = await getChangeRequest(
+ params.spaceId,
+ params.changeRequestId
+ );
+ if (!changeRequest) {
+ throw new DataFetcherError('Change request not found', 404);
+ }
+
+ return changeRequest;
+ })
+ );
},
getRevision(params) {
- return wrapDataFetcherError(async () => {
- return getRevision(params.spaceId, params.revisionId, {
- metadata: params.metadata,
- });
- });
+ return withAPI(() =>
+ wrapDataFetcherError(async () => {
+ return getRevision(params.spaceId, params.revisionId, {
+ metadata: params.metadata,
+ });
+ })
+ );
},
getRevisionFile(params) {
- return wrapDataFetcherError(async () => {
- const revisionFile = await getRevisionFile(
- params.spaceId,
- params.revisionId,
- params.fileId
- );
- if (!revisionFile) {
- throw new DataFetcherError('Revision file not found', 404);
- }
-
- return revisionFile;
- });
+ return withAPI(() =>
+ wrapDataFetcherError(async () => {
+ const revisionFile = await getRevisionFile(
+ params.spaceId,
+ params.revisionId,
+ params.fileId
+ );
+ if (!revisionFile) {
+ throw new DataFetcherError('Revision file not found', 404);
+ }
+
+ return revisionFile;
+ })
+ );
},
getRevisionPageMarkdown() {
@@ -160,117 +206,140 @@ async function getDataFetcherV1(): Promise {
},
getDocument(params) {
- return wrapDataFetcherError(async () => {
- const document = await getDocument(params.spaceId, params.documentId);
- if (!document) {
- throw new DataFetcherError('Document not found', 404);
- }
-
- return document;
- });
+ return withAPI(() =>
+ wrapDataFetcherError(async () => {
+ const document = await getDocument(params.spaceId, params.documentId);
+ if (!document) {
+ throw new DataFetcherError('Document not found', 404);
+ }
+
+ return document;
+ })
+ );
},
getComputedDocument(params) {
- return wrapDataFetcherError(() => {
- return getComputedDocument(
- params.organizationId,
- params.spaceId,
- params.source,
- params.seed
- );
- });
+ return withAPI(() =>
+ wrapDataFetcherError(() => {
+ return getComputedDocument(
+ params.organizationId,
+ params.spaceId,
+ params.source,
+ params.seed
+ );
+ })
+ );
},
getRevisionPages(params) {
- return wrapDataFetcherError(async () => {
- return getRevisionPages(params.spaceId, params.revisionId, {
- metadata: params.metadata,
- });
- });
+ return withAPI(() =>
+ wrapDataFetcherError(async () => {
+ return getRevisionPages(params.spaceId, params.revisionId, {
+ metadata: params.metadata,
+ });
+ })
+ );
},
getRevisionPageByPath(params) {
- return wrapDataFetcherError(async () => {
- const revisionPage = await getRevisionPageByPath(
- params.spaceId,
- params.revisionId,
- params.path
- );
-
- if (!revisionPage) {
- throw new DataFetcherError('Revision page not found', 404);
- }
-
- return revisionPage;
- });
+ return withAPI(() =>
+ wrapDataFetcherError(async () => {
+ const revisionPage = await getRevisionPageByPath(
+ params.spaceId,
+ params.revisionId,
+ params.path
+ );
+
+ if (!revisionPage) {
+ throw new DataFetcherError('Revision page not found', 404);
+ }
+
+ return revisionPage;
+ })
+ );
},
getReusableContent(params) {
- return wrapDataFetcherError(async () => {
- const reusableContent = await getReusableContent(
- params.spaceId,
- params.revisionId,
- params.reusableContentId
- );
-
- if (!reusableContent) {
- throw new DataFetcherError('Reusable content not found', 404);
- }
-
- return reusableContent;
- });
+ return withAPI(() =>
+ wrapDataFetcherError(async () => {
+ const reusableContent = await getReusableContent(
+ params.spaceId,
+ params.revisionId,
+ params.reusableContentId
+ );
+
+ if (!reusableContent) {
+ throw new DataFetcherError('Reusable content not found', 404);
+ }
+
+ return reusableContent;
+ })
+ );
},
getLatestOpenAPISpecVersionContent(params) {
- return wrapDataFetcherError(async () => {
- const openAPISpecVersionContent = await getLatestOpenAPISpecVersionContent(
- params.organizationId,
- params.slug
- );
-
- if (!openAPISpecVersionContent) {
- throw new DataFetcherError('OpenAPI spec version content not found', 404);
- }
-
- return openAPISpecVersionContent;
- });
+ return withAPI(() =>
+ wrapDataFetcherError(async () => {
+ const openAPISpecVersionContent = await getLatestOpenAPISpecVersionContent(
+ params.organizationId,
+ params.slug
+ );
+
+ if (!openAPISpecVersionContent) {
+ throw new DataFetcherError('OpenAPI spec version content not found', 404);
+ }
+
+ return openAPISpecVersionContent;
+ })
+ );
},
getSiteRedirectBySource(params) {
- return wrapDataFetcherError(async () => {
- const siteRedirect = await getSiteRedirectBySource(params);
- if (!siteRedirect) {
- throw new DataFetcherError('Site redirect not found', 404);
- }
-
- return siteRedirect;
- });
+ return withAPI(() =>
+ wrapDataFetcherError(async () => {
+ const siteRedirect = await getSiteRedirectBySource(params);
+ if (!siteRedirect) {
+ throw new DataFetcherError('Site redirect not found', 404);
+ }
+
+ return siteRedirect;
+ })
+ );
},
getEmbedByUrl(params) {
- return wrapDataFetcherError(() => {
- return getEmbedByUrlInSpace(params.spaceId, params.url);
- });
+ return withAPI(() =>
+ wrapDataFetcherError(() => {
+ return getEmbedByUrlInSpace(params.spaceId, params.url);
+ })
+ );
},
searchSiteContent(params) {
- return wrapDataFetcherError(async () => {
- const { organizationId, siteId, query, cacheBust, scope } = params;
- const result = await searchSiteContent(
- organizationId,
- siteId,
- query,
- scope,
- cacheBust
- );
- return result.items;
- });
+ return withAPI(() =>
+ wrapDataFetcherError(async () => {
+ const { organizationId, siteId, query, cacheBust, scope } = params;
+ const result = await searchSiteContent(
+ organizationId,
+ siteId,
+ query,
+ scope,
+ cacheBust
+ );
+ return result.items;
+ })
+ );
},
renderIntegrationUi(params) {
- return wrapDataFetcherError(async () => {
- const result = await renderIntegrationUi(params.integrationName, params.request);
- return result;
- });
+ return withAPI(() =>
+ wrapDataFetcherError(async () => {
+ const result = await renderIntegrationUi(
+ params.integrationName,
+ params.request
+ );
+ return result;
+ })
+ );
},
streamAIResponse() {