diff --git a/.changeset/friendly-trains-exercise.md b/.changeset/friendly-trains-exercise.md deleted file mode 100644 index eb1616633..000000000 --- a/.changeset/friendly-trains-exercise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'gitbook': patch ---- - -Fix multiple bugs due to headers read in an anarchic way in the app. diff --git a/.changeset/seven-hounds-grow.md b/.changeset/seven-hounds-grow.md new file mode 100644 index 000000000..d125b8eea --- /dev/null +++ b/.changeset/seven-hounds-grow.md @@ -0,0 +1,5 @@ +--- +'gitbook': patch +--- + +Fix issue leading to increase the storage write and the stability of the platform diff --git a/packages/gitbook/src/app/(global)/~gitbook/image/route.ts b/packages/gitbook/src/app/(global)/~gitbook/image/route.ts index 67096ba32..f96d0bc90 100644 --- a/packages/gitbook/src/app/(global)/~gitbook/image/route.ts +++ b/packages/gitbook/src/app/(global)/~gitbook/image/route.ts @@ -1,6 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; -import { getGitBookContextFromHeaders } from '@/lib/gitbook-context'; import { CURRENT_SIGNATURE_VERSION, isSignatureVersion, @@ -40,8 +39,7 @@ export async function GET(request: NextRequest) { } // Verify the signature - const ctx = getGitBookContextFromHeaders(request.headers); - const verified = await verifyImageSignature(ctx, url, { signature, version: signatureVersion }); + const verified = await verifyImageSignature(url, { signature, version: signatureVersion }); if (!verified) { return new Response(`Invalid signature "${signature ?? ''}" for "${url}"`, { status: 400 }); } diff --git a/packages/gitbook/src/app/(site)/(content)/[[...pathname]]/not-found.tsx b/packages/gitbook/src/app/(site)/(content)/[[...pathname]]/not-found.tsx index bff278a06..cd36e5b26 100644 --- a/packages/gitbook/src/app/(site)/(content)/[[...pathname]]/not-found.tsx +++ b/packages/gitbook/src/app/(site)/(content)/[[...pathname]]/not-found.tsx @@ -1,18 +1,14 @@ -import { headers } from 'next/headers'; - import { TrackPageViewEvent } from '@/components/Insights'; import { getSpaceLanguage, t } from '@/intl/server'; import { getSiteData, getSpaceContentData } from '@/lib/api'; -import { getGitBookContextFromHeaders } from '@/lib/gitbook-context'; import { getSiteContentPointer } from '@/lib/pointer'; import { tcls } from '@/lib/tailwind'; export default async function NotFound() { - const ctx = getGitBookContextFromHeaders(await headers()); - const pointer = getSiteContentPointer(ctx); + const pointer = await getSiteContentPointer(); const [{ space }, { customization }] = await Promise.all([ - getSpaceContentData(ctx, pointer, pointer.siteShareKey), - getSiteData(ctx, pointer), + getSpaceContentData(pointer, pointer.siteShareKey), + getSiteData(pointer), ]); const language = getSpaceLanguage(customization); diff --git a/packages/gitbook/src/app/(site)/(content)/[[...pathname]]/page.tsx b/packages/gitbook/src/app/(site)/(content)/[[...pathname]]/page.tsx index a9f5a279a..786d5d6eb 100644 --- a/packages/gitbook/src/app/(site)/(content)/[[...pathname]]/page.tsx +++ b/packages/gitbook/src/app/(site)/(content)/[[...pathname]]/page.tsx @@ -1,12 +1,10 @@ import { CustomizationHeaderPreset, CustomizationThemeMode } from '@gitbook/api'; import { Metadata, Viewport } from 'next'; -import { headers } from 'next/headers'; import { notFound, redirect } from 'next/navigation'; import React from 'react'; import { PageAside } from '@/components/PageAside'; import { PageBody, PageCover } from '@/components/PageBody'; -import { getGitBookContextFromHeaders, GitBookContext } from '@/lib/gitbook-context'; import { PageHrefContext, getAbsoluteHref, getPageHref } from '@/lib/links'; import { getPagePath, resolveFirstDocument } from '@/lib/pages'; import { ContentRefContext } from '@/lib/references'; @@ -19,21 +17,17 @@ import { PagePathParams, fetchPageData, getPathnameParam, normalizePathname } fr export const runtime = 'edge'; export const dynamic = 'force-dynamic'; -type Props = { - params: Promise; - searchParams: Promise<{ fallback?: string }>; -}; - /** * Fetch and render a page. */ -export default async function Page(props: Props) { - const [headersList, params, searchParams] = await Promise.all([ - headers(), - props.params, - props.searchParams, - ]); - const ctx = getGitBookContextFromHeaders(headersList); +export default async function Page(props: { + params: Promise; + searchParams: Promise<{ fallback?: string }>; +}) { + const { params: rawParams, searchParams: rawSearchParams } = props; + + const params = await rawParams; + const searchParams = await rawSearchParams; const { content: contentPointer, @@ -46,7 +40,7 @@ export default async function Page(props: Props) { page, ancestors, document, - } = await getPageDataWithFallback(ctx, { + } = await getPageDataWithFallback({ pagePathParams: params, searchParams, redirectOnFallback: true, @@ -59,12 +53,12 @@ export default async function Page(props: Props) { if (pathname !== rawPathname) { // If the pathname was not normalized, redirect to the normalized version // before trying to resolve the page again - redirect(getAbsoluteHref(ctx, pathname)); + redirect(await getAbsoluteHref(pathname)); } else { notFound(); } } else if (getPagePath(pages, page) !== rawPathname) { - redirect(getPageHref(ctx, pages, page, linksContext)); + redirect(await getPageHref(pages, page, linksContext)); } const withTopHeader = customization.header.preset !== CustomizationHeaderPreset.None; @@ -128,10 +122,12 @@ export default async function Page(props: Props) { ); } -export async function generateViewport(props: Props): Promise { - const [params, headersList] = await Promise.all([props.params, headers()]); - const ctx = getGitBookContextFromHeaders(headersList); - const { customization } = await fetchPageData(ctx, params); +export async function generateViewport({ + params, +}: { + params: Promise; +}): Promise { + const { customization } = await fetchPageData(await params); return { colorScheme: customization.themes.toggeable ? customization.themes.default === CustomizationThemeMode.Dark @@ -141,20 +137,17 @@ export async function generateViewport(props: Props): Promise { }; } -export async function generateMetadata(props: Props): Promise { - const [params, searchParams, headersList] = await Promise.all([ - props.params, - props.searchParams, - headers(), - ]); - const ctx = getGitBookContextFromHeaders(headersList); - const { space, pages, page, customization, site, ancestors } = await getPageDataWithFallback( - ctx, - { - pagePathParams: params, - searchParams: searchParams, - }, - ); +export async function generateMetadata({ + params, + searchParams, +}: { + params: Promise; + searchParams: Promise<{ fallback?: string }>; +}): Promise { + const { space, pages, page, customization, site, ancestors } = await getPageDataWithFallback({ + pagePathParams: await params, + searchParams: await searchParams, + }); if (!page) { notFound(); @@ -167,16 +160,17 @@ export async function generateMetadata(props: Props): Promise { description: page.description ?? '', alternates: { // Trim trailing slashes in canonical URL to match the redirect behavior - canonical: getAbsoluteHref(ctx, getPagePath(pages, page), true).replace(/\/+$/, ''), + canonical: (await getAbsoluteHref(getPagePath(pages, page), true)).replace(/\/+$/, ''), }, openGraph: { images: [ customization.socialPreview.url ?? - getAbsoluteHref(ctx, `~gitbook/ogimage/${page.id}`, true), + (await getAbsoluteHref(`~gitbook/ogimage/${page.id}`, true)), ], }, robots: - isSpaceIndexable(ctx, { space, site: site ?? null }) && isPageIndexable(ancestors, page) + (await isSpaceIndexable({ space, site: site ?? null })) && + isPageIndexable(ancestors, page) ? 'index, follow' : 'noindex, nofollow', }; @@ -185,17 +179,14 @@ export async function generateMetadata(props: Props): Promise { /** * Fetches the page data matching the requested pathname and fallback to root page when page is not found. */ -async function getPageDataWithFallback( - ctx: GitBookContext, - args: { - pagePathParams: PagePathParams; - searchParams: { fallback?: string }; - redirectOnFallback?: boolean; - }, -) { +async function getPageDataWithFallback(args: { + pagePathParams: PagePathParams; + searchParams: { fallback?: string }; + redirectOnFallback?: boolean; +}) { const { pagePathParams, searchParams, redirectOnFallback = false } = args; - const { pages, page: targetPage, ...otherPageData } = await fetchPageData(ctx, pagePathParams); + const { pages, page: targetPage, ...otherPageData } = await fetchPageData(pagePathParams); let page = targetPage; const canFallback = !!searchParams.fallback; @@ -203,7 +194,7 @@ async function getPageDataWithFallback( const rootPage = resolveFirstDocument(pages, []); if (redirectOnFallback && rootPage?.page) { - redirect(getPageHref(ctx, pages, rootPage?.page)); + redirect(await getPageHref(pages, rootPage?.page)); } page = rootPage?.page; diff --git a/packages/gitbook/src/app/(site)/(content)/layout.tsx b/packages/gitbook/src/app/(site)/(content)/layout.tsx index cd7132fc3..9c0d17476 100644 --- a/packages/gitbook/src/app/(site)/(content)/layout.tsx +++ b/packages/gitbook/src/app/(site)/(content)/layout.tsx @@ -13,7 +13,6 @@ import { api } from '@/lib/api'; import { assetsDomain } from '@/lib/assets'; import { buildVersion } from '@/lib/build'; import { getContentSecurityPolicyNonce } from '@/lib/csp'; -import { GitBookContext, getGitBookContextFromHeaders } from '@/lib/gitbook-context'; import { getAbsoluteHref, getBaseUrl } from '@/lib/links'; import { isSpaceIndexable } from '@/lib/seo'; import { getContentTitle } from '@/lib/utils'; @@ -29,10 +28,9 @@ export const dynamic = 'force-dynamic'; * Layout when rendering the content. */ export default async function ContentLayout(props: { children: React.ReactNode }) { - const ctx = getGitBookContextFromHeaders(await headers()); const { children } = props; - const nonce = getContentSecurityPolicyNonce(ctx); + const nonce = await getContentSecurityPolicyNonce(); const { content, space, @@ -44,9 +42,10 @@ export default async function ContentLayout(props: { children: React.ReactNode } ancestors, scripts, sections, - } = await fetchContentData(ctx); + } = await fetchContentData(); - ReactDOM.preconnect(api(ctx).client.endpoint); + const apiCtx = await api(); + ReactDOM.preconnect(apiCtx.client.endpoint); if (assetsDomain) { ReactDOM.preconnect(assetsDomain); } @@ -58,7 +57,7 @@ export default async function ContentLayout(props: { children: React.ReactNode } }); }); - const queryStringTheme = getQueryStringTheme(ctx); + const queryStringTheme = await getQueryStringTheme(); return ( @@ -107,8 +106,7 @@ export default async function ContentLayout(props: { children: React.ReactNode } } export async function generateViewport(): Promise { - const ctx = getGitBookContextFromHeaders(await headers()); - const { customization } = await fetchContentData(ctx); + const { customization } = await fetchContentData(); return { colorScheme: customization.themes.toggeable ? customization.themes.default === CustomizationThemeMode.Dark @@ -119,43 +117,46 @@ export async function generateViewport(): Promise { } export async function generateMetadata(): Promise { - const ctx = getGitBookContextFromHeaders(await headers()); - const { space, site, customization } = await fetchContentData(ctx); + const { space, site, customization } = await fetchContentData(); const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null; return { title: getContentTitle(space, customization, site), generator: `GitBook (${buildVersion()})`, - metadataBase: new URL(getBaseUrl(ctx)), + metadataBase: new URL(await getBaseUrl()), icons: { icon: [ { url: customIcon?.light ?? - getAbsoluteHref(ctx, '~gitbook/icon?size=small&theme=light', true), + (await getAbsoluteHref('~gitbook/icon?size=small&theme=light', true)), type: 'image/png', media: '(prefers-color-scheme: light)', }, { url: customIcon?.dark ?? - getAbsoluteHref(ctx, '~gitbook/icon?size=small&theme=dark', true), + (await getAbsoluteHref('~gitbook/icon?size=small&theme=dark', true)), type: 'image/png', media: '(prefers-color-scheme: dark)', }, ], }, - robots: isSpaceIndexable(ctx, { space, site }) ? 'index, follow' : 'noindex, nofollow', + robots: (await isSpaceIndexable({ space, site })) ? 'index, follow' : 'noindex, nofollow', }; } /** * For preview, the theme can be set via query string (?theme=light). */ -function getQueryStringTheme(ctx: GitBookContext) { - if (!ctx.theme) { +async function getQueryStringTheme() { + const headersList = await headers(); + const queryStringTheme = headersList.get('x-gitbook-theme'); + if (!queryStringTheme) { return null; } - return ctx.theme === 'light' ? CustomizationThemeMode.Light : CustomizationThemeMode.Dark; + return queryStringTheme === 'light' + ? CustomizationThemeMode.Light + : CustomizationThemeMode.Dark; } diff --git a/packages/gitbook/src/app/(site)/(core)/robots.txt/route.ts b/packages/gitbook/src/app/(site)/(core)/robots.txt/route.ts index 9db6cf286..8035d6fcb 100644 --- a/packages/gitbook/src/app/(site)/(core)/robots.txt/route.ts +++ b/packages/gitbook/src/app/(site)/(core)/robots.txt/route.ts @@ -1,7 +1,6 @@ import { NextRequest } from 'next/server'; import { getSpace, getSite } from '@/lib/api'; -import { getGitBookContextFromHeaders } from '@/lib/gitbook-context'; import { getAbsoluteHref } from '@/lib/links'; import { getSiteContentPointer } from '@/lib/pointer'; import { isSpaceIndexable } from '@/lib/seo'; @@ -12,18 +11,17 @@ export const runtime = 'edge'; * Generate a robots.txt for the current space. */ export async function GET(req: NextRequest) { - const ctx = getGitBookContextFromHeaders(req.headers); - const pointer = getSiteContentPointer(ctx); + const pointer = await getSiteContentPointer(); const [site, space] = await Promise.all([ - getSite(ctx, pointer.organizationId, pointer.siteId), - getSpace(ctx, pointer.spaceId, pointer.siteShareKey), + getSite(pointer.organizationId, pointer.siteId), + getSpace(pointer.spaceId, pointer.siteShareKey), ]); const lines = [ `User-agent: *`, 'Disallow: /~gitbook/', - ...(isSpaceIndexable(ctx, { space, site }) - ? [`Allow: /`, `Sitemap: ${getAbsoluteHref(ctx, `/sitemap.xml`, true)}`] + ...((await isSpaceIndexable({ space, site })) + ? [`Allow: /`, `Sitemap: ${await getAbsoluteHref(`/sitemap.xml`, true)}`] : [`Disallow: /`]), ]; const content = lines.join('\n'); diff --git a/packages/gitbook/src/app/(site)/(core)/sitemap.xml/route.ts b/packages/gitbook/src/app/(site)/(core)/sitemap.xml/route.ts index fc211e3a4..23fa77de6 100644 --- a/packages/gitbook/src/app/(site)/(core)/sitemap.xml/route.ts +++ b/packages/gitbook/src/app/(site)/(core)/sitemap.xml/route.ts @@ -3,7 +3,6 @@ import jsontoxml from 'jsontoxml'; import { NextRequest } from 'next/server'; import { getSpaceContentData } from '@/lib/api'; -import { getGitBookContextFromHeaders } from '@/lib/gitbook-context'; import { getAbsoluteHref } from '@/lib/links'; import { getPagePath } from '@/lib/pages'; import { getSiteContentPointer } from '@/lib/pointer'; @@ -15,32 +14,33 @@ export const runtime = 'edge'; * Generate a sitemap.xml for the current space. */ export async function GET(req: NextRequest) { - const ctx = getGitBookContextFromHeaders(req.headers); - const pointer = getSiteContentPointer(ctx); - const { pages: rootPages } = await getSpaceContentData(ctx, pointer, pointer.siteShareKey); + const pointer = await getSiteContentPointer(); + const { pages: rootPages } = await getSpaceContentData(pointer, pointer.siteShareKey); const pages = flattenPages(rootPages, (page) => !page.hidden && isPageIndexable([], page)); - const urls = pages.map(({ page, depth }) => { - // Decay priority with depth - const priority = Math.pow(2, -0.25 * depth); - // Normalize to keep 2 decimals - const normalizedPriority = Math.floor(100 * priority) / 100; + const urls = await Promise.all( + pages.map(async ({ page, depth }) => { + // Decay priority with depth + const priority = Math.pow(2, -0.25 * depth); + // Normalize to keep 2 decimals + const normalizedPriority = Math.floor(100 * priority) / 100; - const lastModified = page.updatedAt || page.createdAt; + const lastModified = page.updatedAt || page.createdAt; - return { - url: { - loc: getAbsoluteHref(ctx, getPagePath(rootPages, page), true), - priority: normalizedPriority, - ...(lastModified - ? { - // lastmod format is YYYY-MM-DD - lastmod: new Date(lastModified).toISOString().split('T')[0], - } - : {}), - }, - }; - }); + return { + url: { + loc: await getAbsoluteHref(getPagePath(rootPages, page), true), + priority: normalizedPriority, + ...(lastModified + ? { + // lastmod format is YYYY-MM-DD + lastmod: new Date(lastModified).toISOString().split('T')[0], + } + : {}), + }, + }; + }), + ); const xml = jsontoxml( [ diff --git a/packages/gitbook/src/app/(site)/(core)/~gitbook/icon/route.tsx b/packages/gitbook/src/app/(site)/(core)/~gitbook/icon/route.tsx index ea8f720a5..1640266f6 100644 --- a/packages/gitbook/src/app/(site)/(core)/~gitbook/icon/route.tsx +++ b/packages/gitbook/src/app/(site)/(core)/~gitbook/icon/route.tsx @@ -5,7 +5,6 @@ import React from 'react'; import { getSite, getSiteData, getSpace } from '@/lib/api'; import { getEmojiForCode } from '@/lib/emojis'; -import { getGitBookContextFromHeaders } from '@/lib/gitbook-context'; import { getSiteContentPointer } from '@/lib/pointer'; import { tcls } from '@/lib/tailwind'; import { getContentTitle } from '@/lib/utils'; @@ -33,18 +32,17 @@ const SIZES = { * Render an icon for the space. */ export async function GET(req: NextRequest) { - const ctx = getGitBookContextFromHeaders(req.headers); const options = getOptions(req.url); const size = SIZES[options.size]; - const pointer = getSiteContentPointer(ctx); + const pointer = await getSiteContentPointer(); const spaceId = pointer.spaceId; const [space, { customization }] = await Promise.all([ - getSpace(ctx, spaceId, pointer.siteShareKey), - getSiteData(ctx, pointer), + getSpace(spaceId, pointer.siteShareKey), + getSiteData(pointer), ]); - const site = await getSite(ctx, pointer.organizationId, pointer.siteId); + const site = await getSite(pointer.organizationId, pointer.siteId); const contentTitle = getContentTitle(space, customization, site); return new ImageResponse( diff --git a/packages/gitbook/src/app/(site)/(core)/~gitbook/ogimage/[pageId]/route.tsx b/packages/gitbook/src/app/(site)/(core)/~gitbook/ogimage/[pageId]/route.tsx index 447a8bd2a..0ea76602e 100644 --- a/packages/gitbook/src/app/(site)/(core)/~gitbook/ogimage/[pageId]/route.tsx +++ b/packages/gitbook/src/app/(site)/(core)/~gitbook/ogimage/[pageId]/route.tsx @@ -6,7 +6,6 @@ import colorContrast from 'postcss-color-contrast/js'; import React from 'react'; import { googleFontsMap } from '@/fonts'; -import { getGitBookContextFromHeaders } from '@/lib/gitbook-context'; import { getAbsoluteHref } from '@/lib/links'; import { filterOutNullable } from '@/lib/typescript'; import { getContentTitle } from '@/lib/utils'; @@ -57,8 +56,7 @@ async function loadGoogleFont(input: { fontFamily: string; text: string; weight: * Render the OpenGraph image for a space. */ export async function GET(req: NextRequest, { params }: { params: Promise }) { - const ctx = getGitBookContextFromHeaders(req.headers); - const { space, page, customization, site } = await fetchPageData(ctx, await params); + const { space, page, customization, site } = await fetchPageData(await params); // If user configured a custom social preview, we redirect to it. if (customization.socialPreview.url) { @@ -106,8 +104,10 @@ export async function GET(req: NextRequest, { params }: { params: Promise { + if ('icon' in customization.favicon) + return ( + Icon + ); + if ('emoji' in customization.favicon) + return ( + + {String.fromCodePoint(parseInt('0x' + customization.favicon.emoji))} + + ); + const src = await getAbsoluteHref( + `~gitbook/icon?size=medium&theme=${customization.themes.default}`, + true, + ); + return Icon; + })(); + return new ImageResponse( (
) : (
- {(() => { - if ('icon' in customization.favicon) - return ( - Icon - ); - if ('emoji' in customization.favicon) - return ( - - {String.fromCodePoint( - parseInt('0x' + customization.favicon.emoji), - )} - - ); - const src = getAbsoluteHref( - ctx, - `~gitbook/icon?size=medium&theme=${customization.themes.default}`, - true, - ); - return Icon; - })()} + {favicon}

{contentTitle}

)} diff --git a/packages/gitbook/src/app/(site)/fetch.ts b/packages/gitbook/src/app/(site)/fetch.ts index 4448a5bd9..9cebb3c6c 100644 --- a/packages/gitbook/src/app/(site)/fetch.ts +++ b/packages/gitbook/src/app/(site)/fetch.ts @@ -8,7 +8,6 @@ import { getSiteData, getSiteRedirectBySource, } from '@/lib/api'; -import { GitBookContext } from '@/lib/gitbook-context'; import { resolvePagePath, resolvePageId } from '@/lib/pages'; import { getSiteContentPointer } from '@/lib/pointer'; @@ -23,13 +22,13 @@ export interface PageIdParams { /** * Fetch all the data needed to render the content layout. */ -export async function fetchContentData(ctx: GitBookContext) { - const content = getSiteContentPointer(ctx); +export async function fetchContentData() { + const content = await getSiteContentPointer(); const [{ space, contentTarget, pages }, { customization, site, sections, spaces, scripts }] = await Promise.all([ - getSpaceContentData(ctx, content, content.siteShareKey), - getSiteData(ctx, content), + getSpaceContentData(content, content.siteShareKey), + getSiteData(content), ]); // we grab the space attached to the parent as it contains overriden customizations @@ -54,10 +53,10 @@ export async function fetchContentData(ctx: GitBookContext) { * Fetch all the data needed to render the content. * Optimized to fetch in parallel as much as possible. */ -export async function fetchPageData(ctx: GitBookContext, params: PagePathParams | PageIdParams) { - const contentData = await fetchContentData(ctx); +export async function fetchPageData(params: PagePathParams | PageIdParams) { + const contentData = await fetchContentData(); - const page = await resolvePage(ctx, { + const page = await resolvePage({ organizationId: contentData.space.organization, siteId: contentData.site.id, spaceId: contentData.contentTarget.spaceId, @@ -67,7 +66,7 @@ export async function fetchPageData(ctx: GitBookContext, params: PagePathParams params, }); const document = page?.page.documentId - ? await getDocument(ctx, contentData.space.id, page.page.documentId) + ? await getDocument(contentData.space.id, page.page.documentId) : null; return { @@ -81,18 +80,15 @@ export async function fetchPageData(ctx: GitBookContext, params: PagePathParams * 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( - ctx: GitBookContext, - input: { - organizationId: string; - siteId: string; - spaceId: string; - revisionId: string; - shareKey: string | undefined; - 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) { @@ -113,13 +109,13 @@ async function resolvePage( // 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(ctx, spaceId, revisionId, rawPathname); + 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(ctx, { + const resolvedSiteRedirect = await getSiteRedirectBySource({ organizationId, siteId, source: rawPathname.startsWith('/') ? rawPathname : `/${rawPathname}`, diff --git a/packages/gitbook/src/app/(site)/layout.tsx b/packages/gitbook/src/app/(site)/layout.tsx index 9aa2a62a6..431c97b7d 100644 --- a/packages/gitbook/src/app/(site)/layout.tsx +++ b/packages/gitbook/src/app/(site)/layout.tsx @@ -1,8 +1,5 @@ -import { headers } from 'next/headers'; - import { CustomizationRootLayout } from '@/components/RootLayout'; import { getSiteData } from '@/lib/api'; -import { getGitBookContextFromHeaders } from '@/lib/gitbook-context'; import { getSiteContentPointer } from '@/lib/pointer'; /** @@ -11,10 +8,9 @@ import { getSiteContentPointer } from '@/lib/pointer'; */ export default async function SiteRootLayout(props: { children: React.ReactNode }) { const { children } = props; - const ctx = getGitBookContextFromHeaders(await headers()); - const pointer = getSiteContentPointer(ctx); - const { customization } = await getSiteData(ctx, pointer); + const pointer = await getSiteContentPointer(); + const { customization } = await getSiteData(pointer); return ( {children} diff --git a/packages/gitbook/src/app/(space)/~gitbook/pdf/layout.tsx b/packages/gitbook/src/app/(space)/~gitbook/pdf/layout.tsx index 0a2d6e73f..3ce76b104 100644 --- a/packages/gitbook/src/app/(space)/~gitbook/pdf/layout.tsx +++ b/packages/gitbook/src/app/(space)/~gitbook/pdf/layout.tsx @@ -1,9 +1,7 @@ import { SpaceIntegrationScript } from '@gitbook/api'; -import { headers } from 'next/headers'; import { CustomizationRootLayout } from '@/components/RootLayout'; import { getSiteData, getSpaceCustomization } from '@/lib/api'; -import { getGitBookContextFromHeaders, GitBookContext } from '@/lib/gitbook-context'; import { getSiteOrSpacePointerForPDF } from './pointer'; @@ -12,13 +10,12 @@ import { getSiteOrSpacePointerForPDF } from './pointer'; * site or space and initializes the CustomizationRootLayout with it. */ export default async function PDFRootLayout(props: { children: React.ReactNode }) { - const ctx = getGitBookContextFromHeaders(await headers()); const { children } = props; - const pointer = getSiteOrSpacePointerForPDF(ctx); + const pointer = await getSiteOrSpacePointerForPDF(); const { customization } = await ('siteId' in pointer - ? getSiteData(ctx, pointer) - : getSpaceLayoutData(ctx)); + ? getSiteData(pointer) + : getSpaceLayoutData()); return ( {children} @@ -28,11 +25,14 @@ export default async function PDFRootLayout(props: { children: React.ReactNode } /** * Fetch all the layout data about a space at once. */ -async function getSpaceLayoutData(ctx: GitBookContext) { - const { customization } = await getSpaceCustomization(ctx); +async function getSpaceLayoutData() { + const [{ customization }, scripts] = await Promise.all([ + getSpaceCustomization(), + [] as SpaceIntegrationScript[], + ]); return { customization, - scripts: [] as SpaceIntegrationScript[], + scripts, }; } diff --git a/packages/gitbook/src/app/(space)/~gitbook/pdf/page.tsx b/packages/gitbook/src/app/(space)/~gitbook/pdf/page.tsx index ec9328490..d13201d53 100644 --- a/packages/gitbook/src/app/(space)/~gitbook/pdf/page.tsx +++ b/packages/gitbook/src/app/(space)/~gitbook/pdf/page.tsx @@ -8,7 +8,6 @@ import { } from '@gitbook/api'; import { Icon } from '@gitbook/icons'; import { Metadata } from 'next'; -import { headers } from 'next/headers'; import { notFound } from 'next/navigation'; import * as React from 'react'; @@ -24,7 +23,6 @@ import { getSpaceContentData, getSiteData, } from '@/lib/api'; -import { getGitBookContextFromHeaders } from '@/lib/gitbook-context'; import { getPagePDFContainerId, PageHrefContext, getAbsoluteHref } from '@/lib/links'; import { resolvePageId } from '@/lib/pages'; import { ContentRefContext, resolveContentRef } from '@/lib/references'; @@ -42,11 +40,10 @@ export const runtime = 'edge'; export const dynamic = 'force-dynamic'; export async function generateMetadata(): Promise { - const ctx = getGitBookContextFromHeaders(await headers()); - const pointer = getSiteOrSpacePointerForPDF(ctx); + const pointer = await getSiteOrSpacePointerForPDF(); const [space, { customization }] = await Promise.all([ - getSpace(ctx, pointer.spaceId, 'siteId' in pointer ? pointer.siteShareKey : undefined), - 'siteId' in pointer ? getSiteData(ctx, pointer) : getSpaceCustomization(ctx), + getSpace(pointer.spaceId, 'siteId' in pointer ? pointer.siteShareKey : undefined), + 'siteId' in pointer ? getSiteData(pointer) : getSpaceCustomization(), ]); return { @@ -61,20 +58,19 @@ export async function generateMetadata(): Promise { export default async function PDFHTMLOutput(props: { searchParams: Promise<{ [key: string]: string }>; }) { - const ctx = getGitBookContextFromHeaders(await headers()); - const pointer = getSiteOrSpacePointerForPDF(ctx); + const pointer = await getSiteOrSpacePointerForPDF(); const searchParams = new URLSearchParams(await props.searchParams); const pdfParams = getPDFSearchParams(new URLSearchParams(searchParams)); // Build current PDF URL and preserve all search params - let currentPDFUrl = getAbsoluteHref(ctx, '~gitbook/pdf', true); + let currentPDFUrl = await getAbsoluteHref('~gitbook/pdf', true); currentPDFUrl += '?' + searchParams.toString(); // Load the content, const [{ customization }, { space, contentTarget, pages: rootPages }] = await Promise.all([ - 'siteId' in pointer ? getSiteData(ctx, pointer) : getSpaceCustomization(ctx), - getSpaceContentData(ctx, pointer, 'siteId' in pointer ? pointer.siteShareKey : undefined), + 'siteId' in pointer ? getSiteData(pointer) : getSpaceCustomization(), + getSpaceContentData(pointer, 'siteId' in pointer ? pointer.siteShareKey : undefined), ]); const language = getSpaceLanguage(customization); @@ -93,7 +89,7 @@ export default async function PDFHTMLOutput(props: {
@@ -255,7 +250,7 @@ async function PDFPageDocument(props: { revisionId: refContext.revisionId, }, contentRefContext: refContext, - resolveContentRef: (ref) => resolveContentRef(ctx, ref, refContext), + resolveContentRef: (ref) => resolveContentRef(ref, refContext), getId: (id) => getPagePDFContainerId(page, id), }} /> diff --git a/packages/gitbook/src/app/(space)/~gitbook/pdf/pointer.ts b/packages/gitbook/src/app/(space)/~gitbook/pdf/pointer.ts index 926a9ff51..c1d28c328 100644 --- a/packages/gitbook/src/app/(space)/~gitbook/pdf/pointer.ts +++ b/packages/gitbook/src/app/(space)/~gitbook/pdf/pointer.ts @@ -1,5 +1,4 @@ import { SiteContentPointer, SpaceContentPointer } from '@/lib/api'; -import { GitBookContext } from '@/lib/gitbook-context'; import { getSiteContentPointer, getSpacePointer } from '@/lib/pointer'; /** @@ -9,12 +8,12 @@ import { getSiteContentPointer, getSpacePointer } from '@/lib/pointer'; * * This function returns the pointer depending on the context. */ -export function getSiteOrSpacePointerForPDF( - ctx: GitBookContext, -): SiteContentPointer | SpaceContentPointer { +export async function getSiteOrSpacePointerForPDF(): Promise< + SiteContentPointer | SpaceContentPointer +> { try { - return getSiteContentPointer(ctx); + return await getSiteContentPointer(); } catch (error) { - return getSpacePointer(ctx); + return getSpacePointer(); } } diff --git a/packages/gitbook/src/components/AdminToolbar/AdminToolbar.tsx b/packages/gitbook/src/components/AdminToolbar/AdminToolbar.tsx index ebff66775..dda6343a6 100644 --- a/packages/gitbook/src/components/AdminToolbar/AdminToolbar.tsx +++ b/packages/gitbook/src/components/AdminToolbar/AdminToolbar.tsx @@ -1,10 +1,8 @@ import { Space } from '@gitbook/api'; import { Icon } from '@gitbook/icons'; -import { headers } from 'next/headers'; import React from 'react'; import { getChangeRequest, getRevision, SiteContentPointer } from '@/lib/api'; -import { getGitBookContextFromHeaders } from '@/lib/gitbook-context'; import { tcls } from '@/lib/tailwind'; import { RefreshChangeRequestButton } from './RefreshChangeRequestButton'; @@ -70,9 +68,8 @@ export function AdminToolbar(props: AdminToolbarProps) { async function ChangeRequestToolbar(props: { spaceId: string; changeRequestId: string }) { const { spaceId, changeRequestId } = props; - const ctx = getGitBookContextFromHeaders(await headers()); - const changeRequest = await getChangeRequest(ctx, spaceId, changeRequestId); + const changeRequest = await getChangeRequest(spaceId, changeRequestId); return ( @@ -92,7 +89,6 @@ async function ChangeRequestToolbar(props: { spaceId: string; changeRequestId: s { const result = showPlaceholderAd - ? await renderAd({ source: 'placeholder', ipAndUserAgent }) + ? await renderAd({ source: 'placeholder' }) : realZoneId ? await renderAd({ placement, @@ -104,7 +100,6 @@ export function Ad({ zoneId: realZoneId, mode, source: 'live', - ipAndUserAgent, }) : undefined; @@ -120,7 +115,7 @@ export function Ad({ return () => { cancelled = true; }; - }, [visible, zoneId, ignore, placement, mode, siteAdsStatus, ipAndUserAgent]); + }, [visible, zoneId, ignore, placement, mode, siteAdsStatus]); return (
diff --git a/packages/gitbook/src/components/Ads/AdClassicRendering.tsx b/packages/gitbook/src/components/Ads/AdClassicRendering.tsx index 3d42ac604..dc2859a5b 100644 --- a/packages/gitbook/src/components/Ads/AdClassicRendering.tsx +++ b/packages/gitbook/src/components/Ads/AdClassicRendering.tsx @@ -1,7 +1,5 @@ -import { headers } from 'next/headers'; import * as React from 'react'; -import { getGitBookContextFromHeaders } from '@/lib/gitbook-context'; import { getResizedImageURL } from '@/lib/images'; import { tcls } from '@/lib/tailwind'; @@ -11,11 +9,10 @@ import { AdItem } from './types'; * Classic rendering for an ad. */ export async function AdClassicRendering({ ad }: { ad: AdItem }) { - const ctx = getGitBookContextFromHeaders(await headers()); const smallImgSrc = - 'smallImage' in ad ? getResizedImageURL(ctx, ad.smallImage, { width: 192, dpr: 2 }) : null; + 'smallImage' in ad ? await getResizedImageURL(ad.smallImage, { width: 192, dpr: 2 }) : null; const logoSrc = - 'logo' in ad ? getResizedImageURL(ctx, ad.logo, { width: 192 - 48, dpr: 2 }) : null; + 'logo' in ad ? await getResizedImageURL(ad.logo, { width: 192 - 48, dpr: 2 }) : null; return ( { - const { ip, userAgent } = ipAndUserAgent; + const { ip, userAgent } = await getUserAgentAndIp(); + const url = new URL(`https://srv.buysellads.com/ads/${zoneId}.json`); url.searchParams.set('segment', `placement:${placement}`); url.searchParams.set('v', 'true'); @@ -91,7 +86,9 @@ async function fetchAd({ return null; } -function getPlaceholderAd(options: FetchPlaceholderAdOptions): { ad: AdItem; ip: string } { +async function getPlaceholderAd(): Promise<{ ad: AdItem; ip: string }> { + const { ip } = await getUserAgentAndIp(); + return { ad: { active: '1', @@ -118,6 +115,20 @@ function getPlaceholderAd(options: FetchPlaceholderAdOptions): { ad: AdItem; ip: zoneid: '', zonekey: '', }, - ip: options.ipAndUserAgent.ip, + ip, }; } + +async function getUserAgentAndIp() { + const headersSet = await headers(); + const ip = + headersSet.get('x-gitbook-ipv4') ?? + headersSet.get('x-gitbook-ip') ?? + headersSet.get('cf-pseudo-ipv4') ?? + headersSet.get('cf-connecting-ip') ?? + headersSet.get('x-forwarded-for') ?? + ''; + const userAgent = headersSet.get('user-agent') ?? ''; + + return { ip, userAgent }; +} diff --git a/packages/gitbook/src/components/AutoRefreshContent/server-actions.ts b/packages/gitbook/src/components/AutoRefreshContent/server-actions.ts index afc574e28..9888af8fc 100644 --- a/packages/gitbook/src/components/AutoRefreshContent/server-actions.ts +++ b/packages/gitbook/src/components/AutoRefreshContent/server-actions.ts @@ -1,23 +1,15 @@ 'use server'; import { getChangeRequest } from '@/lib/api'; -import { GitBookContext } from '@/lib/gitbook-context'; /** * Return true if a change-request has been updated. */ -export async function hasContentBeenUpdated( - ctx: GitBookContext, - props: { - spaceId: string; - changeRequestId: string; - revisionId: string; - }, -) { - const changeRequest = await getChangeRequest.revalidate( - ctx, - props.spaceId, - props.changeRequestId, - ); +export async function hasContentBeenUpdated(props: { + spaceId: string; + changeRequestId: string; + revisionId: string; +}) { + const changeRequest = await getChangeRequest.revalidate(props.spaceId, props.changeRequestId); return changeRequest.revision !== props.revisionId; } diff --git a/packages/gitbook/src/components/AutoRefreshContent/useCheckForContentUpdate.ts b/packages/gitbook/src/components/AutoRefreshContent/useCheckForContentUpdate.ts index 9ad2ddc95..6048b3a15 100644 --- a/packages/gitbook/src/components/AutoRefreshContent/useCheckForContentUpdate.ts +++ b/packages/gitbook/src/components/AutoRefreshContent/useCheckForContentUpdate.ts @@ -1,9 +1,6 @@ 'use client'; import React from 'react'; -import { useEventCallback } from 'usehooks-ts'; - -import { GitBookContext } from '@/lib/gitbook-context'; import { hasContentBeenUpdated } from './server-actions'; @@ -11,23 +8,17 @@ import { hasContentBeenUpdated } from './server-actions'; * Return a callback to check if a change request has been updated and to refresh the page if it has. */ export function useCheckForContentUpdate(props: { - ctx: GitBookContext; spaceId: string; changeRequestId: string; revisionId: string; }) { - const { ctx, spaceId, changeRequestId, revisionId } = props; - const getCtx = useEventCallback(() => ctx); + const { spaceId, changeRequestId, revisionId } = props; return React.useCallback(async () => { - const updated = await hasContentBeenUpdated(getCtx(), { - spaceId, - changeRequestId, - revisionId, - }); + const updated = await hasContentBeenUpdated({ spaceId, changeRequestId, revisionId }); if (updated) { window.location.reload(); } - }, [spaceId, changeRequestId, revisionId, getCtx]); + }, [spaceId, changeRequestId, revisionId]); } diff --git a/packages/gitbook/src/components/DocumentView/BlockContentRef.tsx b/packages/gitbook/src/components/DocumentView/BlockContentRef.tsx index c96d89e2e..b98997efe 100644 --- a/packages/gitbook/src/components/DocumentView/BlockContentRef.tsx +++ b/packages/gitbook/src/components/DocumentView/BlockContentRef.tsx @@ -1,9 +1,7 @@ import { DocumentBlockContentRef } from '@gitbook/api'; -import { headers } from 'next/headers'; import { Card } from '@/components/primitives'; import { getSpaceCustomization, ignoreAPIError } from '@/lib/api'; -import { getGitBookContextFromHeaders } from '@/lib/gitbook-context'; import { ResolvedContentRef } from '@/lib/references'; import { BlockProps } from './Block'; @@ -54,9 +52,7 @@ async function SpaceRefCard( return null; } - const ctx = getGitBookContextFromHeaders(await headers()); - - const { customization: spaceCustomization } = await getSpaceCustomization(ctx); + const { customization: spaceCustomization } = await getSpaceCustomization(); const customFavicon = spaceCustomization?.favicon; const customEmoji = customFavicon && 'emoji' in customFavicon ? customFavicon.emoji : undefined; const customIcon = customFavicon && 'icon' in customFavicon ? customFavicon.icon : undefined; diff --git a/packages/gitbook/src/components/DocumentView/Embed.tsx b/packages/gitbook/src/components/DocumentView/Embed.tsx index dade807c9..a44d9d220 100644 --- a/packages/gitbook/src/components/DocumentView/Embed.tsx +++ b/packages/gitbook/src/components/DocumentView/Embed.tsx @@ -5,7 +5,6 @@ import ReactDOM from 'react-dom'; import { Card } from '@/components/primitives'; import { getEmbedByUrlInSpace, getEmbedByUrl } from '@/lib/api'; -import { getGitBookContextFromHeaders } from '@/lib/gitbook-context'; import { tcls } from '@/lib/tailwind'; import { BlockProps } from './Block'; @@ -13,15 +12,15 @@ import { Caption } from './Caption'; import { IntegrationBlock } from './Integration'; export async function Embed(props: BlockProps) { - const ctx = getGitBookContextFromHeaders(await headers()); const { block, context, ...otherProps } = props; - const nonce = ctx.nonce || undefined; + const headersList = await headers(); + const nonce = headersList.get('x-nonce') || undefined; ReactDOM.preload('https://cdn.iframe.ly/embed.js', { as: 'script', nonce }); const embed = await (context.content - ? getEmbedByUrlInSpace(ctx, context.content.spaceId, block.data.url) - : getEmbedByUrl(ctx, block.data.url)); + ? getEmbedByUrlInSpace(context.content.spaceId, block.data.url) + : getEmbedByUrl(block.data.url)); return ( diff --git a/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx b/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx index b9843035b..72ede9d59 100644 --- a/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx +++ b/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx @@ -1,11 +1,9 @@ import { ContentKitContext, DocumentBlockIntegration } from '@gitbook/api'; import { Icon } from '@gitbook/icons'; import { ContentKit, ContentKitOutput, ContentKitServerContext } from '@gitbook/react-contentkit'; -import { headers } from 'next/headers'; import { ignoreAPIError, renderIntegrationUi } from '@/lib/api'; import { INTEGRATIONS_HOST } from '@/lib/csp'; -import { getGitBookContextFromHeaders } from '@/lib/gitbook-context'; import { parseMarkdown } from '@/lib/markdown'; import { tcls } from '@/lib/tailwind'; @@ -47,8 +45,6 @@ export async function IntegrationBlock(props: BlockProps { 'use server'; - const output = await renderIntegrationUi(ctx, block.data.integration, request); + const output = await renderIntegrationUi(block.data.integration, request); return { children: , diff --git a/packages/gitbook/src/components/DocumentView/ReusableContent.tsx b/packages/gitbook/src/components/DocumentView/ReusableContent.tsx index 61ad0f210..e8ed31312 100644 --- a/packages/gitbook/src/components/DocumentView/ReusableContent.tsx +++ b/packages/gitbook/src/components/DocumentView/ReusableContent.tsx @@ -1,14 +1,11 @@ import { DocumentBlockReusableContent } from '@gitbook/api'; -import { headers } from 'next/headers'; import { getDocument } from '@/lib/api'; -import { getGitBookContextFromHeaders } from '@/lib/gitbook-context'; import { BlockProps } from './Block'; import { UnwrappedBlocks } from './Blocks'; export async function ReusableContent(props: BlockProps) { - const ctx = getGitBookContextFromHeaders(await headers()); const { block, context, ancestorBlocks } = props; if (!context.content) { @@ -20,11 +17,7 @@ export async function ReusableContent(props: BlockProps diff --git a/packages/gitbook/src/components/Header/HeaderLogo.tsx b/packages/gitbook/src/components/Header/HeaderLogo.tsx index 6262b4033..18c1201ee 100644 --- a/packages/gitbook/src/components/Header/HeaderLogo.tsx +++ b/packages/gitbook/src/components/Header/HeaderLogo.tsx @@ -5,10 +5,8 @@ import { SiteCustomizationSettings, Space, } from '@gitbook/api'; -import { headers } from 'next/headers'; import { Image } from '@/components/utils'; -import { getGitBookContextFromHeaders } from '@/lib/gitbook-context'; import { getAbsoluteHref } from '@/lib/links'; import { tcls } from '@/lib/tailwind'; import { getContentTitle } from '@/lib/utils'; @@ -27,9 +25,8 @@ interface HeaderLogoProps { */ export async function HeaderLogo(props: HeaderLogoProps) { - const ctx = getGitBookContextFromHeaders(await headers()); const { customization } = props; - const href = getAbsoluteHref(ctx, ''); + const href = await getAbsoluteHref(''); return ( {withPageFeedback ? ( - + ) : null} {customization.git.showEditLink && space.gitSync?.url && page.git ? ( @@ -229,7 +223,6 @@ export async function PageAside(props: {
- resolveContentRef(ctx, ref, context), - ); + const sections = await getDocumentSections(document, (ref) => resolveContentRef(ref, context)); return sections.length > 1 ? : null; } diff --git a/packages/gitbook/src/components/PageBody/PageBody.tsx b/packages/gitbook/src/components/PageBody/PageBody.tsx index aab553a19..699c018a2 100644 --- a/packages/gitbook/src/components/PageBody/PageBody.tsx +++ b/packages/gitbook/src/components/PageBody/PageBody.tsx @@ -5,14 +5,12 @@ import { SiteCustomizationSettings, Space, } from '@gitbook/api'; -import { headers } from 'next/headers'; import React from 'react'; import { getSpaceLanguage } from '@/intl/server'; import { t } from '@/intl/translate'; -import { ContentTarget, SiteContentPointer } from '@/lib/api'; +import { ContentTarget, SiteContentPointer, api } from '@/lib/api'; import { hasFullWidthBlock, isNodeEmpty } from '@/lib/document'; -import { getGitBookContextFromHeaders } from '@/lib/gitbook-context'; import { AncestorRevisionPage } from '@/lib/pages'; import { ContentRefContext, resolveContentRef } from '@/lib/references'; import { tcls } from '@/lib/tailwind'; @@ -27,7 +25,7 @@ import { TrackPageViewEvent } from '../Insights'; import { PageFeedbackForm } from '../PageFeedback'; import { DateRelative } from '../primitives'; -export async function PageBody(props: { +export function PageBody(props: { space: Space; pointer: SiteContentPointer; contentTarget: ContentTarget; @@ -38,7 +36,6 @@ export async function PageBody(props: { context: ContentRefContext; withPageFeedback: boolean; }) { - const ctx = getGitBookContextFromHeaders(await headers()); const { space, contentTarget, @@ -102,7 +99,7 @@ export async function PageBody(props: { content: contentTarget, contentRefContext: context, resolveContentRef: (ref, options) => - resolveContentRef(ctx, ref, context, options), + resolveContentRef(ref, context, options), }} /> @@ -143,7 +140,7 @@ export async function PageBody(props: {

) : null} {withPageFeedback ? ( - + ) : null}
diff --git a/packages/gitbook/src/components/PageBody/PageBodyBlankslate.tsx b/packages/gitbook/src/components/PageBody/PageBodyBlankslate.tsx index 581e624b7..40f8f27dc 100644 --- a/packages/gitbook/src/components/PageBody/PageBodyBlankslate.tsx +++ b/packages/gitbook/src/components/PageBody/PageBodyBlankslate.tsx @@ -1,8 +1,6 @@ import { RevisionPage, RevisionPageDocument, RevisionPageType } from '@gitbook/api'; -import { headers } from 'next/headers'; import { Card } from '@/components/primitives'; -import { getGitBookContextFromHeaders } from '@/lib/gitbook-context'; import { getPageHref } from '@/lib/links'; import { ContentRefContext, resolveContentRef } from '@/lib/references'; import { tcls } from '@/lib/tailwind'; @@ -17,7 +15,6 @@ export async function PageBodyBlankslate(props: { rootPages: RevisionPage[]; context: ContentRefContext; }) { - const ctx = getGitBookContextFromHeaders(await headers()); const { page, rootPages, context } = props; const pages = page.pages.filter((child) => @@ -38,7 +35,7 @@ export async function PageBodyBlankslate(props: { 'Unexpected computed page, it should have been computed in the API', ); } else if (child.type === RevisionPageType.Link) { - const resolved = await resolveContentRef(ctx, child.target, context); + const resolved = await resolveContentRef(child.target, context); if (!resolved) { return null; } @@ -56,7 +53,7 @@ export async function PageBodyBlankslate(props: { /> ); } else { - const href = getPageHref(ctx, rootPages, child); + const href = await getPageHref(rootPages, child); return ; } }), diff --git a/packages/gitbook/src/components/PageBody/PageCover.tsx b/packages/gitbook/src/components/PageBody/PageCover.tsx index 3b7f41632..0047cdf4f 100644 --- a/packages/gitbook/src/components/PageBody/PageCover.tsx +++ b/packages/gitbook/src/components/PageBody/PageCover.tsx @@ -1,8 +1,6 @@ import { RevisionPageDocument, RevisionPageDocumentCover } from '@gitbook/api'; -import { headers } from 'next/headers'; import { Image, ImageSize } from '@/components/utils'; -import { getGitBookContextFromHeaders } from '@/lib/gitbook-context'; import { ContentRefContext, resolveContentRef } from '@/lib/references'; import { tcls } from '@/lib/tailwind'; @@ -19,9 +17,8 @@ export async function PageCover(props: { cover: RevisionPageDocumentCover; context: ContentRefContext; }) { - const ctx = getGitBookContextFromHeaders(await headers()); const { as, page, cover, context } = props; - const resolved = cover.ref ? await resolveContentRef(ctx, cover.ref, context) : null; + const resolved = cover.ref ? await resolveContentRef(cover.ref, context) : null; return (
{ - const href = await getPageHref(ctx, pages, breadcrumb); + const href = await getPageHref(pages, breadcrumb); return (
  • diff --git a/packages/gitbook/src/components/PageFeedback/PageFeedbackForm.tsx b/packages/gitbook/src/components/PageFeedback/PageFeedbackForm.tsx index 27b89b851..5571d4e27 100644 --- a/packages/gitbook/src/components/PageFeedback/PageFeedbackForm.tsx +++ b/packages/gitbook/src/components/PageFeedback/PageFeedbackForm.tsx @@ -1,12 +1,10 @@ 'use client'; import { PageFeedbackRating } from '@gitbook/api'; -import { headers } from 'next/headers'; import React from 'react'; import { useLanguage } from '@/intl/client'; import { t, tString } from '@/intl/translate'; -import { getGitBookContextFromHeaders, GitBookContext } from '@/lib/gitbook-context'; import { tcls } from '@/lib/tailwind'; import { getVisitorId, useTrackEvent } from '../Insights'; @@ -16,12 +14,11 @@ import { postPageFeedback } from './server-actions'; * Form to submit feedback on a page. */ export function PageFeedbackForm(props: { - ctx: GitBookContext; orientation?: 'horizontal' | 'vertical'; pageId: string; className?: string; }) { - const { ctx, orientation = 'vertical', pageId, className } = props; + const { orientation = 'vertical', pageId, className } = props; const languages = useLanguage(); const trackEvent = useTrackEvent(); const [submitted, setSubmitted] = React.useState(false); @@ -29,7 +26,7 @@ export function PageFeedbackForm(props: { const onSubmit = async (rating: PageFeedbackRating) => { setSubmitted(true); const visitorId = await getVisitorId(); - await postPageFeedback(ctx, { pageId, visitorId, rating }); + await postPageFeedback({ pageId, visitorId, rating }); trackEvent({ type: 'page_post_feedback', diff --git a/packages/gitbook/src/components/PageFeedback/server-actions.ts b/packages/gitbook/src/components/PageFeedback/server-actions.ts index a86193520..248e03197 100644 --- a/packages/gitbook/src/components/PageFeedback/server-actions.ts +++ b/packages/gitbook/src/components/PageFeedback/server-actions.ts @@ -4,25 +4,23 @@ import { PageFeedbackRating } from '@gitbook/api'; import { assert } from 'ts-essentials'; import { api } from '@/lib/api'; -import { GitBookContext } from '@/lib/gitbook-context'; import { getSiteContentPointer } from '@/lib/pointer'; -export async function postPageFeedback( - ctx: GitBookContext, - args: { - pageId: string; - visitorId: string; - rating: PageFeedbackRating; - }, -) { - const { organizationId, siteId, siteSpaceId } = getSiteContentPointer(ctx); +export async function postPageFeedback(args: { + pageId: string; + visitorId: string; + rating: PageFeedbackRating; +}) { + const { organizationId, siteId, siteSpaceId } = await getSiteContentPointer(); assert( siteSpaceId, `No siteSpaceId in pointer. organizationId: ${organizationId}, siteId: ${siteId}, pageId: ${args.pageId}`, ); - await api(ctx).client.orgs.createSitesPageFeedback( + const apiCtx = await api(); + + await apiCtx.client.orgs.createSitesPageFeedback( organizationId, siteId, siteSpaceId, diff --git a/packages/gitbook/src/components/Search/SearchAskAnswer.tsx b/packages/gitbook/src/components/Search/SearchAskAnswer.tsx index 6334327a5..018575132 100644 --- a/packages/gitbook/src/components/Search/SearchAskAnswer.tsx +++ b/packages/gitbook/src/components/Search/SearchAskAnswer.tsx @@ -2,7 +2,6 @@ import { Icon } from '@gitbook/icons'; import React from 'react'; -import { useEventCallback } from 'usehooks-ts'; import { Loading } from '@/components/primitives'; import { useLanguage } from '@/intl/client'; @@ -10,7 +9,6 @@ import { t } from '@/intl/translate'; import { TranslationLanguage } from '@/intl/translations'; import { iterateStreamResponse } from '@/lib/actions'; import { SiteContentPointer } from '@/lib/api'; -import { GitBookContext } from '@/lib/gitbook-context'; import { tcls } from '@/lib/tailwind'; import { AskAnswerResult, AskAnswerSource, streamAskQuestion } from './server-actions'; @@ -34,19 +32,14 @@ export type SearchAskState = /** * Fetch and render the answers to a question. */ -export function SearchAskAnswer(props: { - ctx: GitBookContext; - pointer: SiteContentPointer; - query: string; -}) { - const { ctx, pointer, query } = props; +export function SearchAskAnswer(props: { pointer: SiteContentPointer; query: string }) { + const { pointer, query } = props; const language = useLanguage(); const trackEvent = useTrackEvent(); const [, setSearchState] = useSearch(); const [askState, setAskState] = useSearchAskContext(); const { organizationId, siteId, siteSpaceId } = pointer; - const getCtx = useEventCallback(() => ctx); React.useEffect(() => { let cancelled = false; @@ -59,13 +52,7 @@ export function SearchAskAnswer(props: { query, }); - const response = streamAskQuestion( - getCtx(), - organizationId, - siteId, - siteSpaceId ?? null, - query, - ); + const response = streamAskQuestion(organizationId, siteId, siteSpaceId ?? null, query); const stream = iterateStreamResponse(response); // When we pass in "ask" mode, the query could still be updated by the client @@ -94,16 +81,7 @@ export function SearchAskAnswer(props: { cancelled = true; } }; - }, [ - organizationId, - siteId, - siteSpaceId, - query, - setAskState, - setSearchState, - trackEvent, - getCtx, - ]); + }, [organizationId, siteId, siteSpaceId, query, setAskState, setSearchState, trackEvent]); React.useEffect(() => { return () => { diff --git a/packages/gitbook/src/components/Search/SearchModal.tsx b/packages/gitbook/src/components/Search/SearchModal.tsx index a887e1d83..ca165e731 100644 --- a/packages/gitbook/src/components/Search/SearchModal.tsx +++ b/packages/gitbook/src/components/Search/SearchModal.tsx @@ -8,7 +8,6 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { tString, useLanguage } from '@/intl/client'; import { SiteContentPointer } from '@/lib/api'; -import { GitBookContext } from '@/lib/gitbook-context'; import { tcls } from '@/lib/tailwind'; import { SearchAskAnswer } from './SearchAskAnswer'; @@ -19,7 +18,6 @@ import { SearchState, UpdateSearchState, useSearch } from './useSearch'; import { LoadingPane } from '../primitives/LoadingPane'; interface SearchModalProps { - ctx: GitBookContext; spaceId: string; revisionId: string; spaceTitle: string; @@ -310,7 +308,6 @@ function SearchModalBody( {!state.ask || !withAsk ? ( ) : null} {state.query && state.ask && withAsk ? ( - + ) : null} ); diff --git a/packages/gitbook/src/components/Search/SearchResults.tsx b/packages/gitbook/src/components/Search/SearchResults.tsx index a96de2c62..2b9bf3640 100644 --- a/packages/gitbook/src/components/Search/SearchResults.tsx +++ b/packages/gitbook/src/components/Search/SearchResults.tsx @@ -1,12 +1,11 @@ 'use client'; + import { captureException } from '@sentry/nextjs'; import assertNever from 'assert-never'; import React from 'react'; -import { useEventCallback } from 'usehooks-ts'; import { t, useLanguage } from '@/intl/client'; import { SiteContentPointer } from '@/lib/api'; -import { GitBookContext } from '@/lib/gitbook-context'; import { tcls } from '@/lib/tailwind'; import { SearchPageResultItem } from './SearchPageResultItem'; @@ -40,7 +39,6 @@ type ResultType = */ export const SearchResults = React.forwardRef(function SearchResults( props: { - ctx: GitBookContext; children?: React.ReactNode; query: string; spaceId: string; @@ -52,8 +50,7 @@ export const SearchResults = React.forwardRef(function SearchResults( }, ref: React.Ref, ) { - const { ctx, children, query, pointer, spaceId, revisionId, withAsk, global, onSwitchToAsk } = - props; + const { children, query, pointer, spaceId, revisionId, withAsk, global, onSwitchToAsk } = props; const language = useLanguage(); const trackEvent = useTrackEvent(); @@ -64,7 +61,6 @@ export const SearchResults = React.forwardRef(function SearchResults( const [cursor, setCursor] = React.useState(null); const refs = React.useRef<(null | HTMLAnchorElement)[]>([]); const suggestedQuestionsRef = React.useRef(null); - const getCtx = useEventCallback(() => ctx); React.useEffect(() => { if (!query) { @@ -81,7 +77,7 @@ export const SearchResults = React.forwardRef(function SearchResults( let cancelled = false; setResultsState({ results: [], fetching: true }); - getRecommendedQuestions(getCtx(), spaceId).then((questions) => { + getRecommendedQuestions(spaceId).then((questions) => { if (!questions) { if (!cancelled) { setResultsState({ results: [], fetching: false }); @@ -115,8 +111,8 @@ export const SearchResults = React.forwardRef(function SearchResults( let cancelled = false; const timeout = setTimeout(async () => { const results = await (global - ? searchAllSiteContent(getCtx(), query, pointer) - : searchSiteSpaceContent(getCtx(), query, pointer, revisionId)); + ? searchAllSiteContent(query, pointer) + : searchSiteSpaceContent(query, pointer, revisionId)); if (cancelled) { return; @@ -146,7 +142,7 @@ export const SearchResults = React.forwardRef(function SearchResults( clearTimeout(timeout); }; } - }, [query, global, pointer, spaceId, revisionId, withAsk, trackEvent, getCtx]); + }, [query, global, pointer, spaceId, revisionId, withAsk, trackEvent]); const results: ResultType[] = React.useMemo(() => { if (!withAsk) { diff --git a/packages/gitbook/src/components/Search/server-actions.tsx b/packages/gitbook/src/components/Search/server-actions.tsx index 148c9d06a..c82d48bf8 100644 --- a/packages/gitbook/src/components/Search/server-actions.tsx +++ b/packages/gitbook/src/components/Search/server-actions.tsx @@ -7,7 +7,6 @@ import { assert } from 'ts-essentials'; import { streamResponse } from '@/lib/actions'; import * as api from '@/lib/api'; -import { GitBookContext } from '@/lib/gitbook-context'; import { getAbsoluteHref, getPageHref } from '@/lib/links'; import { resolvePageId } from '@/lib/pages'; import { filterOutNullable } from '@/lib/typescript'; @@ -50,18 +49,15 @@ export interface AskAnswerResult { /** * Search for content in a site by scoping the search to all content, a specific spaces or current space. */ -async function searchSiteContent( - ctx: GitBookContext, - args: { - pointer: api.SiteContentPointer; - query: string; - scope: - | { mode: 'all' } - | { mode: 'current'; siteSpaceId: string } - | { mode: 'specific'; siteSpaceIds: string[] }; - cacheBust?: string; - }, -): Promise { +async function searchSiteContent(args: { + pointer: api.SiteContentPointer; + query: string; + scope: + | { mode: 'all' } + | { mode: 'current'; siteSpaceId: string } + | { mode: 'specific'; siteSpaceIds: string[] }; + cacheBust?: string; +}): Promise { const { pointer, scope, query, cacheBust } = args; if (query.length <= 1) { @@ -74,8 +70,8 @@ async function searchSiteContent( (scope.mode === 'specific' && scope.siteSpaceIds.length > 1); const [searchResults, siteData] = await Promise.all([ - api.searchSiteContent(ctx, pointer.organizationId, pointer.siteId, query, scope, cacheBust), - needsStructure ? api.getSiteData(ctx, pointer) : null, + api.searchSiteContent(pointer.organizationId, pointer.siteId, query, scope, cacheBust), + needsStructure ? api.getSiteData(pointer) : null, ]); const siteStructure = siteData?.structure; @@ -98,31 +94,38 @@ async function searchSiteContent( if (siteSpaces) { // We are searching all of this Site's content - return searchResults.items - .map((spaceItem) => { - const siteSpace = siteSpaces.find( - (siteSpace) => siteSpace.space.id === spaceItem.id, - ); + return ( + await Promise.all( + searchResults.items.map(async (spaceItem) => { + const siteSpace = siteSpaces.find( + (siteSpace) => siteSpace.space.id === spaceItem.id, + ); - return spaceItem.pages.map((item) => transformSitePageResult(ctx, item, siteSpace)); - }) - .flat(2); + return Promise.all( + spaceItem.pages.map((item) => transformSitePageResult(item, siteSpace)), + ); + }), + ) + ).flat(2); } - return searchResults.items - .map((spaceItem) => spaceItem.pages.map((item) => transformPageResult(ctx, item))) - .flat(2); + return ( + await Promise.all( + searchResults.items.map((spaceItem) => { + return Promise.all(spaceItem.pages.map((item) => transformPageResult(item))); + }), + ) + ).flat(2); } /** * Server action to search content in the entire site. */ export async function searchAllSiteContent( - ctx: GitBookContext, query: string, pointer: api.SiteContentPointer, ): Promise { - return await searchSiteContent(ctx, { + return await searchSiteContent({ pointer, query, scope: { mode: 'all' }, @@ -133,7 +136,6 @@ export async function searchAllSiteContent( * Server action to search content in a space. */ export async function searchSiteSpaceContent( - ctx: GitBookContext, query: string, pointer: api.SiteContentPointer, revisionId: string, @@ -141,7 +143,7 @@ export async function searchSiteSpaceContent( const siteSpaceId = pointer.siteSpaceId; assert(siteSpaceId, 'Expected siteSpaceId for searchSiteSpaceContent'); - return await searchSiteContent(ctx, { + return await searchSiteContent({ pointer, query, // If we have a siteSectionId that means its a sections site use `current` mode @@ -158,13 +160,13 @@ export async function searchSiteSpaceContent( * Server action to ask a question in a space. */ export const streamAskQuestion = streamResponse(async function* ( - ctx: GitBookContext, organizationId: string, siteId: string, siteSpaceId: string | null, question: string, ) { - const stream = api.api(ctx).client.orgs.streamAskInSite( + const apiCtx = await api.api(); + const stream = apiCtx.client.orgs.streamAskInSite( organizationId, siteId, { @@ -198,9 +200,7 @@ export const streamAskQuestion = streamResponse(async function* ( if (!spacePromises.has(source.space)) { spacePromises.set( source.space, - api.getRevisionPages(ctx, source.space, source.revision, { - metadata: false, - }), + api.getRevisionPages(source.space, source.revision, { metadata: false }), ); } @@ -222,56 +222,48 @@ export const streamAskQuestion = streamResponse(async function* ( return map; }, new Map()); }); - yield transformAnswer(ctx, chunk.answer, pages); + yield await transformAnswer(chunk.answer, pages); } }); /** * List suggested questions for a space. */ -export async function getRecommendedQuestions( - ctx: GitBookContext, - spaceId: string, -): Promise { - const data = await api.getRecommendedQuestionsInSpace(ctx, spaceId); - if (!data.questions) { - captureException(new Error('Expected questions in getRecommendedQuestions'), { - extra: { data }, - }); - return []; - } +export async function getRecommendedQuestions(spaceId: string): Promise { + const data = await api.getRecommendedQuestionsInSpace(spaceId); return data.questions; } -function transformAnswer( - ctx: GitBookContext, +async function transformAnswer( answer: SearchAIAnswer, spacePages: Map, -): AskAnswerResult { - const sources = answer.sources - .map((source) => { - if (source.type !== 'page') { - return null; - } +): Promise { + const sources = ( + await Promise.all( + answer.sources.map(async (source) => { + if (source.type !== 'page') { + return null; + } - const pages = spacePages.get(source.space); + const pages = spacePages.get(source.space); - if (!pages) { - return null; - } + if (!pages) { + return null; + } - const page = resolvePageId(pages, source.page); - if (!page) { - return null; - } + const page = resolvePageId(pages, source.page); + if (!page) { + return null; + } - return { - id: source.page, - title: page.page.title, - href: getPageHref(ctx, pages, page.page), - }; - }) - .filter(filterOutNullable); + return { + id: source.page, + title: page.page.title, + href: await getPageHref(pages, page.page), + }; + }), + ) + ).filter(filterOutNullable); return { body: @@ -292,19 +284,16 @@ function transformAnswer( }; } -function transformSectionsAndPage( - ctx: GitBookContext, - args: { - item: SearchPageResult; - space?: Space; - spaceURL?: string; - }, -): [ComputedPageResult, ComputedSectionResult[]] { +async function transformSectionsAndPage(args: { + item: SearchPageResult; + space?: Space; + spaceURL?: string; +}): Promise<[ComputedPageResult, ComputedSectionResult[]]> { const { item, space, spaceURL } = args; // Resolve a relative path to an absolute URL // if the search result is relative to another space, we use the space URL - const getURL = (path: string, spaceURL?: string) => { + const getURL = async (path: string, spaceURL?: string) => { if (spaceURL) { if (!spaceURL.endsWith('/')) { spaceURL += '/'; @@ -314,36 +303,33 @@ function transformSectionsAndPage( } return spaceURL + path; } else { - return getAbsoluteHref(ctx, path); + return getAbsoluteHref(path); } }; - const sections = - item.sections?.map((section) => ({ + const sections = await Promise.all( + item.sections?.map>(async (section) => ({ type: 'section', id: item.id + '/' + section.id, title: section.title, - href: getURL(section.path, spaceURL), + href: await getURL(section.path, spaceURL), body: section.body, - })) ?? []; + })) ?? [], + ); const page: ComputedPageResult = { type: 'page', id: item.id, title: item.title, - href: getURL(item.path, spaceURL), + href: await getURL(item.path, spaceURL), spaceTitle: space?.title, }; return [page, sections]; } -function transformSitePageResult( - ctx: GitBookContext, - item: SearchPageResult, - siteSpace?: SiteSpace, -) { - const [page, sections] = transformSectionsAndPage(ctx, { +async function transformSitePageResult(item: SearchPageResult, siteSpace?: SiteSpace) { + const [page, sections] = await transformSectionsAndPage({ item, space: siteSpace?.space, spaceURL: siteSpace?.urls.published, @@ -352,8 +338,8 @@ function transformSitePageResult( return [page, ...sections]; } -function transformPageResult(ctx: GitBookContext, item: SearchPageResult, space?: Space) { - const [page, sections] = transformSectionsAndPage(ctx, { +async function transformPageResult(item: SearchPageResult, space?: Space) { + const [page, sections] = await transformSectionsAndPage({ item, space, spaceURL: space?.urls.published ?? space?.urls.app, diff --git a/packages/gitbook/src/components/Space/SpaceIcon.tsx b/packages/gitbook/src/components/Space/SpaceIcon.tsx index 6563a148c..4bee2bad1 100644 --- a/packages/gitbook/src/components/Space/SpaceIcon.tsx +++ b/packages/gitbook/src/components/Space/SpaceIcon.tsx @@ -1,8 +1,6 @@ import { CustomizationThemedURL } from '@gitbook/api'; -import { headers } from 'next/headers'; import { Image } from '@/components/utils'; -import { getGitBookContextFromHeaders } from '@/lib/gitbook-context'; import { getAbsoluteHref } from '@/lib/links'; import { Emoji } from '../primitives'; @@ -16,7 +14,6 @@ export async function SpaceIcon( 'sources' >, ) { - const ctx = getGitBookContextFromHeaders(await headers()); const { icon, emoji, alt, ...imageProps } = props; if (emoji && !icon) { @@ -40,16 +37,14 @@ export async function SpaceIcon( } : { light: { - src: getAbsoluteHref( - ctx, + src: await getAbsoluteHref( '~gitbook/icon?size=medium&theme=light', true, ), size: { width: 256, height: 256 }, }, dark: { - src: getAbsoluteHref( - ctx, + src: await getAbsoluteHref( '~gitbook/icon?size=medium&theme=dark', true, ), diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx index 68d7ddf7e..8c2027665 100644 --- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx +++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx @@ -9,7 +9,6 @@ import { SiteCustomizationSettings, Space, } from '@gitbook/api'; -import { headers } from 'next/headers'; import React from 'react'; import { Footer } from '@/components/Footer'; @@ -20,10 +19,10 @@ import { TableOfContents } from '@/components/TableOfContents'; import { getSpaceLanguage } from '@/intl/server'; import { t } from '@/intl/translate'; import { api, ContentTarget, type SectionsList, SiteContentPointer } from '@/lib/api'; -import { getGitBookContextFromHeaders } from '@/lib/gitbook-context'; import { ContentRefContext } from '@/lib/references'; import { tcls } from '@/lib/tailwind'; import { shouldTrackEvents } from '@/lib/tracking'; +import { getCurrentVisitorToken } from '@/lib/visitor-token'; import { SpacesDropdown } from '../Header/SpacesDropdown'; import { InsightsProvider } from '../Insights'; @@ -44,7 +43,6 @@ export async function SpaceLayout(props: { ancestors: Array; children: React.ReactNode; }) { - const ctx = getGitBookContextFromHeaders(await headers()); const { space, contentTarget, @@ -76,9 +74,9 @@ export async function SpaceLayout(props: { 'sidebar' in customization.styling && customization.styling.sidebar.background === CustomizationSidebarBackgroundStyle.Filled, }; - const apiHost = api(ctx).client.endpoint; - const visitorAuthToken = ctx.visitorToken; - const enabled = shouldTrackEvents(ctx); + const apiHost = (await api()).client.endpoint; + const visitorAuthToken = await getCurrentVisitorToken(); + const enabled = await shouldTrackEvents(); return ( ; context: ContentRefContext; }) { - const ctx = getGitBookContextFromHeaders(await headers()); const { rootPages, page, ancestors, context } = props; - const href = await getPageHref(ctx, rootPages, page); + const href = await getPageHref(rootPages, page); return (
  • diff --git a/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx index d68dccac0..7a10e9f2b 100644 --- a/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx @@ -1,19 +1,16 @@ import { RevisionPageLink } from '@gitbook/api'; import { Icon } from '@gitbook/icons'; -import { headers } from 'next/headers'; import { Link } from '@/components/primitives'; -import { getGitBookContextFromHeaders } from '@/lib/gitbook-context'; import { ContentRefContext, resolveContentRef } from '@/lib/references'; import { tcls } from '@/lib/tailwind'; import { TOCPageIcon } from './TOCPageIcon'; export async function PageLinkItem(props: { page: RevisionPageLink; context: ContentRefContext }) { - const ctx = getGitBookContextFromHeaders(await headers()); const { page, context } = props; - const resolved = await resolveContentRef(ctx, page.target, context); + const resolved = await resolveContentRef(page.target, context); return (
  • diff --git a/packages/gitbook/src/components/utils/Image.tsx b/packages/gitbook/src/components/utils/Image.tsx index 62190a781..3ae58ed59 100644 --- a/packages/gitbook/src/components/utils/Image.tsx +++ b/packages/gitbook/src/components/utils/Image.tsx @@ -1,8 +1,6 @@ /* eslint-disable @next/next/no-img-element */ -import { headers } from 'next/headers'; import ReactDOM from 'react-dom'; -import { getGitBookContextFromHeaders, GitBookContext } from '@/lib/gitbook-context'; import { checkIsHttpURL, getImageSize, getResizedImageURLFactory } from '@/lib/images'; import { ClassValue, tcls } from '@/lib/tailwind'; @@ -193,8 +191,6 @@ async function ImagePictureSized( } & ImageCommonProps >, ) { - const ctx = getGitBookContextFromHeaders(await headers()); - const { source, sizes, @@ -214,7 +210,7 @@ async function ImagePictureSized( throw new Error('You must provide at least one size for the image.'); } - const attrs = await getImageAttributes(ctx, { sizes, source, quality, resize }); + const attrs = await getImageAttributes({ sizes, source, quality, resize }); const canBeFetched = checkIsHttpURL(attrs.src); const fetchPriority = canBeFetched ? getFetchPriority(priority) : undefined; const loading = priority === 'lazy' ? 'lazy' : undefined; @@ -247,15 +243,12 @@ async function ImagePictureSized( * Get the attributes for an image. * src, srcSet, sizes, width, height, etc. */ -async function getImageAttributes( - ctx: GitBookContext, - params: { - sizes: ImageResponsiveSize[]; - source: ImageSourceSized; - quality: number; - resize: boolean; - }, -): Promise<{ +async function getImageAttributes(params: { + sizes: ImageResponsiveSize[]; + source: ImageSourceSized; + quality: number; + resize: boolean; +}): Promise<{ src: string; srcSet?: string; sizes?: string; @@ -265,7 +258,7 @@ async function getImageAttributes( const { sizes, source, quality, resize } = params; let src = source.src; - const getURL = resize ? getResizedImageURLFactory(ctx, source.src) : null; + const getURL = resize ? await getResizedImageURLFactory(source.src) : null; if (!getURL) { return { diff --git a/packages/gitbook/src/lib/api.ts b/packages/gitbook/src/lib/api.ts index 856f3d334..0f2159df4 100644 --- a/packages/gitbook/src/lib/api.ts +++ b/packages/gitbook/src/lib/api.ts @@ -32,7 +32,6 @@ import { noCacheFetchOptions, parseCacheResponse, } from './cache'; -import { GitBookContext } from './gitbook-context'; import { defaultCustomizationForSpace } from './utils'; /** @@ -111,14 +110,16 @@ export const DEFAULT_API_ENDPOINT = process.env.GITBOOK_API_URL ?? 'https://api. /** * Create a new API client with a token. */ -export function apiWithToken( +export async function apiWithToken( apiToken: string, contextId: string | undefined, - ctx: GitBookContext, -): GitBookAPIContext { +): Promise { + const headersList = await headers(); + const apiEndpoint = headersList.get('x-gitbook-api') ?? DEFAULT_API_ENDPOINT; + const gitbook = new GitBookAPI({ authToken: apiToken, - endpoint: ctx.apiEndpoint, + endpoint: apiEndpoint, userAgent: userAgent(), }); @@ -128,19 +129,23 @@ export function apiWithToken( /** * Create an API client for the current request. */ -export function api(ctx: GitBookContext): GitBookAPIContext { +export async function api(): Promise { const existing = apiSyncStorage.getStore(); if (existing) { return existing; } - if (!ctx.apiToken) { + const headersList = await headers(); + const apiToken = headersList.get('x-gitbook-token'); + const contextId = headersList.get('x-gitbook-token-context') ?? undefined; + + if (!apiToken) { throw new Error( 'Missing GitBook API token, please check that the request is correctly processed by the middleware', ); } - return apiWithToken(ctx.apiToken, ctx.apiTokenContextId ?? undefined, ctx); + return apiWithToken(apiToken, contextId); } /** @@ -172,14 +177,15 @@ export type PublishedContentWithCache = */ export const getUserById = cache({ name: 'api.getUserById', - tag: (_ctx, userId) => + tag: (userId) => getAPICacheTag({ tag: 'user', user: userId, }), - get: async (ctx: GitBookContext, userId: string, options: CacheFunctionOptions) => { + get: async (userId: string, options: CacheFunctionOptions) => { try { - const response = await api(ctx).client.users.getUserById(userId, { + const apiCtx = await api(); + const response = await apiCtx.client.users.getUserById(userId, { signal: options.signal, ...noCacheFetchOptions, }); @@ -204,13 +210,12 @@ export const getUserById = cache({ */ export const getPublishedContentByUrl = cache({ name: 'api.getPublishedContentByUrl.v4', - tag: (_ctx, url) => + tag: (url) => getAPICacheTag({ tag: 'url', hostname: new URL(url).hostname, }), get: async ( - ctx: GitBookContext, url: string, visitorAuthToken: string | undefined, // Prefer undefined for a better cache key. @@ -218,7 +223,8 @@ export const getPublishedContentByUrl = cache({ options: CacheFunctionOptions, ) => { try { - const response = await api(ctx).client.urls.getPublishedContentByUrl( + const apiCtx = await api(); + const response = await apiCtx.client.urls.getPublishedContentByUrl( { url, visitorAuthToken, @@ -266,14 +272,10 @@ export const getPublishedContentByUrl = cache({ */ export const getSpace = cache({ name: 'api.getSpace', - tag: (_ctx, spaceId) => getAPICacheTag({ tag: 'space', space: spaceId }), - get: async ( - ctx: GitBookContext, - spaceId: string, - shareKey: string | undefined, - options: CacheFunctionOptions, - ) => { - const response = await api(ctx).client.spaces.getSpaceById( + tag: (spaceId) => getAPICacheTag({ tag: 'space', space: spaceId }), + get: async (spaceId: string, shareKey: string | undefined, options: CacheFunctionOptions) => { + const apiCtx = await api(); + const response = await apiCtx.client.spaces.getSpaceById( spaceId, { shareKey, @@ -294,22 +296,14 @@ export const getSpace = cache({ */ export const getChangeRequest = cache({ name: 'api.getChangeRequest', - tag: (_ctx, spaceId, changeRequestId) => + tag: (spaceId, changeRequestId) => getAPICacheTag({ tag: 'change-request', space: spaceId, changeRequest: changeRequestId }), - get: async ( - ctx: GitBookContext, - spaceId: string, - changeRequestId: string, - options: CacheFunctionOptions, - ) => { - const response = await api(ctx).client.spaces.getChangeRequestById( - spaceId, - changeRequestId, - { - ...noCacheFetchOptions, - signal: options.signal, - }, - ); + get: async (spaceId: string, changeRequestId: string, options: CacheFunctionOptions) => { + const apiCtx = await api(); + const response = await apiCtx.client.spaces.getChangeRequestById(spaceId, changeRequestId, { + ...noCacheFetchOptions, + signal: options.signal, + }); return cacheResponse(response, { ttl: 60 * 60, revalidateBefore: 10 * 60, @@ -327,22 +321,27 @@ interface GetRevisionOptions { metadata: boolean; } +const getAPIContextId = async () => { + const apiCtx = await api(); + return apiCtx.contextId; +}; + /** * Get a revision by its ID. */ export const getRevision = cache({ name: 'api.getRevision.v2', - tag: (_ctx, spaceId, revisionId) => + tag: (spaceId, revisionId) => getAPICacheTag({ tag: 'revision', space: spaceId, revision: revisionId }), - getKeySuffix: (ctx) => api(ctx).contextId, + getKeySuffix: getAPIContextId, get: async ( - ctx: GitBookContext, spaceId: string, revisionId: string, fetchOptions: GetRevisionOptions, options: CacheFunctionOptions, ) => { - const response = await api(ctx).client.spaces.getRevisionById( + const apiCtx = await api(); + const response = await apiCtx.client.spaces.getRevisionById( spaceId, revisionId, { @@ -364,17 +363,17 @@ export const getRevision = cache({ */ export const getRevisionPages = cache({ name: 'api.getRevisionPages.v4', - tag: (_ctx, spaceId, revisionId) => + tag: (spaceId, revisionId) => getAPICacheTag({ tag: 'revision', space: spaceId, revision: revisionId }), - getKeySuffix: (ctx) => api(ctx).contextId, + getKeySuffix: getAPIContextId, get: async ( - ctx: GitBookContext, spaceId: string, revisionId: string, fetchOptions: GetRevisionOptions, options: CacheFunctionOptions, ) => { - const response = await api(ctx).client.spaces.listPagesInRevisionById( + const apiCtx = await api(); + const response = await apiCtx.client.spaces.listPagesInRevisionById( spaceId, revisionId, { @@ -399,11 +398,10 @@ export const getRevisionPages = cache({ */ export const getRevisionPageByPath = cache({ name: 'api.getRevisionPageByPath.v3', - tag: (_ctx, spaceId, revisionId) => + tag: (spaceId, revisionId) => getAPICacheTag({ tag: 'revision', space: spaceId, revision: revisionId }), - getKeySuffix: (ctx) => api(ctx).contextId, + getKeySuffix: getAPIContextId, get: async ( - ctx: GitBookContext, spaceId: string, revisionId: string, pagePath: string, @@ -412,7 +410,8 @@ export const getRevisionPageByPath = cache({ const encodedPath = encodeURIComponent(pagePath); try { - const response = await api(ctx).client.spaces.getPageInRevisionByPath( + const apiCtx = await api(); + const response = await apiCtx.client.spaces.getPageInRevisionByPath( spaceId, revisionId, encodedPath, @@ -445,17 +444,17 @@ export const getRevisionPageByPath = cache({ */ const getRevisionFileById = cache({ name: 'api.getRevisionFile.v3', - tag: (_ctx, spaceId, revisionId) => + tag: (spaceId, revisionId) => getAPICacheTag({ tag: 'revision', space: spaceId, revision: revisionId }), get: async ( - ctx: GitBookContext, spaceId: string, revisionId: string, fileId: string, options: CacheFunctionOptions, ) => { try { - const response = await api(ctx).client.spaces.getFileInRevisionById( + const apiCtx = await api(); + const response = await apiCtx.client.spaces.getFileInRevisionById( spaceId, revisionId, fileId, @@ -481,18 +480,18 @@ const getRevisionFileById = cache({ const getRevisionReusableContentById = cache({ name: 'api.getRevisionReusableContentById.v1', - tag: (_ctx, spaceId, revisionId) => + tag: (spaceId, revisionId) => getAPICacheTag({ tag: 'revision', space: spaceId, revision: revisionId }), - getKeySuffix: (ctx) => api(ctx).contextId, + getKeySuffix: getAPIContextId, get: async ( - ctx: GitBookContext, spaceId: string, revisionId: string, reusableContentId: string, options: CacheFunctionOptions, ) => { try { - const response = await api(ctx).client.spaces.getReusableContentInRevisionById( + const apiCtx = await api(); + const response = await apiCtx.client.spaces.getReusableContentInRevisionById( spaceId, revisionId, reusableContentId, @@ -522,17 +521,13 @@ const getRevisionReusableContentById = cache({ */ const getRevisionAllFiles = cache({ name: 'api.getRevisionAllFiles.v2', - tag: (_ctx, spaceId, revisionId) => + tag: (spaceId, revisionId) => getAPICacheTag({ tag: 'revision', space: spaceId, revision: revisionId }), - get: async ( - ctx: GitBookContext, - spaceId: string, - revisionId: string, - options: CacheFunctionOptions, - ) => { + get: async (spaceId: string, revisionId: string, options: CacheFunctionOptions) => { const response = await getAll( async (params) => { - const response = await api(ctx).client.spaces.listFilesInRevisionById( + const apiCtx = await api(); + const response = await apiCtx.client.spaces.listFilesInRevisionById( spaceId, revisionId, { @@ -566,39 +561,35 @@ const getRevisionAllFiles = cache({ * The approach is optimized to use the entire list of files in the revision if it has been fetched * or to use a per-file approach if not. */ -export const getRevisionFile = batch<[GitBookContext, string, string, string], RevisionFile | null>( +export const getRevisionFile = batch<[string, string, string], RevisionFile | null>( async (executions) => { - const [ctx, spaceId, revisionId] = executions[0]; + const [spaceId, revisionId] = executions[0]; - const hasRevisionInMemory = await getRevision.hasInMemory(ctx, spaceId, revisionId, { + const hasRevisionInMemory = await getRevision.hasInMemory(spaceId, revisionId, { metadata: false, }); - const hasRevisionFilesInMemory = await getRevisionAllFiles.hasInMemory( - ctx, - spaceId, - revisionId, - ); + const hasRevisionFilesInMemory = await getRevisionAllFiles.hasInMemory(spaceId, revisionId); // When fetching more than 5 files, we should bundle them all into one call to get the entire revision if (executions.length > 5 || hasRevisionFilesInMemory || hasRevisionInMemory) { let files: Record = {}; if (hasRevisionInMemory) { - const revision = await getRevision(ctx, spaceId, revisionId, { metadata: false }); + const revision = await getRevision(spaceId, revisionId, { metadata: false }); files = {}; revision.files.forEach((file) => { files[file.id] = file; }); } else { - files = await getRevisionAllFiles(ctx, spaceId, revisionId); + files = await getRevisionAllFiles(spaceId, revisionId); } - return executions.map(([ctx, spaceId, revisionId, fileId]) => files[fileId] ?? null); + return executions.map(([spaceId, revisionId, fileId]) => files[fileId] ?? null); } else { // Fetch file individually return Promise.all( - executions.map(([ctx, spaceId, revisionId, fileId]) => - getRevisionFileById(ctx, spaceId, revisionId, fileId), + executions.map(([spaceId, revisionId, fileId]) => + getRevisionFileById(spaceId, revisionId, fileId), ), ); } @@ -606,13 +597,13 @@ export const getRevisionFile = batch<[GitBookContext, string, string, string], R { delay: 20, groupBy: (spaceId, revisionId) => spaceId + '/' + revisionId, - skip: async (ctx, spaceId, revisionId, fileId) => { + skip: async (spaceId, revisionId, fileId) => { return ( - (await getRevision.hasInMemory(ctx, spaceId, revisionId, { + (await getRevision.hasInMemory(spaceId, revisionId, { metadata: false, })) || - (await getRevisionAllFiles.hasInMemory(ctx, spaceId, revisionId)) || - (await getRevisionFileById.hasInMemory(ctx, spaceId, revisionId, fileId)) + (await getRevisionAllFiles.hasInMemory(spaceId, revisionId)) || + (await getRevisionFileById.hasInMemory(spaceId, revisionId, fileId)) ); }, }, @@ -622,24 +613,23 @@ export const getRevisionFile = batch<[GitBookContext, string, string, string], R * Get reusable content in a revision. */ export const getReusableContent = async ( - ctx: GitBookContext, spaceId: string, revisionId: string, reusableContentId: string, ): Promise => { - const hasRevisionInMemory = await getRevision.hasInMemory(ctx, spaceId, revisionId, { + const hasRevisionInMemory = await getRevision.hasInMemory(spaceId, revisionId, { metadata: false, }); if (hasRevisionInMemory) { - const revision = await getRevision(ctx, spaceId, revisionId, { metadata: false }); + const revision = await getRevision(spaceId, revisionId, { metadata: false }); return ( revision.reusableContents.find( (reusableContent) => reusableContent.id === reusableContentId, ) ?? null ); } else { - return getRevisionReusableContentById(ctx, spaceId, revisionId, reusableContentId); + return getRevisionReusableContentById(spaceId, revisionId, reusableContentId); } }; @@ -648,16 +638,12 @@ export const getReusableContent = async ( */ export const getDocument = cache({ name: 'api.getDocument.v2', - tag: (_ctx, spaceId, documentId) => + tag: (spaceId, documentId) => getAPICacheTag({ tag: 'document', space: spaceId, document: documentId }), - getKeySuffix: (ctx) => api(ctx).contextId, - get: async ( - ctx: GitBookContext, - spaceId: string, - documentId: string, - options: CacheFunctionOptions, - ) => { - const response = await api(ctx).client.spaces.getDocumentById( + getKeySuffix: getAPIContextId, + get: async (spaceId: string, documentId: string, options: CacheFunctionOptions) => { + const apiCtx = await api(); + const response = await apiCtx.client.spaces.getDocumentById( spaceId, documentId, { @@ -688,10 +674,9 @@ function validateSiteRedirectSource(source: string) { */ export const getSiteRedirectBySource = cache({ name: 'api.getSiteRedirectBySource', - tag: (_ctx, { siteId }) => getAPICacheTag({ tag: 'site', site: siteId }), - getKeySuffix: (ctx) => api(ctx).contextId, + tag: ({ siteId }) => getAPICacheTag({ tag: 'site', site: siteId }), + getKeySuffix: getAPIContextId, get: async ( - ctx: GitBookContext, args: { organizationId: string; siteId: string; @@ -709,7 +694,8 @@ export const getSiteRedirectBySource = cache({ }; } try { - const response = await api(ctx).client.orgs.getSiteRedirectBySource( + const apiCtx = await api(); + const response = await apiCtx.client.orgs.getSiteRedirectBySource( args.organizationId, args.siteId, { @@ -749,15 +735,11 @@ export const getSiteRedirectBySource = cache({ */ export const getSite = cache({ name: 'api.getSite', - tag: (_ctx, _organizationId, siteId) => getAPICacheTag({ tag: 'site', site: siteId }), - getKeySuffix: (ctx) => api(ctx).contextId, - get: async ( - ctx: GitBookContext, - organizationId: string, - siteId: string, - options: CacheFunctionOptions, - ) => { - const response = await api(ctx).client.orgs.getSiteById(organizationId, siteId, { + tag: (organizationId, siteId) => getAPICacheTag({ tag: 'site', site: siteId }), + getKeySuffix: getAPIContextId, + get: async (organizationId: string, siteId: string, options: CacheFunctionOptions) => { + const apiCtx = await api(); + const response = await apiCtx.client.orgs.getSiteById(organizationId, siteId, { ...noCacheFetchOptions, signal: options.signal, }); @@ -772,10 +754,9 @@ export const getSite = cache({ */ export const getPublishedContentSite = cache({ name: 'api.getPublishedContentSite', - tag: (_ctx, { siteId }) => getAPICacheTag({ tag: 'site', site: siteId }), - getKeySuffix: (ctx) => api(ctx).contextId, + tag: ({ siteId }) => getAPICacheTag({ tag: 'site', site: siteId }), + getKeySuffix: getAPIContextId, get: async ( - ctx: GitBookContext, args: { organizationId: string; siteId: string /** Site share key that can be used as context to resolve site space published urls */; @@ -783,7 +764,8 @@ export const getPublishedContentSite = cache({ }, options: CacheFunctionOptions, ) => { - const response = await api(ctx).client.orgs.getPublishedContentSite( + const apiCtx = await api(); + const response = await apiCtx.client.orgs.getPublishedContentSite( args.organizationId, args.siteId, { @@ -831,7 +813,6 @@ function parseSiteSectionsList(siteSectionId: string, sections: SiteSection[]) { * experience for the site (structure, customizations, scripts etc) */ export async function getSiteData( - ctx: GitBookContext, pointer: Pick< SiteContentPointer, 'organizationId' | 'siteId' | 'siteSectionId' | 'siteSpaceId' | 'siteShareKey' @@ -842,7 +823,7 @@ export async function getSiteData( structure: siteStructure, customizations, scripts, - } = await getPublishedContentSite(ctx, { + } = await getPublishedContentSite({ organizationId: pointer.organizationId, siteId: pointer.siteId, siteShareKey: pointer.siteShareKey, @@ -869,8 +850,7 @@ export async function getSiteData( const spaces = siteSpaces ?? (sections ? parseSpacesFromSiteSpaces(sections.section.siteSpaces) : []); - const customization = getActiveCustomizationSettings( - ctx, + const customization = await getActiveCustomizationSettings( pointer.siteSpaceId ? customizations.siteSpaces[pointer.siteSpaceId] : customizations.site, ); @@ -887,12 +867,13 @@ export async function getSiteData( /** * Get the customization settings for a space from the API. */ -export function getSpaceCustomization(ctx: GitBookContext): { +export async function getSpaceCustomization(): Promise<{ customization: CustomizationSettings; -} { +}> { + const headersList = await headers(); const raw = defaultCustomizationForSpace(); - const extend = ctx.customization; + const extend = headersList.get('x-gitbook-customization'); if (extend) { try { const parsed = rison.decode_object>(extend); @@ -916,9 +897,10 @@ export function getSpaceCustomization(ctx: GitBookContext): { */ export const getCollection = cache({ name: 'api.getCollection', - tag: (_ctx, collectionId) => getAPICacheTag({ tag: 'collection', collection: collectionId }), - get: async (ctx: GitBookContext, collectionId: string, options: CacheFunctionOptions) => { - const response = await api(ctx).client.collections.getCollectionById(collectionId, { + tag: (collectionId) => getAPICacheTag({ tag: 'collection', collection: collectionId }), + get: async (collectionId: string, options: CacheFunctionOptions) => { + const apiCtx = await api(); + const response = await apiCtx.client.collections.getCollectionById(collectionId, { ...noCacheFetchOptions, signal: options.signal, }); @@ -933,10 +915,11 @@ export const getCollection = cache({ */ export const getCollectionSpaces = cache({ name: 'api.getCollectionSpaces', - tag: (_ctx, collectionId) => getAPICacheTag({ tag: 'collection', collection: collectionId }), - get: async (ctx: GitBookContext, collectionId: string, options: CacheFunctionOptions) => { + tag: (collectionId) => getAPICacheTag({ tag: 'collection', collection: collectionId }), + get: async (collectionId: string, options: CacheFunctionOptions) => { const response = await getAll(async (params) => { - const response = await api(ctx).client.collections.listSpacesInCollectionById( + const apiCtx = await api(); + const response = await apiCtx.client.collections.listSpacesInCollectionById( collectionId, params, { @@ -962,22 +945,19 @@ export const getCollectionSpaces = cache({ * instead of calling the individual functions. */ export async function getSpaceContentData( - ctx: GitBookContext, pointer: SpaceContentPointer, shareKey: string | undefined, ) { const [space, changeRequest] = await Promise.all([ - getSpace(ctx, pointer.spaceId, shareKey), - pointer.changeRequestId - ? getChangeRequest(ctx, pointer.spaceId, pointer.changeRequestId) - : null, + getSpace(pointer.spaceId, shareKey), + pointer.changeRequestId ? getChangeRequest(pointer.spaceId, pointer.changeRequestId) : null, ]); const contentTarget: ContentTarget = { spaceId: pointer.spaceId, revisionId: changeRequest?.revision ?? pointer.revisionId ?? space.revision, }; - const pages = await getRevisionPages(ctx, space.id, contentTarget.revisionId, { + const pages = await getRevisionPages(space.id, contentTarget.revisionId, { // We only care about the Git metadata when the Git sync is enabled // otherwise we can optimize performance by not fetching it metadata: !!space.gitSync, @@ -995,17 +975,17 @@ export async function getSpaceContentData( */ export const searchSpaceContent = cache({ name: 'api.searchSpaceContent', - tag: (_ctx, spaceId) => getAPICacheTag({ tag: 'space', space: spaceId }), - getKeySuffix: (ctx) => api(ctx).contextId, + tag: (spaceId) => getAPICacheTag({ tag: 'space', space: spaceId }), + getKeySuffix: getAPIContextId, get: async ( - ctx: GitBookContext, spaceId: string, /** The revision ID is used as a cache bust key, to avoid revalidating lot of cache entries by tags */ revisionId: string, query: string, options: CacheFunctionOptions, ) => { - const response = await api(ctx).client.spaces.searchSpaceContent( + const apiCtx = await api(); + const response = await apiCtx.client.spaces.searchSpaceContent( spaceId, { query }, { @@ -1022,15 +1002,11 @@ export const searchSpaceContent = cache({ */ export const searchParentContent = cache({ name: 'api.searchParentContent', - tag: (_ctx, spaceId) => getAPICacheTag({ tag: 'space', space: spaceId }), - getKeySuffix: (ctx) => api(ctx).contextId, - get: async ( - ctx: GitBookContext, - parentId: string, - query: string, - options: CacheFunctionOptions, - ) => { - const response = await api(ctx).client.search.searchContent( + tag: (spaceId) => getAPICacheTag({ tag: 'space', space: spaceId }), + getKeySuffix: getAPIContextId, + get: async (parentId: string, query: string, options: CacheFunctionOptions) => { + const apiCtx = await api(); + const response = await apiCtx.client.search.searchContent( { query }, { ...noCacheFetchOptions, @@ -1048,10 +1024,9 @@ export const searchParentContent = cache({ */ export const searchSiteContent = cache({ name: 'api.searchSiteContent', - tag: (_ctx, _organizationId, siteId) => getAPICacheTag({ tag: 'site', site: siteId }), - getKeySuffix: (ctx) => api(ctx).contextId, + tag: (organizationId, siteId) => getAPICacheTag({ tag: 'site', site: siteId }), + getKeySuffix: getAPIContextId, get: async ( - ctx: GitBookContext, organizationId: string, siteId: string, query: string, @@ -1063,7 +1038,8 @@ export const searchSiteContent = cache({ cacheBust?: string, options?: CacheFunctionOptions, ) => { - const response = await api(ctx).client.orgs.searchSiteContent( + const apiCtx = await api(); + const response = await apiCtx.client.orgs.searchSiteContent( organizationId, siteId, { @@ -1088,9 +1064,10 @@ export const searchSiteContent = cache({ */ export const getRecommendedQuestionsInSpace = cache({ name: 'api.getRecommendedQuestionsInSpace', - tag: (_ctx, spaceId) => getAPICacheTag({ tag: 'space', space: spaceId }), - get: async (ctx: GitBookContext, spaceId: string, options: CacheFunctionOptions) => { - const response = await api(ctx).client.spaces.getRecommendedQuestionsInSpace(spaceId, { + tag: (spaceId) => getAPICacheTag({ tag: 'space', space: spaceId }), + get: async (spaceId: string, options: CacheFunctionOptions) => { + const apiCtx = await api(); + const response = await apiCtx.client.spaces.getRecommendedQuestionsInSpace(spaceId, { ...noCacheFetchOptions, signal: options.signal, }); @@ -1103,15 +1080,14 @@ export const getRecommendedQuestionsInSpace = cache({ */ export const renderIntegrationUi = cache({ name: 'api.renderIntegrationUi', - tag: (_ctx, integrationName) => - getAPICacheTag({ tag: 'integration', integration: integrationName }), + tag: (integrationName) => getAPICacheTag({ tag: 'integration', integration: integrationName }), get: async ( - ctx: GitBookContext, integrationName: string, request: RequestRenderIntegrationUI, options: CacheFunctionOptions, ) => { - const response = await api(ctx).client.integrations.renderIntegrationUiWithPost( + const apiCtx = await api(); + const response = await apiCtx.client.integrations.renderIntegrationUiWithPost( integrationName, request, { @@ -1129,8 +1105,9 @@ export const renderIntegrationUi = cache({ */ export const getEmbedByUrl = cache({ name: 'api.getEmbedByUrl', - get: async (ctx: GitBookContext, url: string, options: CacheFunctionOptions) => { - const response = await api(ctx).client.urls.getEmbedByUrl( + get: async (url: string, options: CacheFunctionOptions) => { + const apiCtx = await api(); + const response = await apiCtx.client.urls.getEmbedByUrl( { url }, { ...noCacheFetchOptions, @@ -1146,14 +1123,10 @@ export const getEmbedByUrl = cache({ */ export const getEmbedByUrlInSpace = cache({ name: 'api.getEmbedByUrlInSpace', - tag: (_ctx, spaceId) => getAPICacheTag({ tag: 'space', space: spaceId }), - get: async ( - ctx: GitBookContext, - spaceId: string, - url: string, - options: CacheFunctionOptions, - ) => { - const response = await api(ctx).client.spaces.getEmbedByUrlInSpace( + tag: (spaceId) => getAPICacheTag({ tag: 'space', space: spaceId }), + get: async (spaceId: string, url: string, options: CacheFunctionOptions) => { + const apiCtx = await api(); + const response = await apiCtx.client.spaces.getEmbedByUrlInSpace( spaceId, { url }, { @@ -1324,11 +1297,11 @@ async function getAll( * Selects the customization settings from the x-gitbook-customization header if present, * otherwise returns the original API-provided settings. */ -function getActiveCustomizationSettings( - ctx: GitBookContext, +async function getActiveCustomizationSettings( settings: SiteCustomizationSettings, -): SiteCustomizationSettings { - const extend = ctx.customization; +): Promise { + const headersList = await headers(); + const extend = headersList.get('x-gitbook-customization'); if (extend) { try { const parsedSettings = rison.decode_object(extend); diff --git a/packages/gitbook/src/lib/cache/cache.test.ts b/packages/gitbook/src/lib/cache/cache.test.ts index 4e3a4c896..0870d66da 100644 --- a/packages/gitbook/src/lib/cache/cache.test.ts +++ b/packages/gitbook/src/lib/cache/cache.test.ts @@ -74,7 +74,7 @@ describe('cache', () => { describe('cache with suffix key', () => { const impl = mock((arg: string) => 'test-' + arg); const getKeySuffixImpl: Mock['getKeySuffix']>> = - mock(() => hash({ test: 1 })); + mock(async () => hash({ test: 1 })); let fn: CacheFunction<[string], string>; let testId = 0; @@ -112,7 +112,7 @@ describe('cache with suffix key', () => { expect(impl).toHaveBeenCalledTimes(2); - getKeySuffixImpl.mockImplementation(() => hash({ test: 2 })); + getKeySuffixImpl.mockImplementation(async () => hash({ test: 2 })); expect(await fn('a')).toEqual('test-a'); expect(impl).toHaveBeenCalledTimes(3); @@ -120,7 +120,7 @@ describe('cache with suffix key', () => { it('should preserve behaviour even when the returned key suffix is undefined', async () => { // Start with the returned suffix being undefined - getKeySuffixImpl.mockImplementation(() => undefined); + getKeySuffixImpl.mockImplementation(async () => undefined); const result = await fn('a'); expect(result).toEqual('test-a'); @@ -128,19 +128,19 @@ describe('cache with suffix key', () => { expect(impl).toHaveBeenCalledTimes(1); // The returned suffix changes so we should get the value computed by the function - getKeySuffixImpl.mockImplementation(() => hash({ test: 1 })); + getKeySuffixImpl.mockImplementation(async () => hash({ test: 1 })); expect(await fn('a')).toEqual('test-a'); expect(impl).toHaveBeenCalledTimes(2); // The returned suffix is undefined again so we should get the value from a previous cache entry - getKeySuffixImpl.mockImplementation(() => undefined); + getKeySuffixImpl.mockImplementation(async () => undefined); expect(await fn('a')).toEqual('test-a'); expect(impl).toHaveBeenCalledTimes(2); // The returned suffix goes back to a previous hash so we should the value from a previous cache entry - getKeySuffixImpl.mockImplementation(() => hash({ test: 1 })); + getKeySuffixImpl.mockImplementation(async () => hash({ test: 1 })); expect(await fn('a')).toEqual('test-a'); expect(impl).toHaveBeenCalledTimes(2); diff --git a/packages/gitbook/src/lib/cache/cache.ts b/packages/gitbook/src/lib/cache/cache.ts index 1c6423370..cef72fe99 100644 --- a/packages/gitbook/src/lib/cache/cache.ts +++ b/packages/gitbook/src/lib/cache/cache.ts @@ -57,7 +57,7 @@ export interface CacheDefinition { getKeyArgs?: (args: Args) => any[]; /** Returns a precomputed hash that is used alongside arguments to generate the cache key */ - getKeySuffix?: (...args: Args) => string | undefined; + getKeySuffix?: () => Promise; /** Default ttl (in seconds) */ defaultTtl?: number; @@ -245,7 +245,7 @@ export function cache( const [args, { signal }] = extractCacheFunctionOptions(rawArgs); const cacheArgs = cacheDef.getKeyArgs ? cacheDef.getKeyArgs(args) : args; - const cacheKeySuffix = cacheDef.getKeySuffix ? cacheDef.getKeySuffix(...args) : undefined; + const cacheKeySuffix = cacheDef.getKeySuffix ? await cacheDef.getKeySuffix() : undefined; const key = getCacheKey(cacheDef.name, cacheArgs, cacheKeySuffix); return await trace( @@ -263,7 +263,7 @@ export function cache( cacheFn.revalidate = async (...rawArgs: Args | [...Args, CacheFunctionOptions]) => { const [args, { signal }] = extractCacheFunctionOptions(rawArgs); const cacheArgs = cacheDef.getKeyArgs ? cacheDef.getKeyArgs(args) : args; - const cacheKeySuffix = cacheDef.getKeySuffix ? cacheDef.getKeySuffix(...args) : undefined; + const cacheKeySuffix = cacheDef.getKeySuffix ? await cacheDef.getKeySuffix() : undefined; const key = getCacheKey(cacheDef.name, cacheArgs, cacheKeySuffix); const result = await revalidate(key, signal, ...args); @@ -272,7 +272,7 @@ export function cache( cacheFn.hasInMemory = async (...args: Args) => { const cacheArgs = cacheDef.getKeyArgs ? cacheDef.getKeyArgs(args) : args; - const cacheKeySuffix = cacheDef.getKeySuffix ? cacheDef.getKeySuffix(...args) : undefined; + const cacheKeySuffix = cacheDef.getKeySuffix ? await cacheDef.getKeySuffix() : undefined; const key = getCacheKey(cacheDef.name, cacheArgs, cacheKeySuffix); const tag = cacheDef.tag?.(...args); diff --git a/packages/gitbook/src/lib/csp.ts b/packages/gitbook/src/lib/csp.ts index 49a0d6428..663a2a14a 100644 --- a/packages/gitbook/src/lib/csp.ts +++ b/packages/gitbook/src/lib/csp.ts @@ -3,17 +3,18 @@ import { merge } from 'content-security-policy-merger'; import { headers } from 'next/headers'; import { assetsDomain } from './assets'; -import { GitBookContext } from './gitbook-context'; import { filterOutNullable } from './typescript'; /** * Get the current nonce for the current request. */ -export function getContentSecurityPolicyNonce(ctx: GitBookContext): string { - if (!ctx.nonce) { +export async function getContentSecurityPolicyNonce(): Promise { + const headersList = await headers(); + const nonce = headersList.get('x-nonce'); + if (!nonce) { throw new Error('No nonce found in headers'); } - return ctx.nonce; + return nonce; } /** diff --git a/packages/gitbook/src/lib/gitbook-context.ts b/packages/gitbook/src/lib/gitbook-context.ts deleted file mode 100644 index 78f57ab07..000000000 --- a/packages/gitbook/src/lib/gitbook-context.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { headers } from 'next/headers'; - -import { DEFAULT_API_ENDPOINT } from './api'; -import { formatBasePath } from './links'; - -export type GitBookContext = { - theme: string | null; - nonce: string | null; - visitorToken: string | null; - trackPageViews: boolean; - apiEndpoint: string; - apiToken: string | null; - apiTokenContextId: string | null; - customization: string | null; - host: string; - basePath: string; - protocol: string; - originBasePath: string; - - // Content pointers - spaceId: string | null; - siteId: string | null; - organizationId: string | null; - siteSpaceId: string | null; - siteSectionId: string | null; - siteShareKey: string | null; - contentRevisionId: string | null; - changeRequestId: string | null; - - // Indexation - searchIndexation: boolean; -}; - -/** - * Extract the gitbook context from the headers. - */ -export function getGitBookContextFromHeaders(headers: Headers): GitBookContext { - return { - theme: headers.get('x-gitbook-theme'), - nonce: headers.get('x-nonce'), - visitorToken: headers.get('x-gitbook-visitor-token'), - trackPageViews: headers.has('x-gitbook-track-page-views'), - apiEndpoint: headers.get('x-gitbook-api') ?? DEFAULT_API_ENDPOINT, - apiToken: headers.get('x-gitbook-token'), - apiTokenContextId: headers.get('x-gitbook-token-context'), - customization: headers.get('x-gitbook-customization'), - basePath: formatBasePath(headers.get('x-gitbook-basepath')), - host: headers.get('x-gitbook-host') ?? headers.get('host') ?? '', - protocol: headers.get('x-forwarded-proto') ?? 'https', - originBasePath: headers.get('x-gitbook-origin-basepath') ?? '/', - spaceId: headers.get('x-gitbook-content-space'), - siteId: headers.get('x-gitbook-content-site'), - organizationId: headers.get('x-gitbook-content-organization'), - siteSpaceId: headers.get('x-gitbook-content-site-space'), - siteSectionId: headers.get('x-gitbook-content-site-section'), - siteShareKey: headers.get('x-gitbook-content-site-share-key'), - contentRevisionId: headers.get('x-gitbook-content-revision'), - changeRequestId: headers.get('x-gitbook-content-changerequest'), - searchIndexation: headers.has('x-gitbook-search-indexation'), - }; -} - -export type IpAndUserAgent = { - ip: string; - userAgent: string; -}; - -/** - * Read the IP and User-Agent from the headers. - * This function can only be called at the top level of a component or route. - */ -export function getIpAndUserAgentFromHeaders(headers: Headers): IpAndUserAgent { - const ip = - headers.get('x-gitbook-ipv4') ?? - headers.get('x-gitbook-ip') ?? - headers.get('cf-pseudo-ipv4') ?? - headers.get('cf-connecting-ip') ?? - headers.get('x-forwarded-for') ?? - ''; - const userAgent = headers.get('user-agent') ?? ''; - - return { ip, userAgent }; -} diff --git a/packages/gitbook/src/lib/image-signatures.ts b/packages/gitbook/src/lib/image-signatures.ts index 1caee65a3..08e042e85 100644 --- a/packages/gitbook/src/lib/image-signatures.ts +++ b/packages/gitbook/src/lib/image-signatures.ts @@ -3,7 +3,7 @@ import 'server-only'; import fnv1a from '@sindresorhus/fnv1a'; import type { MaybePromise } from 'p-map'; -import { GitBookContext } from './gitbook-context'; +import { getHost } from './links'; /** * GitBook has supported different version of image signing in the past. To maintain backwards @@ -19,14 +19,12 @@ export const CURRENT_SIGNATURE_VERSION: SignatureVersion = '2'; /** * A mapping of signature versions to signature functions. */ -const IMAGE_SIGNATURE_FUNCTIONS: Record< - SignatureVersion, - (ctx: GitBookContext, input: string) => MaybePromise -> = { - '0': generateSignatureV0, - '1': generateSignatureV1, - '2': generateSignatureV2, -}; +const IMAGE_SIGNATURE_FUNCTIONS: Record MaybePromise> = + { + '0': generateSignatureV0, + '1': generateSignatureV1, + '2': generateSignatureV2, + }; export function isSignatureVersion(input: string): input is SignatureVersion { return Object.keys(IMAGE_SIGNATURE_FUNCTIONS).includes(input); @@ -36,12 +34,11 @@ export function isSignatureVersion(input: string): input is SignatureVersion { * Verify a signature of an image URL */ export async function verifyImageSignature( - ctx: GitBookContext, input: string, { signature, version }: { signature: string; version: SignatureVersion }, ): Promise { const generator = IMAGE_SIGNATURE_FUNCTIONS[version]; - const generated = await generator(ctx, input); + const generated = await generator(input); return generated === signature; } @@ -51,14 +48,11 @@ export async function verifyImageSignature( * This function is sync. If you need to implement an async version of image signing, you'll need to change * ths signature of this fn and where it's used. */ -export function generateImageSignature( - ctx: GitBookContext, - input: string, -): { +export async function generateImageSignature(input: string): Promise<{ signature: string; version: SignatureVersion; -} { - const result = generateSignatureV2(ctx, input); +}> { + const result = await generateSignatureV2(input); return { signature: result, version: CURRENT_SIGNATURE_VERSION }; } @@ -69,8 +63,8 @@ const fnv1aUtf8Buffer = new Uint8Array(512); * Generate a signature for an image. * The signature is relative to the current site being rendered to avoid serving images from other sites on the same domain. */ -function generateSignatureV2(ctx: GitBookContext, input: string): string { - const hostName = ctx.host; +async function generateSignatureV2(input: string): Promise { + const hostName = await getHost(); const all = [ input, hostName, // The hostname is used to avoid serving images from other sites on the same domain @@ -89,7 +83,7 @@ const fnv1aUtf8BufferV1 = new Uint8Array(512); * When setting it in a URL, we use version '1' for the 'sv' querystring parameneter * to know that it was the algorithm that was used. */ -function generateSignatureV1(ctx: GitBookContext, input: string): string { +function generateSignatureV1(input: string): string { const all = [input, process.env.GITBOOK_IMAGE_RESIZE_SIGNING_KEY].filter(Boolean).join(':'); return fnv1a(all, { utf8Buffer: fnv1aUtf8BufferV1 }).toString(16); } @@ -99,7 +93,7 @@ function generateSignatureV1(ctx: GitBookContext, input: string): string { * We still need it to validate older signatures that were generated without versioning * but still exist in previously generated and cached content. */ -async function generateSignatureV0(ctx: GitBookContext, input: string): Promise { +async function generateSignatureV0(input: string): Promise { const all = [input, process.env.GITBOOK_IMAGE_RESIZE_SIGNING_KEY].filter(Boolean).join(':'); const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(all)); diff --git a/packages/gitbook/src/lib/images.ts b/packages/gitbook/src/lib/images.ts index c911c5309..83406cf33 100644 --- a/packages/gitbook/src/lib/images.ts +++ b/packages/gitbook/src/lib/images.ts @@ -2,7 +2,6 @@ import 'server-only'; import { noCacheFetchOptions } from '@/lib/cache/http'; -import { GitBookContext } from './gitbook-context'; import { generateImageSignature } from './image-signatures'; import { getRootUrl } from './links'; import { getImageAPIUrl } from './urls'; @@ -85,16 +84,17 @@ interface ResizeImageOptions { /** * Create a function to get resized image URLs for a given image URL. */ -export function getResizedImageURLFactory( - ctx: GitBookContext, +export async function getResizedImageURLFactory( input: string, -): ((options: ResizeImageOptions) => string) | null { +): Promise<((options: ResizeImageOptions) => string) | null> { if (!checkIsSizableImageURL(input)) { return null; } - const { signature, version } = generateImageSignature(ctx, input); - const rootUrl = getRootUrl(ctx); + const [{ signature, version }, rootUrl] = await Promise.all([ + generateImageSignature(input), + getRootUrl(), + ]); return (options) => { const url = new URL('/~gitbook/image', rootUrl); @@ -124,12 +124,11 @@ export function getResizedImageURLFactory( * Create a new URL for an image with resized parameters. * The URL is signed and verified by the server. */ -export function getResizedImageURL( - ctx: GitBookContext, +export async function getResizedImageURL( input: string, options: ResizeImageOptions, -): string { - const factory = getResizedImageURLFactory(ctx, input); +): Promise { + const factory = await getResizedImageURLFactory(input); return factory?.(options) ?? input; } diff --git a/packages/gitbook/src/lib/links.ts b/packages/gitbook/src/lib/links.ts index 7eebdba77..5bbf04f55 100644 --- a/packages/gitbook/src/lib/links.ts +++ b/packages/gitbook/src/lib/links.ts @@ -8,7 +8,6 @@ import { } from '@gitbook/api'; import { headers } from 'next/headers'; -import { GitBookContext } from './gitbook-context'; import { getPagePath } from './pages'; export interface PageHrefContext { @@ -23,8 +22,9 @@ export interface PageHrefContext { * Return the base path for the current request. * The value will start and finish with / */ -export function formatBasePath(headerBasePath: string | null): string { - let path = headerBasePath ?? '/'; +export async function getBasePath(): Promise { + const headersList = await headers(); + let path = headersList.get('x-gitbook-basepath') ?? '/'; if (!path.startsWith('/')) { path = '/' + path; @@ -37,15 +37,24 @@ export function formatBasePath(headerBasePath: string | null): string { return path; } +/** + * Return the current host for the current request. + */ +export async function getHost(): Promise { + const headersList = await headers(); + return headersList.get('x-gitbook-host') ?? headersList.get('host') ?? ''; +} + /** * Return the root URL for the GitBook Open instance (not the content). * Use `baseUrl` to get the base URL for the current content. * * The URL will end with "/". */ -export function getRootUrl(ctx: GitBookContext): string { - const protocol = ctx.protocol; - let path = ctx.originBasePath; +export async function getRootUrl(): Promise { + const [headersList, host] = await Promise.all([headers(), getHost()]); + const protocol = headersList.get('x-forwarded-proto') ?? 'https'; + let path = headersList.get('x-gitbook-origin-basepath') ?? '/'; if (!path.startsWith('/')) { path = '/' + path; @@ -55,26 +64,24 @@ export function getRootUrl(ctx: GitBookContext): string { path = path + '/'; } - return `${protocol}://${ctx.host}${path}`; + return `${protocol}://${host}${path}`; } /** * Return the base URL for the current content. * The URL will end with "/". */ -export function getBaseUrl(ctx: GitBookContext): string { - return `${ctx.protocol}://${ctx.host}${ctx.basePath}`; +export async function getBaseUrl(): Promise { + const [headersList, host, basePath] = await Promise.all([headers(), getHost(), getBasePath()]); + const protocol = headersList.get('x-forwarded-proto') ?? 'https'; + return `${protocol}://${host}${basePath}`; } /** * Create an absolute href in the current content. */ -export function getAbsoluteHref( - ctx: GitBookContext, - href: string, - withHost: boolean = false, -): string { - const base = withHost ? getBaseUrl(ctx) : ctx.basePath; +export async function getAbsoluteHref(href: string, withHost: boolean = false): Promise { + const base = withHost ? await getBaseUrl() : await getBasePath(); return `${base}${href.startsWith('/') ? href.slice(1) : href}`; } @@ -91,14 +98,13 @@ export function getGitbookAppHref(pathname: string): string { /** * Create a link to a page path in the current space. */ -export function getPageHref( - ctx: GitBookContext, +export async function getPageHref( rootPages: RevisionPage[], page: RevisionPageDocument | RevisionPageGroup, context: PageHrefContext = {}, /** Anchor to link to in the page. */ anchor?: string, -): string { +): Promise { const { pdf } = context; if (pdf) { @@ -114,7 +120,9 @@ export function getPageHref( } } - return getAbsoluteHref(ctx, getPagePath(rootPages, page)) + (anchor ? '#' + anchor : ''); + const href = + (await getAbsoluteHref(getPagePath(rootPages, page))) + (anchor ? '#' + anchor : ''); + return href; } /** diff --git a/packages/gitbook/src/lib/pointer.ts b/packages/gitbook/src/lib/pointer.ts index 53b8ffbac..400fdd51d 100644 --- a/packages/gitbook/src/lib/pointer.ts +++ b/packages/gitbook/src/lib/pointer.ts @@ -1,11 +1,18 @@ +import { headers } from 'next/headers'; + import { SiteContentPointer, SpaceContentPointer } from './api'; -import { GitBookContext } from './gitbook-context'; /** * Get the current site content pointer from the headers */ -export function getSiteContentPointer(ctx: GitBookContext): SiteContentPointer { - const { siteId, spaceId, organizationId, siteSectionId, siteSpaceId, siteShareKey } = ctx; +export async function getSiteContentPointer(): Promise { + const headersList = await headers(); + const spaceId = headersList.get('x-gitbook-content-space'); + const siteId = headersList.get('x-gitbook-content-site'); + const organizationId = headersList.get('x-gitbook-content-organization'); + const siteSpaceId = headersList.get('x-gitbook-content-site-space'); + const siteSectionId = headersList.get('x-gitbook-content-site-section'); + const siteShareKey = headersList.get('x-gitbook-content-site-share-key'); if (!spaceId || !siteId || !organizationId) { throw new Error( @@ -20,8 +27,8 @@ export function getSiteContentPointer(ctx: GitBookContext): SiteContentPointer { siteSpaceId: siteSpaceId ?? undefined, siteShareKey: siteShareKey ?? undefined, organizationId, - revisionId: ctx.contentRevisionId ?? undefined, - changeRequestId: ctx.changeRequestId ?? undefined, + revisionId: headersList.get('x-gitbook-content-revision') ?? undefined, + changeRequestId: headersList.get('x-gitbook-content-changerequest') ?? undefined, }; return pointer; @@ -31,8 +38,9 @@ export function getSiteContentPointer(ctx: GitBookContext): SiteContentPointer { * Get the current space pointer from the headers. This should be used when rendering * the space in an isolated context (e.g. PDF generation). */ -export function getSpacePointer(ctx: GitBookContext): SpaceContentPointer { - const spaceId = ctx.spaceId; +export async function getSpacePointer(): Promise { + const headersList = await headers(); + const spaceId = headersList.get('x-gitbook-content-space'); if (!spaceId) { throw new Error( 'getSpacePointer is called outside the scope of a request processed by the middleware', @@ -41,8 +49,8 @@ export function getSpacePointer(ctx: GitBookContext): SpaceContentPointer { const pointer: SpaceContentPointer = { spaceId, - revisionId: ctx.contentRevisionId ?? undefined, - changeRequestId: ctx.changeRequestId ?? undefined, + revisionId: headersList.get('x-gitbook-content-revision') ?? undefined, + changeRequestId: headersList.get('x-gitbook-content-changerequest') ?? undefined, }; return pointer; diff --git a/packages/gitbook/src/lib/references.tsx b/packages/gitbook/src/lib/references.tsx index c62468f5c..b71ffa430 100644 --- a/packages/gitbook/src/lib/references.tsx +++ b/packages/gitbook/src/lib/references.tsx @@ -28,7 +28,6 @@ import { parseSpacesFromSiteSpaces, } from './api'; import { getBlockById, getBlockTitle } from './document'; -import { GitBookContext } from './gitbook-context'; import { getGitbookAppHref, getPageHref, PageHrefContext } from './links'; import { getPagePath, resolvePageId } from './pages'; import { ClassValue } from './tailwind'; @@ -101,7 +100,6 @@ export interface ResolveContentRefOptions { * Resolve a content reference to be rendered. */ export async function resolveContentRef( - ctx: GitBookContext, contentRef: ContentRef, context: ContentRefContext, options: ResolveContentRefOptions = {}, @@ -119,7 +117,7 @@ export async function resolveContentRef( } case 'file': { - const file = await getRevisionFile(ctx, space.id, revisionId, contentRef.file); + const file = await getRevisionFile(space.id, revisionId, contentRef.file); if (file) { return { href: file.downloadURL, @@ -135,7 +133,7 @@ export async function resolveContentRef( case 'anchor': case 'page': { if (contentRef.space && contentRef.space !== space.id) { - return resolveContentRefInSpace(ctx, contentRef.space, siteContext, contentRef); + return resolveContentRefInSpace(contentRef.space, siteContext, contentRef); } const resolvePageResult = @@ -164,7 +162,7 @@ export async function resolveContentRef( if (resolveAnchorText) { const document = page.documentId - ? await getDocument(ctx, space.id, page.documentId) + ? await getDocument(space.id, page.documentId) : null; if (document) { const block = getBlockById(document, anchor); @@ -195,7 +193,7 @@ export async function resolveContentRef( } } else { // Page in the current content - href = await getPageHref(ctx, pages, page, linksContext, anchor); + href = await getPageHref(pages, page, linksContext, anchor); } return { @@ -211,7 +209,7 @@ export async function resolveContentRef( const targetSpace = contentRef.space === space.id ? space - : await getBestTargetSpace(ctx, contentRef.space, siteContext); + : await getBestTargetSpace(contentRef.space, siteContext); if (!targetSpace) { return { @@ -229,7 +227,7 @@ export async function resolveContentRef( } case 'user': { - const user = await getUserById(ctx, contentRef.user); + const user = await getUserById(contentRef.user); if (user) { return { href: `mailto:${user.email}`, @@ -252,7 +250,7 @@ export async function resolveContentRef( } case 'collection': { - const collection = await ignoreAPIError(getCollection(ctx, contentRef.collection)); + const collection = await ignoreAPIError(getCollection(contentRef.collection)); if (!collection) { return { href: getGitbookAppHref(`/s/${contentRef.collection}`), @@ -270,7 +268,6 @@ export async function resolveContentRef( case 'reusable-content': { const reusableContent = await getReusableContent( - ctx, space.id, revisionId, contentRef.reusableContent, @@ -296,21 +293,16 @@ export async function resolveContentRef( * It will try to return the space in the site context if it exists to avoid cross-site links. */ async function getBestTargetSpace( - ctx: GitBookContext, spaceId: string, siteContext: SiteContentPointer | null, ): Promise { const [fetchedSpace, publishedContentSite] = await Promise.all([ ignoreAPIError( - getSpace( - ctx, - spaceId, - siteContext?.siteShareKey ? siteContext.siteShareKey : undefined, - ), + getSpace(spaceId, siteContext?.siteShareKey ? siteContext.siteShareKey : undefined), ), siteContext ? ignoreAPIError( - getPublishedContentSite(ctx, { + getPublishedContentSite({ organizationId: siteContext.organizationId, siteId: siteContext.siteId, siteShareKey: siteContext.siteShareKey, @@ -341,7 +333,6 @@ async function getBestTargetSpace( } async function resolveContentRefInSpace( - ctx: GitBookContext, spaceId: string, siteContext: SiteContentPointer | null, contentRef: ContentRef, @@ -351,8 +342,8 @@ async function resolveContentRefInSpace( }; const [result, bestTargetSpace] = await Promise.all([ - ignoreAPIError(getSpaceContentData(ctx, pointer, siteContext?.siteShareKey)), - getBestTargetSpace(ctx, spaceId, siteContext), + ignoreAPIError(getSpaceContentData(pointer, siteContext?.siteShareKey)), + getBestTargetSpace(spaceId, siteContext), ]); if (!result) { return null; @@ -367,7 +358,7 @@ async function resolveContentRefInSpace( baseUrl += '/'; } - const resolved = await resolveContentRef(ctx, contentRef, { + const resolved = await resolveContentRef(contentRef, { siteContext, space, revisionId: space.revision, diff --git a/packages/gitbook/src/lib/seo.ts b/packages/gitbook/src/lib/seo.ts index f979207c9..a7db11b8f 100644 --- a/packages/gitbook/src/lib/seo.ts +++ b/packages/gitbook/src/lib/seo.ts @@ -1,4 +1,5 @@ import { + Collection, ContentVisibility, RevisionPageDocument, RevisionPageGroup, @@ -6,8 +7,7 @@ import { SiteVisibility, Space, } from '@gitbook/api'; - -import { GitBookContext } from './gitbook-context'; +import { headers } from 'next/headers'; /** * Return true if a page is indexable in search. @@ -31,16 +31,21 @@ export function isPageIndexable( /** * Return true if a space should be indexed by search engines. */ -export function isSpaceIndexable( - ctx: GitBookContext, - { space, site }: { space: Space; site: Site | null }, -) { - if (process.env.GITBOOK_BLOCK_SEARCH_INDEXATION && !ctx.searchIndexation) { +export async function isSpaceIndexable({ space, site }: { space: Space; site: Site | null }) { + const headersList = await headers(); + + if ( + process.env.GITBOOK_BLOCK_SEARCH_INDEXATION && + !headersList.has('x-gitbook-search-indexation') + ) { return false; } // Prevent indexation of preview of revisions / change-requests - if (ctx.contentRevisionId || ctx.changeRequestId) { + if ( + headersList.get('x-gitbook-content-revision') || + headersList.get('x-gitbook-content-changerequest') + ) { return false; } diff --git a/packages/gitbook/src/lib/tracking.ts b/packages/gitbook/src/lib/tracking.ts index edd106d72..7c52a01d0 100644 --- a/packages/gitbook/src/lib/tracking.ts +++ b/packages/gitbook/src/lib/tracking.ts @@ -1,12 +1,15 @@ -import { GitBookContext } from './gitbook-context'; +import { headers } from 'next/headers'; /** * Return true if events should be tracked on the site. */ -export function shouldTrackEvents(ctx: GitBookContext): boolean { +export async function shouldTrackEvents(): Promise { + const headersList = await headers(); + if ( process.env.NODE_ENV === 'development' || - (process.env.GITBOOK_BLOCK_PAGE_VIEWS_TRACKING && !ctx.trackPageViews) + (process.env.GITBOOK_BLOCK_PAGE_VIEWS_TRACKING && + !headersList.has('x-gitbook-track-page-views')) ) { return false; } diff --git a/packages/gitbook/src/lib/visitor-token.ts b/packages/gitbook/src/lib/visitor-token.ts index 3656aa606..50987f8fd 100644 --- a/packages/gitbook/src/lib/visitor-token.ts +++ b/packages/gitbook/src/lib/visitor-token.ts @@ -90,6 +90,15 @@ export function normalizeVisitorAuthURL(url: URL): URL { return withoutVAParam; } +/** + * Get the visitor token from the request context. + */ +export async function getCurrentVisitorToken(): Promise { + const headersList = await headers(); + const visitorToken = headersList.get('x-gitbook-visitor-token'); + return visitorToken; +} + /** * Get all possible basePaths for a given URL. This is used to find the visitor * authentication cookie token. diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts index 84df0da31..b17659d77 100644 --- a/packages/gitbook/src/middleware.ts +++ b/packages/gitbook/src/middleware.ts @@ -30,7 +30,6 @@ import { normalizeVisitorAuthURL, } from '@/lib/visitor-token'; -import { getGitBookContextFromHeaders, GitBookContext } from './lib/gitbook-context'; import { waitUntil } from './lib/waitUntil'; export const config = { @@ -97,7 +96,6 @@ export type LookupResult = PublishedContentWithCache & { * The middleware also takes care of persisting the visitor authentication state. */ export async function middleware(request: NextRequest) { - const ctx = getGitBookContextFromHeaders(request.headers); const { url, mode } = getInputURL(request); setTag('url', url.toString()); @@ -130,7 +128,7 @@ export async function middleware(request: NextRequest) { }), contextId: undefined, }, - () => lookupSiteForURL(ctx, mode, request, inputURL), + () => lookupSiteForURL(mode, request, inputURL), ); if ('error' in resolved) { return new NextResponse(resolved.error.message, { @@ -184,7 +182,7 @@ export async function middleware(request: NextRequest) { async () => { const [siteData] = await Promise.all([ 'site' in resolved - ? getSiteData(ctx, { + ? getSiteData({ organizationId: resolved.organization, siteId: resolved.site, siteSectionId: resolved.siteSection, @@ -196,7 +194,6 @@ export async function middleware(request: NextRequest) { // the cache will handle concurrent calls waitUntil( getSpaceContentData( - ctx, { spaceId: resolved.space, changeRequestId: resolved.changeRequest, @@ -377,26 +374,25 @@ function getInputURL(request: NextRequest): { } async function lookupSiteForURL( - ctx: GitBookContext, mode: URLLookupMode, request: NextRequest, url: URL, ): Promise { switch (mode) { case 'single': { - return lookupSiteInSingleMode(ctx, url); + return await lookupSiteInSingleMode(url); } case 'multi': { - return await lookupSiteInMultiMode(ctx, request, url); + return await lookupSiteInMultiMode(request, url); } case 'multi-path': { - return await lookupSiteInMultiPathMode(ctx, request, url); + return await lookupSiteInMultiPathMode(request, url); } case 'multi-id': { - return await lookupSiteOrSpaceInMultiIdMode(ctx, request, url); + return await lookupSiteOrSpaceInMultiIdMode(request, url); } case 'proxy': - return await lookupSiteInProxy(ctx, request, url); + return await lookupSiteInProxy(request, url); default: assertNever(mode); } @@ -406,7 +402,7 @@ async function lookupSiteForURL( * GITBOOK_MODE=single * When serving a single space, configured using GITBOOK_SPACE_ID and GITBOOK_TOKEN. */ -function lookupSiteInSingleMode(ctx: GitBookContext, url: URL): LookupResult { +async function lookupSiteInSingleMode(url: URL): Promise { const spaceId = process.env.GITBOOK_SPACE_ID; if (!spaceId) { throw new Error( @@ -414,7 +410,8 @@ function lookupSiteInSingleMode(ctx: GitBookContext, url: URL): LookupResult { ); } - const apiToken = getDefaultAPIToken(api(ctx).client.endpoint); + const apiCtx = await api(); + const apiToken = getDefaultAPIToken(apiCtx.client.endpoint); if (!apiToken) { throw new Error( `Missing GITBOOK_TOKEN environment variable. It should be passed when using GITBOOK_MODE=single.`, @@ -435,11 +432,7 @@ function lookupSiteInSingleMode(ctx: GitBookContext, url: URL): LookupResult { * GITBOOK_MODE=proxy * When proxying a site on a different base URL. */ -async function lookupSiteInProxy( - ctx: GitBookContext, - request: NextRequest, - url: URL, -): Promise { +async function lookupSiteInProxy(request: NextRequest, url: URL): Promise { const rawSiteUrl = request.headers.get('x-gitbook-site-url'); if (!rawSiteUrl) { throw new Error( @@ -450,20 +443,16 @@ async function lookupSiteInProxy( const siteUrl = new URL(rawSiteUrl); siteUrl.pathname = joinPath(siteUrl.pathname, url.pathname); - return await lookupSiteInMultiMode(ctx, request, siteUrl); + return await lookupSiteInMultiMode(request, siteUrl); } /** * GITBOOK_MODE=multi * When serving multi spaces based on the current URL. */ -async function lookupSiteInMultiMode( - ctx: GitBookContext, - request: NextRequest, - url: URL, -): Promise { +async function lookupSiteInMultiMode(request: NextRequest, url: URL): Promise { const visitorAuthToken = getVisitorToken(request, url); - const lookup = await lookupSiteByAPI(ctx, url, visitorAuthToken); + const lookup = await lookupSiteByAPI(url, visitorAuthToken); return { ...lookup, ...('basePath' in lookup && visitorAuthToken @@ -483,7 +472,6 @@ async function lookupSiteInMultiMode( * - /~space|~site/:id/~revisions/:revisionId/:path */ async function lookupSiteOrSpaceInMultiIdMode( - ctx: GitBookContext, request: NextRequest, url: URL, ): Promise { @@ -562,8 +550,9 @@ async function lookupSiteOrSpaceInMultiIdMode( // invalidated when trying to preview the site with different visitor // attributes. const contextId = decoded.claims ? hash(decoded.claims) : undefined; + const apiCtx = await api(); const gitbookAPI = new GitBookAPI({ - endpoint: apiEndpoint ?? api(ctx).client.endpoint, + endpoint: apiEndpoint ?? apiCtx.client.endpoint, authToken: apiToken, userAgent: userAgent(), }); @@ -572,7 +561,7 @@ async function lookupSiteOrSpaceInMultiIdMode( // (the cache is not dependend on the auth token, so it could leak data) if (source.kind === 'space') { await withAPI({ client: gitbookAPI, contextId }, () => - getSpace.revalidate(ctx, source.id, undefined), + getSpace.revalidate(source.id, undefined), ); } @@ -580,7 +569,7 @@ async function lookupSiteOrSpaceInMultiIdMode( // (the cache is not dependend on the auth token, so it could leak data) if (source.kind === 'site') { await withAPI({ client: gitbookAPI, contextId }, () => - getPublishedContentSite.revalidate(ctx, { + getPublishedContentSite.revalidate({ organizationId: decoded.organization, siteId: source.id, siteShareKey: undefined, @@ -630,11 +619,7 @@ async function lookupSiteOrSpaceInMultiIdMode( * GITBOOK_MODE=multi-path * When serving multi spaces with the url passed in the path. */ -async function lookupSiteInMultiPathMode( - ctx: GitBookContext, - request: NextRequest, - url: URL, -): Promise { +async function lookupSiteInMultiPathMode(request: NextRequest, url: URL): Promise { // Skip useless requests if ( url.pathname === '/favicon.ico' || @@ -673,7 +658,7 @@ async function lookupSiteInMultiPathMode( const visitorAuthToken = getVisitorToken(request, target); - const lookup = await lookupSiteByAPI(ctx, target, visitorAuthToken); + const lookup = await lookupSiteByAPI(target, visitorAuthToken); if ('error' in lookup) { return lookup; } @@ -711,7 +696,6 @@ async function lookupSiteInMultiPathMode( * To optimize caching, we try multiple lookup alternatives and return the first one that matches. */ async function lookupSiteByAPI( - ctx: GitBookContext, lookupURL: URL, visitorTokenLookup: VisitorTokenLookup, ): Promise { @@ -729,7 +713,6 @@ async function lookupSiteByAPI( const result = await race(lookup.urls, async (alternative, { signal }) => { const data = await getPublishedContentByUrl( - ctx, alternative.url, visitorTokenLookup?.token, redirectOnError || undefined,