diff --git a/bun.lockb b/bun.lockb index 5b99ab574..322433687 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/gitbook/e2e/pages.spec.ts b/packages/gitbook/e2e/pages.spec.ts index 92d4b6fdb..e0d456dcc 100644 --- a/packages/gitbook/e2e/pages.spec.ts +++ b/packages/gitbook/e2e/pages.spec.ts @@ -491,6 +491,19 @@ const testCases: TestsCase[] = [ }, ], }, + { + name: 'Site Redirects', + baseUrl: 'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/', + tests: [ + { + name: 'Redirect to SSO page', + url: 'a/redirect/to/sso', + run: async (page) => { + await expect(page.locator('h1')).toHaveText('SSO'); + }, + }, + ], + }, { name: 'Share links', baseUrl: 'https://gitbook.gitbook.io/gbo-tests-share-links/', diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index 54c7683bc..241bb7c97 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -16,7 +16,7 @@ "clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static" }, "dependencies": { - "@gitbook/api": "^0.76.0", + "@gitbook/api": "^0.77.0", "@gitbook/cache-do": "workspace:*", "@gitbook/emoji-codepoints": "workspace:*", "@gitbook/icons": "workspace:*", diff --git a/packages/gitbook/src/app/(site)/fetch.ts b/packages/gitbook/src/app/(site)/fetch.ts index 3c6b11887..b98f94f62 100644 --- a/packages/gitbook/src/app/(site)/fetch.ts +++ b/packages/gitbook/src/app/(site)/fetch.ts @@ -1,11 +1,12 @@ import { RevisionPage } from '@gitbook/api'; +import { redirect } from 'next/navigation'; import { getRevisionPageByPath, getDocument, - ContentTarget, getSpaceContentData, getSiteData, + getSiteRedirectBySource, } from '@/lib/api'; import { resolvePagePath, resolvePageId } from '@/lib/pages'; import { getSiteContentPointer } from '@/lib/pointer'; @@ -41,6 +42,7 @@ export async function fetchContentData() { site, sections, spaces, + shareKey: content.siteShareKey, customization, scripts, ancestors: [], @@ -54,7 +56,15 @@ export async function fetchContentData() { export async function fetchPageData(params: PagePathParams | PageIdParams) { const contentData = await fetchContentData(); - const page = await resolvePage(contentData.contentTarget, contentData.pages, params); + const page = await resolvePage({ + organizationId: contentData.space.organization, + siteId: contentData.site.id, + spaceId: contentData.contentTarget.spaceId, + revisionId: contentData.contentTarget.revisionId, + pages: contentData.pages, + shareKey: contentData.shareKey, + params, + }); const document = page?.page.documentId ? await getDocument(contentData.space.id, page.page.documentId) : null; @@ -70,11 +80,17 @@ export async function fetchPageData(params: PagePathParams | PageIdParams) { * Resolve a page from the params. * If the path can't be found, we try to resolve it from the API to handle redirects. */ -async function resolvePage( - contentTarget: ContentTarget, - pages: RevisionPage[], - params: PagePathParams | PageIdParams, -) { +async function resolvePage(input: { + organizationId: string; + siteId: string; + spaceId: string; + revisionId: string; + shareKey: string | undefined; + pages: RevisionPage[]; + params: PagePathParams | PageIdParams; +}) { + const { organizationId, siteId, spaceId, revisionId, pages, shareKey, params } = input; + if ('pageId' in params) { return resolvePageId(pages, params.pageId); } @@ -88,20 +104,26 @@ async function resolvePage( return page; } - // If page can't be found, we try with the API, in case we have a redirect - // We use the raw pathname to handle special/malformed redirects setup by users in the GitSync. - // The page rendering will take care of redirecting to a normalized pathname. - // // We don't test path that are too long as GitBook doesn't support them and will return a 404 anyway. if (rawPathname.length <= 512) { - const resolved = await getRevisionPageByPath( - contentTarget.spaceId, - contentTarget.revisionId, - rawPathname, - ); + // If page can't be found, we try with the API, in case we have a redirect at space level. + // We use the raw pathname to handle special/malformed redirects setup by users in the GitSync. + // The page rendering will take care of redirecting to a normalized pathname. + const resolved = await getRevisionPageByPath(spaceId, revisionId, rawPathname); if (resolved) { return resolvePageId(pages, resolved.id); } + + // If a page still can't be found, we try with the API, in case we have a redirect at site level. + const resolvedSiteRedirect = await getSiteRedirectBySource({ + organizationId, + siteId, + source: rawPathname.startsWith('/') ? rawPathname : `/${rawPathname}`, + siteShareKey: input.shareKey, + }); + if (resolvedSiteRedirect) { + return redirect(resolvedSiteRedirect.target); + } } return undefined; diff --git a/packages/gitbook/src/lib/api.ts b/packages/gitbook/src/lib/api.ts index b9a7992bd..ba016de9f 100644 --- a/packages/gitbook/src/lib/api.ts +++ b/packages/gitbook/src/lib/api.ts @@ -620,6 +620,49 @@ export const getDocument = cache({ timeout: 20 * 1000, }); +/** + * Resolve a site redirect by its source path. + */ +export const getSiteRedirectBySource = cache({ + name: 'api.getSiteRedirectBySource', + tag: ({ siteId }) => getAPICacheTag({ tag: 'site', site: siteId }), + get: async ( + args: { + organizationId: string; + siteId: string; + /** Site share key that can be used as context to resolve site space published urls */ + siteShareKey: string | undefined; + source: string; + }, + options: CacheFunctionOptions, + ) => { + try { + const response = await api().orgs.getSiteRedirectBySource( + args.organizationId, + args.siteId, + { + shareKey: args.siteShareKey, + source: args.source, + }, + { + ...noCacheFetchOptions, + signal: options.signal, + }, + ); + return cacheResponse(response, cacheTtl_1day); + } catch (error) { + if ((error as GitBookAPIError).code === 404) { + return { + data: null, + ...cacheTtl_1day, + }; + } + + throw error; + } + }, +}); + /** * Get the infos about a site by its ID. */ diff --git a/packages/react-contentkit/package.json b/packages/react-contentkit/package.json index c4e468d7b..82b92e432 100644 --- a/packages/react-contentkit/package.json +++ b/packages/react-contentkit/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "classnames": "^2.5.1", - "@gitbook/api": "^0.76.0", + "@gitbook/api": "^0.77.0", "assert-never": "^1.2.1" }, "peerDependencies": {