mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-23 19:06:31 +00:00
Fix previews for sites in multi-id mode (#2289)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Metadata> {
|
||||
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),
|
||||
]);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
+92
-15
@@ -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<SiteCustomizationSettings> {
|
||||
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<Partial<SiteCustomizationSettings>>(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<SiteCustomizationSettings> {
|
||||
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.
|
||||
*/
|
||||
|
||||
+17
-4
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user