From 4005895337396c30005d78bfeca66b94ae0edc0e Mon Sep 17 00:00:00 2001 From: Taran Vohra Date: Thu, 18 Apr 2024 18:19:52 +0530 Subject: [PATCH] Fix previews for sites in multi-id mode (#2289) --- .../(content)/[[...pathname]]/not-found.tsx | 4 +- .../(space)/(core)/~gitbook/icon/route.tsx | 4 +- src/app/(space)/(core)/~gitbook/pdf/page.tsx | 6 +- src/app/(space)/fetch.ts | 10 +- src/app/(space)/layout.tsx | 4 +- src/lib/api.ts | 107 +++++++++++++++--- src/middleware.ts | 21 +++- 7 files changed, 123 insertions(+), 33 deletions(-) diff --git a/src/app/(space)/(content)/[[...pathname]]/not-found.tsx b/src/app/(space)/(content)/[[...pathname]]/not-found.tsx index e617f5398..91bbbc7f6 100644 --- a/src/app/(space)/(content)/[[...pathname]]/not-found.tsx +++ b/src/app/(space)/(content)/[[...pathname]]/not-found.tsx @@ -1,5 +1,5 @@ import { getSpaceLanguage, t } from '@/intl/server'; -import { getSiteSpaceLayoutData, getSpaceLayoutData } from '@/lib/api'; +import { getCurrentSiteLayoutData, getSpaceLayoutData } from '@/lib/api'; import { tcls } from '@/lib/tailwind'; import { getContentPointer } from '../../fetch'; @@ -7,7 +7,7 @@ import { getContentPointer } from '../../fetch'; export default async function NotFound() { const pointer = getContentPointer(); const { customization } = await ('siteId' in pointer - ? getSiteSpaceLayoutData(pointer) + ? getCurrentSiteLayoutData(pointer) : getSpaceLayoutData(pointer.spaceId)); const language = getSpaceLanguage(customization); diff --git a/src/app/(space)/(core)/~gitbook/icon/route.tsx b/src/app/(space)/(core)/~gitbook/icon/route.tsx index 94c7b933f..09b2436e8 100644 --- a/src/app/(space)/(core)/~gitbook/icon/route.tsx +++ b/src/app/(space)/(core)/~gitbook/icon/route.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { getContentPointer } from '@/app/(space)/fetch'; import { getCollection, + getCurrentSiteCustomization, getSite, - getSiteSpaceCustomization, getSpace, getSpaceCustomization, } from '@/lib/api'; @@ -46,7 +46,7 @@ export async function GET(req: NextRequest) { const [space, customization] = await Promise.all([ getSpace(spaceId), - 'siteId' in pointer ? getSiteSpaceCustomization(pointer) : getSpaceCustomization(spaceId), + 'siteId' in pointer ? getCurrentSiteCustomization(pointer) : getSpaceCustomization(spaceId), ]); const parent = 'siteId' in pointer diff --git a/src/app/(space)/(core)/~gitbook/pdf/page.tsx b/src/app/(space)/(core)/~gitbook/pdf/page.tsx index 3063dad9a..86f35571e 100644 --- a/src/app/(space)/(core)/~gitbook/pdf/page.tsx +++ b/src/app/(space)/(core)/~gitbook/pdf/page.tsx @@ -22,7 +22,7 @@ import { getSpace, getSpaceCustomization, getSpaceContentData, - getSiteSpaceCustomization, + getCurrentSiteCustomization, } from '@/lib/api'; import { pagePDFContainerId, PageHrefContext, absoluteHref } from '@/lib/links'; import { resolvePageId } from '@/lib/pages'; @@ -43,7 +43,7 @@ export async function generateMetadata(): Promise { const [space, customization] = await Promise.all([ getSpace(contentPointer.spaceId), 'siteId' in contentPointer - ? getSiteSpaceCustomization(contentPointer) + ? getCurrentSiteCustomization(contentPointer) : getSpaceCustomization(contentPointer.spaceId), ]); @@ -69,7 +69,7 @@ export default async function PDFHTMLOutput(props: { searchParams: { [key: strin // Load the content, const [customization, { space, contentTarget, pages: rootPages }] = await Promise.all([ 'siteId' in contentPointer - ? getSiteSpaceCustomization(contentPointer) + ? getCurrentSiteCustomization(contentPointer) : getSpaceCustomization(contentPointer.spaceId), getSpaceContentData(contentPointer), ]); diff --git a/src/app/(space)/fetch.ts b/src/app/(space)/fetch.ts index 8d8bb663c..c73b5ac03 100644 --- a/src/app/(space)/fetch.ts +++ b/src/app/(space)/fetch.ts @@ -10,7 +10,7 @@ import { getSpaceData, ContentTarget, SiteContentPointer, - getSiteSpaceData, + getCurrentSiteData, getSite, getSiteSpaces, } from '@/lib/api'; @@ -40,14 +40,14 @@ export function getContentPointer(): ContentPointer | SiteContentPointer { if (siteId) { const organizationId = headerSet.get('x-gitbook-content-organization'); const siteSpaceId = headerSet.get('x-gitbook-content-site-space'); - if (!organizationId || !siteSpaceId) { + if (!organizationId) { throw new Error('Missing site content headers'); } const siteContent: SiteContentPointer = { siteId, spaceId, - siteSpaceId, + siteSpaceId: siteSpaceId ?? undefined, organizationId, revisionId: headerSet.get('x-gitbook-content-revision') ?? undefined, changeRequestId: headerSet.get('x-gitbook-content-changerequest') ?? undefined, @@ -71,7 +71,7 @@ export async function fetchSpaceData() { const [{ space, contentTarget, pages, customization, scripts }, parentSite] = await Promise.all( 'siteId' in content - ? [getSiteSpaceData(content), fetchParentSite(content.organizationId, content.siteId)] + ? [getCurrentSiteData(content), fetchParentSite(content.organizationId, content.siteId)] : [getSpaceData(content)], ); @@ -96,7 +96,7 @@ export async function fetchSpaceData() { export async function fetchPageData(params: PagePathParams | PageIdParams) { const content = getContentPointer(); const { space, contentTarget, pages, customization, scripts } = await ('siteId' in content - ? getSiteSpaceData(content) + ? getCurrentSiteData(content) : getSpaceData(content)); const page = await resolvePage(contentTarget, pages, params); diff --git a/src/app/(space)/layout.tsx b/src/app/(space)/layout.tsx index 14e1814bf..9e2aba412 100644 --- a/src/app/(space)/layout.tsx +++ b/src/app/(space)/layout.tsx @@ -11,7 +11,7 @@ import colors from 'tailwindcss/colors'; import { emojiFontClassName } from '@/components/primitives'; import { fonts, ibmPlexMono } from '@/fonts'; import { getSpaceLanguage } from '@/intl/server'; -import { getSiteSpaceLayoutData, getSpaceLayoutData } from '@/lib/api'; +import { getCurrentSiteLayoutData, getSpaceLayoutData } from '@/lib/api'; import { hexToRgb, shadesOfColor } from '@/lib/colors'; import { tcls } from '@/lib/tailwind'; @@ -28,7 +28,7 @@ export default async function SpaceRootLayout(props: { children: React.ReactNode const pointer = getContentPointer(); const { customization } = await ('siteId' in pointer - ? getSiteSpaceLayoutData(pointer) + ? getCurrentSiteLayoutData(pointer) : getSpaceLayoutData(pointer.spaceId)); const headerTheme = generateHeaderTheme(customization); const language = getSpaceLanguage(customization); diff --git a/src/lib/api.ts b/src/lib/api.ts index ea680e526..661ddf8bf 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -43,7 +43,10 @@ export interface ContentPointer { export interface SiteContentPointer extends ContentPointer { organizationId: string; siteId: string; - siteSpaceId: string; + /** + * ID of the siteSpace can be undefined when rendering in multi-id mode (for site previews) + */ + siteSpaceId: string | undefined; } /** @@ -595,7 +598,7 @@ export const getDocument = cache( /** * Get the customization settings for a site-space from the API. */ -export const getSiteSpaceCustomizationFromAPI = cache( +const getSiteSpaceCustomizationFromAPI = cache( 'api.getSiteSpaceCustomizationById', async ( organizationId: string, @@ -625,10 +628,33 @@ export const getSiteSpaceCustomizationFromAPI = cache( }, ); +/** + * Get the customization settings for a site from the API. + */ +const getSiteCustomizationFromAPI = cache( + 'api.getSiteCustomizationById', + async (organizationId: string, siteId: string, options: CacheFunctionOptions) => { + const response = await api().orgs.getSiteCustomizationById(organizationId, siteId, { + signal: options.signal, + ...noCacheFetchOptions, + }); + return cacheResponse(response, { + revalidateBefore: 60 * 60, + tags: [ + getAPICacheTag({ + tag: 'site', + organization: organizationId, + site: siteId, + }), + ], + }); + }, +); + /** * Get the customization settings for a site space from the API. */ -export async function getSiteSpaceCustomization(args: { +async function getSiteSpaceCustomization(args: { organizationId: string; siteId: string; siteSpaceId: string; @@ -657,6 +683,33 @@ export async function getSiteSpaceCustomization(args: { return raw; } +/** + * Get the customization settings for a site space from the API. + */ +async function getSiteCustomization(args: { + organizationId: string; + siteId: string; +}): Promise { + const headersList = headers(); + const raw = await getSiteCustomizationFromAPI(args.organizationId, args.siteId); + + const extend = headersList.get('x-gitbook-customization'); + if (extend) { + try { + const parsed = rison.decode_object>(extend); + return { ...raw, ...parsed }; + } catch (error) { + console.error( + `Failed to parse x-gitbook-customization header (ignored): ${ + (error as Error).stack ?? (error as Error).message ?? error + }`, + ); + } + } + + return raw; +} + /** * Get the infos about a site by its ID. */ @@ -713,12 +766,12 @@ export const getSiteIntegrationScripts = cache( ); /** - * Fetch all the data to render a site-space at once. + * Fetch all the data to render the current site at once. */ -export async function getSiteSpaceData(pointer: SiteContentPointer) { +export async function getCurrentSiteData(pointer: SiteContentPointer) { const [{ space, pages, contentTarget }, { customization, scripts }] = await Promise.all([ getSpaceData(pointer), - getSiteSpaceLayoutData(pointer), + getCurrentSiteLayoutData(pointer), ]); return { @@ -731,20 +784,24 @@ export async function getSiteSpaceData(pointer: SiteContentPointer) { } /** - * Fetch all the layout data about a site-space at once. + * Fetch all the layout data about the current site at once. */ -export async function getSiteSpaceLayoutData(args: { +export async function getCurrentSiteLayoutData(args: { organizationId: string; siteId: string; - siteSpaceId: string; - spaceId: string; + siteSpaceId: string | undefined; }) { const [customization, scripts] = await Promise.all([ - getSiteSpaceCustomization({ - organizationId: args.organizationId, - siteId: args.siteId, - siteSpaceId: args.siteSpaceId, - }), + args.siteSpaceId + ? getSiteSpaceCustomization({ + organizationId: args.organizationId, + siteId: args.siteId, + siteSpaceId: args.siteSpaceId, + }) + : getSiteCustomization({ + organizationId: args.organizationId, + siteId: args.siteId, + }), getSiteIntegrationScripts(args.organizationId, args.siteId), ]); @@ -754,6 +811,26 @@ export async function getSiteSpaceLayoutData(args: { }; } +/** + * Get the customization settings for the current site from the API. + */ +export async function getCurrentSiteCustomization(args: { + organizationId: string; + siteId: string; + siteSpaceId: string | undefined; +}): Promise { + return args.siteSpaceId + ? getSiteSpaceCustomization({ + organizationId: args.organizationId, + siteId: args.siteId, + siteSpaceId: args.siteSpaceId, + }) + : getSiteCustomization({ + organizationId: args.organizationId, + siteId: args.siteId, + }); +} + /** * Get the customization settings for a space from the API. */ diff --git a/src/middleware.ts b/src/middleware.ts index ab2d3422c..2cd0152e6 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,6 +1,7 @@ import { GitBookAPI } from '@gitbook/api'; import * as Sentry from '@sentry/nextjs'; import assertNever from 'assert-never'; +import jwt from 'jsonwebtoken'; import type { ResponseCookie } from 'next/dist/compiled/@edge-runtime/cookies'; import { NextResponse, NextRequest } from 'next/server'; @@ -14,7 +15,7 @@ import { withAPI, getSpaceLayoutData, DEFAULT_API_ENDPOINT, - getSiteSpaceLayoutData, + getCurrentSiteLayoutData, } from '@/lib/api'; import { race } from '@/lib/async'; import { buildVersion } from '@/lib/build'; @@ -73,6 +74,13 @@ export type LookupResult = PublishedContentWithCache & { cookies?: LookupCookies; }; +interface ContentAPITokenPayload { + organization: string; + spaces: string[]; + collection?: string; + site?: string; +} + /** * Middleware to lookup the space to render. * It takes as input a request with an URL, and a set of headers: @@ -170,11 +178,10 @@ export async function middleware(request: NextRequest) { ); const { scripts } = await ('site' in resolved - ? getSiteSpaceLayoutData({ + ? getCurrentSiteLayoutData({ organizationId: resolved.organization, siteId: resolved.site, siteSpaceId: resolved.siteSpace, - spaceId: resolved.space, }) : getSpaceLayoutData(resolved.space)); return getContentSecurityPolicy(scripts, nonce); @@ -196,7 +203,9 @@ export async function middleware(request: NextRequest) { if ('site' in resolved) { headers.set('x-gitbook-content-organization', resolved.organization); headers.set('x-gitbook-content-site', resolved.site); - headers.set('x-gitbook-content-site-space', resolved.siteSpace); + if (resolved.siteSpace) { + headers.set('x-gitbook-content-site-space', resolved.siteSpace); + } } if (resolved.revision) { headers.set('x-gitbook-content-revision', resolved.revision); @@ -475,10 +484,14 @@ async function lookupSpaceInMultiIdMode(request: NextRequest, url: URL): Promise }; } + const decoded = jwt.decode(apiToken) as ContentAPITokenPayload; + return { space: spaceId, changeRequest: changeRequestId, revision: revisionId, + site: decoded.site, + organization: decoded.organization, basePath: normalizePathname(basePathParts.join('/')), pathname: normalizePathname(pathSegments.join('/')), apiToken,