diff --git a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/(content)/[pagePath]/page.tsx b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/(content)/[pagePath]/page.tsx index e5f520d392..79eb7452f2 100644 --- a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/(content)/[pagePath]/page.tsx +++ b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/(content)/[pagePath]/page.tsx @@ -3,7 +3,7 @@ import { generateSitePageMetadata, generateSitePageViewport, } from '@/components/SitePage'; -import { type RouteParams, getDynamicSiteContext, getPagePathFromParams } from '@v2/app/utils'; +import { type RouteParams, getDynamicSiteContext } from '@v2/app/utils'; import type { Metadata, Viewport } from 'next'; type PageProps = { @@ -14,9 +14,8 @@ type PageProps = { export default async function Page(props: PageProps) { const params = await props.params; const { context } = await getDynamicSiteContext(params); - const pathname = getPagePathFromParams(params); - return ; + return ; } export async function generateViewport(props: PageProps): Promise { @@ -27,10 +26,9 @@ export async function generateViewport(props: PageProps): Promise { export async function generateMetadata(props: PageProps): Promise { const params = await props.params; const { context } = await getDynamicSiteContext(params); - const pathname = getPagePathFromParams(params); return generateSitePageMetadata({ context, - pageParams: { pathname }, + pageParams: params, }); } diff --git a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/(content)/layout.tsx b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/(content)/layout.tsx index 8934279f01..0194c7d04f 100644 --- a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/(content)/layout.tsx +++ b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/(content)/layout.tsx @@ -1,9 +1,5 @@ import { CustomizationRootLayout } from '@/components/RootLayout'; -import { - SiteLayout, - generateSiteLayoutMetadata, - generateSiteLayoutViewport, -} from '@/components/SiteLayout'; +import { SiteLayout, generateSiteLayoutViewport } from '@/components/SiteLayout'; import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils'; import { GITBOOK_DISABLE_TRACKING } from '@v2/lib/env'; import { getThemeFromMiddleware } from '@v2/lib/middleware'; @@ -38,7 +34,7 @@ export async function generateViewport({ params }: SiteDynamicLayoutProps) { return generateSiteLayoutViewport(context); } -export async function generateMetadata({ params }: SiteDynamicLayoutProps) { - const { context } = await getDynamicSiteContext(await params); - return generateSiteLayoutMetadata(context); -} +// export async function generateMetadata({ params }: SiteDynamicLayoutProps) { +// const { context } = await getDynamicSiteContext(await params); +// return generateSiteLayoutMetadata(context); +// } diff --git a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/[pagePath]/page.tsx b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/[pagePath]/page.tsx index a35ecb69c8..45bbdc9768 100644 --- a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/[pagePath]/page.tsx +++ b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/[pagePath]/page.tsx @@ -3,7 +3,8 @@ import { generateSitePageMetadata, generateSitePageViewport, } from '@/components/SitePage'; -import { type RouteParams, getPagePathFromParams, getStaticSiteContext } from '@v2/app/utils'; +import type { RouteParams } from '@v2/app/utils'; +import { getPrefetchedDataFromLayoutParams } from '@v2/lib/data/prefetch'; import type { Metadata, Viewport } from 'next'; @@ -15,24 +16,24 @@ type PageProps = { export default async function Page(props: PageProps) { const params = await props.params; - const { context } = await getStaticSiteContext(params); - const pathname = getPagePathFromParams(params); + const { staticSiteContext } = getPrefetchedDataFromLayoutParams(params); + const { context } = await staticSiteContext; - return ; + return ; } export async function generateViewport(props: PageProps): Promise { - const { context } = await getStaticSiteContext(await props.params); + const params = await props.params; + const { context } = await getPrefetchedDataFromLayoutParams(params).staticSiteContext; return generateSitePageViewport(context); } export async function generateMetadata(props: PageProps): Promise { const params = await props.params; - const { context } = await getStaticSiteContext(params); - const pathname = getPagePathFromParams(params); + const { context } = await getPrefetchedDataFromLayoutParams(params).staticSiteContext; return generateSitePageMetadata({ context, - pageParams: { pathname }, + pageParams: params, }); } diff --git a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/layout.tsx b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/layout.tsx index c8f1aefe49..396d333d68 100644 --- a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/layout.tsx +++ b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/layout.tsx @@ -5,6 +5,7 @@ import { generateSiteLayoutViewport, } from '@/components/SiteLayout'; import { type RouteLayoutParams, getStaticSiteContext } from '@v2/app/utils'; +import { getPrefetchedDataFromLayoutParams } from '@v2/lib/data/prefetch'; import { GITBOOK_DISABLE_TRACKING } from '@v2/lib/env'; interface SiteStaticLayoutProps { @@ -36,6 +37,6 @@ export async function generateViewport({ params }: SiteStaticLayoutProps) { } export async function generateMetadata({ params }: SiteStaticLayoutProps) { - const { context } = await getStaticSiteContext(await params); - return generateSiteLayoutMetadata(context); + const prefetchedData = getPrefetchedDataFromLayoutParams(await params); + return generateSiteLayoutMetadata(prefetchedData); } diff --git a/packages/gitbook-v2/src/app/utils.ts b/packages/gitbook-v2/src/app/utils.ts index 932902df0e..e9c12e8161 100644 --- a/packages/gitbook-v2/src/app/utils.ts +++ b/packages/gitbook-v2/src/app/utils.ts @@ -1,7 +1,13 @@ import { getVisitorAuthClaims, getVisitorAuthClaimsFromToken } from '@/lib/adaptive'; import { getDynamicCustomizationSettings } from '@/lib/customization'; import type { SiteAPIToken } from '@gitbook/api'; -import { type SiteURLData, fetchSiteContextByURLLookup, getBaseContext } from '@v2/lib/context'; +import { + type GitBookSiteContext, + type SiteURLData, + fetchSiteContextByURLLookup, + getBaseContext, +} from '@v2/lib/context'; +import { getResizedImageURL } from '@v2/lib/images'; import { jwtDecode } from 'jwt-decode'; import { forbidden } from 'next/navigation'; import rison from 'rison'; @@ -105,3 +111,15 @@ function getSiteURLDataFromParams(params: RouteLayoutParams): SiteURLData { const decoded = decodeURIComponent(params.siteData); return rison.decode(decoded); } + +export const getIcon = async (context: GitBookSiteContext, theme: 'light' | 'dark') => { + const { linker, imageResizer, customization } = context; + const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null; + const faviconSize = 48; + return customIcon?.[theme] + ? await getResizedImageURL(imageResizer, customIcon[theme], { + width: faviconSize, + height: faviconSize, + }) + : linker.toAbsoluteURL(linker.toPathInSpace('~gitbook/icon?size=small&theme=light')); +}; diff --git a/packages/gitbook-v2/src/lib/data/prefetch.ts b/packages/gitbook-v2/src/lib/data/prefetch.ts new file mode 100644 index 0000000000..1078fd945e --- /dev/null +++ b/packages/gitbook-v2/src/lib/data/prefetch.ts @@ -0,0 +1,220 @@ +import { type PagePathParams, fetchPageData } from '@/components/SitePage'; +import type { VisitorAuthClaims } from '@/lib/adaptive'; +import type { AncestorRevisionPage } from '@/lib/pages'; +import { + type ResolveContentRefOptions, + type ResolvedContentRef, + resolveContentRef, +} from '@/lib/references'; +import type { ContentRef, JSONDocument, RevisionPageDocument } from '@gitbook/api'; +import { + type RouteLayoutParams, + type RouteParams, + getIcon, + getPagePathFromParams, + getStaticSiteContext, +} from '@v2/app/utils'; +import { identify } from 'object-identity'; +import { cache } from '../cache'; +import type { GitBookSiteContext } from '../context'; +import { getPageDocument } from './pages'; + +export interface PrefetchedLayoutData { + staticSiteContext: Promise<{ + context: GitBookSiteContext; + visitorAuthClaims: VisitorAuthClaims; + }>; + icons: Promise< + { + url: string; + type: string; + media: string; + }[] + >; +} + +export interface PrefetchedPageData { + pageData: Promise<{ + context: GitBookSiteContext & { + page?: RevisionPageDocument; + }; + pageTarget?: { + page: RevisionPageDocument; + ancestors: AncestorRevisionPage[]; + }; + }>; + document: Promise; + getPrefetchedRef: ( + ref?: ContentRef, + options?: ResolveContentRefOptions + ) => Promise; +} + +const cachedInitialDate = cache(() => Date.now()); + +/** + * Fetches the page data matching the requested pathname and fallback to root page when page is not found. + */ +export async function getPageDataWithFallback(args: { + context: GitBookSiteContext; + pagePathParams: PagePathParams; +}) { + const { context: baseContext, pagePathParams } = args; + const { context, pageTarget } = await fetchPageData(baseContext, pagePathParams); + + return { + context: { + ...context, + page: pageTarget?.page, + }, + pageTarget, + }; +} + +export async function getIcons(context: GitBookSiteContext): Promise< + { + url: string; + type: string; + media: string; + }[] +> { + return Promise.all([getIcon(context, 'light'), getIcon(context, 'dark')]).then((urls) => [ + { + url: urls[0], + type: 'image/png', + media: '(prefers-color-scheme: light)', + }, + { + url: urls[1], + type: 'image/png', + media: '(prefers-color-scheme: dark)', + }, + ]); +} + +export const getPrefetchedDataFromLayoutParams = cache( + (params: RouteLayoutParams): PrefetchedLayoutData => { + const startingDate = cachedInitialDate(); + const staticSiteContext = getStaticSiteContext(params).finally(() => { + console.log(`Finished fetching static site context in ${Date.now() - startingDate}ms`); + }); + const icons = staticSiteContext + .then(({ context }) => getIcons(context)) + .finally(() => { + console.log(`Finished fetching icons in ${Date.now() - startingDate}ms`); + }); + + return { + staticSiteContext, + icons, + }; + } +); + +export const prefetchedDocumentRef = ( + document: JSONDocument | null, + context: GitBookSiteContext +) => { + const fetched = new Map>(); + if (!document) return fetched; + + const traverseNodes = (nodes: any[]): void => { + for (const node of nodes) { + // We try prefetching as many references as possible. + if (node.data?.ref) { + fetched.set(identify(node.data.ref), resolveContentRef(node.data.ref, context)); + } + // Handle prefetching of references for cards + if (node.data?.view && node.data.view.type === 'cards') { + const view = node.data.view; + const records = Object.entries(node.data.records || {}); + + records.forEach(async (record: [string, any]) => { + const coverFile = view.coverDefinition + ? record[1].values[view.coverDefinition]?.[0] + : null; + const targetRef = view.targetDefinition + ? (record[1].values[view.targetDefinition] as ContentRef) + : null; + if (targetRef) { + fetched.set(identify(targetRef), resolveContentRef(targetRef, context)); + } + if (coverFile) { + const fileRef = { + kind: 'file' as const, + file: coverFile, + }; + fetched.set(identify(fileRef), resolveContentRef(fileRef, context)); + } + }); + } + if (node.nodes && Array.isArray(node.nodes)) { + traverseNodes(node.nodes); + } + if (node.fragments && Array.isArray(node.fragments)) { + traverseNodes(node.fragments); + } + } + }; + + if (document.nodes && Array.isArray(document.nodes)) { + traverseNodes(document.nodes); + } + return fetched; +}; + +export const getPrefetchedDataFromPageParams = cache((params: RouteParams): PrefetchedPageData => { + const startingDate = cachedInitialDate(); + const { staticSiteContext } = getPrefetchedDataFromLayoutParams(params); + const pathname = getPagePathFromParams(params); + const pageData = staticSiteContext + .then(({ context }) => + getPageDataWithFallback({ + context, + pagePathParams: { + pathname, + }, + }) + ) + .finally(() => { + console.log(`Finished fetching page data in ${Date.now() - startingDate}ms`); + }); + const document = pageData + .then(({ context, pageTarget }) => { + if (!pageTarget?.page) { + return null; + } + return getPageDocument(context, pageTarget?.page); + }) + .finally(() => { + console.log(`Finished fetching document in ${Date.now() - startingDate}ms`); + }); + const prefetchedRef = Promise.all([staticSiteContext, document]) + .then(([{ context }, document]) => { + // Prefetch the references in the document + return prefetchedDocumentRef(document, context); + }) + .finally(() => { + console.log(`Finished prefetching references in ${Date.now() - startingDate}ms`); + }); + + const getContentRef = async ( + ref?: ContentRef, + options?: ResolveContentRefOptions + ): Promise => { + if (!ref) { + return null; + } + if (options) { + const { context } = await staticSiteContext; + return resolveContentRef(ref, context, options); + } + return prefetchedRef.then((prefetched) => prefetched.get(identify(ref)) ?? null); + }; + + return { + pageData, + document, + getPrefetchedRef: getContentRef, + }; +}); diff --git a/packages/gitbook/src/app/middleware/(site)/(content)/layout.tsx b/packages/gitbook/src/app/middleware/(site)/(content)/layout.tsx index 10db3a3459..4fce1512cb 100644 --- a/packages/gitbook/src/app/middleware/(site)/(content)/layout.tsx +++ b/packages/gitbook/src/app/middleware/(site)/(content)/layout.tsx @@ -11,6 +11,7 @@ import { getVisitorAuthClaims } from '@/lib/adaptive'; import { getSiteContentPointer } from '@/lib/pointer'; import { shouldTrackEvents } from '@/lib/tracking'; import { fetchV1ContextForSitePointer } from '@/lib/v1'; +import { getIcons } from '@v2/lib/data/prefetch'; export const runtime = 'edge'; export const dynamic = 'force-dynamic'; @@ -44,7 +45,14 @@ export async function generateViewport(): Promise { export async function generateMetadata(): Promise { const context = await fetchLayoutData(); - return generateSiteLayoutMetadata(context); + const icons = getIcons(context); + return generateSiteLayoutMetadata({ + staticSiteContext: Promise.resolve({ + context, + visitorAuthClaims: {}, + }), + icons, + }); } async function fetchLayoutData() { diff --git a/packages/gitbook/src/components/DocumentView/BlockContentRef.tsx b/packages/gitbook/src/components/DocumentView/BlockContentRef.tsx index 7640a66e28..a8941d52a7 100644 --- a/packages/gitbook/src/components/DocumentView/BlockContentRef.tsx +++ b/packages/gitbook/src/components/DocumentView/BlockContentRef.tsx @@ -1,19 +1,17 @@ import { type DocumentBlockContentRef, SiteInsightsLinkPosition } from '@gitbook/api'; import { Card } from '@/components/primitives'; -import { type ResolvedContentRef, resolveContentRef } from '@/lib/references'; +import type { ResolvedContentRef } from '@/lib/references'; import type { BlockProps } from './Block'; export async function BlockContentRef(props: BlockProps) { const { block, context, style } = props; - const resolved = context.contentContext - ? await resolveContentRef(block.data.ref, context.contentContext, { - resolveAnchorText: true, - iconStyle: ['text-xl', 'text-tint'], - }) - : null; + const resolved = await context.getContentRef(block.data.ref, { + resolveAnchorText: true, + iconStyle: ['text-xl', 'text-tint'], + }); if (!resolved) { return null; diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/PlainCodeBlock.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/PlainCodeBlock.tsx index 9c50b95a0b..318a61cc67 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/PlainCodeBlock.tsx +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/PlainCodeBlock.tsx @@ -48,6 +48,7 @@ export function PlainCodeBlock(props: { code: string; syntax: string }) { document={document} context={{ mode: 'default', + getContentRef: async () => null, // No content references needed for plain code block }} block={block} ancestorBlocks={[]} diff --git a/packages/gitbook/src/components/DocumentView/DocumentView.tsx b/packages/gitbook/src/components/DocumentView/DocumentView.tsx index 724a7f2b05..abce771200 100644 --- a/packages/gitbook/src/components/DocumentView/DocumentView.tsx +++ b/packages/gitbook/src/components/DocumentView/DocumentView.tsx @@ -1,7 +1,8 @@ import type { ClassValue } from '@/lib/tailwind'; -import type { JSONDocument } from '@gitbook/api'; +import type { ContentRef, JSONDocument } from '@gitbook/api'; import type { GitBookAnyContext } from '@v2/lib/context'; +import type { ResolveContentRefOptions, ResolvedContentRef } from '@/lib/references'; import { BlockSkeleton } from './Block'; import { Blocks } from './Blocks'; @@ -28,6 +29,11 @@ export interface DocumentContext { * @default true */ wrapBlocksInSuspense?: boolean; + + getContentRef: ( + ref: ContentRef, + options?: ResolveContentRefOptions + ) => Promise; } export interface DocumentContextProps { diff --git a/packages/gitbook/src/components/DocumentView/Drawing.tsx b/packages/gitbook/src/components/DocumentView/Drawing.tsx index ca565a6905..e9d7cff795 100644 --- a/packages/gitbook/src/components/DocumentView/Drawing.tsx +++ b/packages/gitbook/src/components/DocumentView/Drawing.tsx @@ -1,7 +1,5 @@ import type { DocumentBlockDrawing } from '@gitbook/api'; -import { resolveContentRef } from '@/lib/references'; - import { Image } from '../utils'; import type { BlockProps } from './Block'; import { Caption } from './Caption'; @@ -9,11 +7,12 @@ import { imageBlockSizes } from './Images'; export async function Drawing(props: BlockProps) { const { block, context } = props; + if (!block.data.ref) { + return null; + } + + const resolved = await context.getContentRef(block.data.ref); - const resolved = - block.data.ref && context.contentContext - ? await resolveContentRef(block.data.ref, context.contentContext) - : null; if (!resolved) { return null; } diff --git a/packages/gitbook/src/components/DocumentView/File.tsx b/packages/gitbook/src/components/DocumentView/File.tsx index fe72164fc6..a604c71651 100644 --- a/packages/gitbook/src/components/DocumentView/File.tsx +++ b/packages/gitbook/src/components/DocumentView/File.tsx @@ -1,7 +1,6 @@ import { type DocumentBlockFile, SiteInsightsLinkPosition } from '@gitbook/api'; import { getSimplifiedContentType } from '@/lib/files'; -import { resolveContentRef } from '@/lib/references'; import { tcls } from '@/lib/tailwind'; import { Link } from '../primitives'; @@ -12,9 +11,8 @@ import { FileIcon } from './FileIcon'; export async function File(props: BlockProps) { const { block, context } = props; - const contentRef = context.contentContext - ? await resolveContentRef(block.data.ref, context.contentContext) - : null; + const contentRef = await context.getContentRef(block.data.ref); + const file = contentRef?.file; if (!file) { diff --git a/packages/gitbook/src/components/DocumentView/Images.tsx b/packages/gitbook/src/components/DocumentView/Images.tsx index 91d869002e..f2d8a5ad16 100644 --- a/packages/gitbook/src/components/DocumentView/Images.tsx +++ b/packages/gitbook/src/components/DocumentView/Images.tsx @@ -1,7 +1,6 @@ import type { DocumentBlockImage, DocumentBlockImages, JSONDocument, Length } from '@gitbook/api'; import { Image, type ImageResponsiveSize } from '@/components/utils'; -import { resolveContentRef } from '@/lib/references'; import { type ClassValue, tcls } from '@/lib/tailwind'; import type { BlockProps } from './Block'; @@ -66,10 +65,8 @@ async function ImageBlock(props: { const { block, context, isEstimatedOffscreen } = props; const [src, darkSrc] = await Promise.all([ - context.contentContext ? resolveContentRef(block.data.ref, context.contentContext) : null, - block.data.refDark && context.contentContext - ? resolveContentRef(block.data.refDark, context.contentContext) - : null, + context.getContentRef(block.data.ref), + block.data.refDark ? context.getContentRef(block.data.refDark) : null, ]); if (!src) { diff --git a/packages/gitbook/src/components/DocumentView/InlineButton.tsx b/packages/gitbook/src/components/DocumentView/InlineButton.tsx index a36cd74527..fe42dfae4b 100644 --- a/packages/gitbook/src/components/DocumentView/InlineButton.tsx +++ b/packages/gitbook/src/components/DocumentView/InlineButton.tsx @@ -1,4 +1,3 @@ -import { resolveContentRef } from '@/lib/references'; import * as api from '@gitbook/api'; import { Button } from '../primitives'; import type { InlineProps } from './Inline'; @@ -10,7 +9,7 @@ export async function InlineButton(props: InlineProps) throw new Error('InlineButton requires a contentContext'); } - const resolved = await resolveContentRef(inline.data.ref, context.contentContext); + const resolved = await context.getContentRef(inline.data.ref); if (!resolved) { return null; diff --git a/packages/gitbook/src/components/DocumentView/InlineImage.tsx b/packages/gitbook/src/components/DocumentView/InlineImage.tsx index 4a3663b823..d857af0e03 100644 --- a/packages/gitbook/src/components/DocumentView/InlineImage.tsx +++ b/packages/gitbook/src/components/DocumentView/InlineImage.tsx @@ -2,7 +2,7 @@ import type { DocumentInlineImage } from '@gitbook/api'; import type { GitBookBaseContext } from '@v2/lib/context'; import assertNever from 'assert-never'; -import { type ResolvedContentRef, resolveContentRef } from '@/lib/references'; +import type { ResolvedContentRef } from '@/lib/references'; import { tcls } from '@/lib/tailwind'; import { Image } from '../utils'; @@ -13,10 +13,8 @@ export async function InlineImage(props: InlineProps) { const { size = 'original' } = inline.data; const [src, darkSrc] = await Promise.all([ - context.contentContext ? resolveContentRef(inline.data.ref, context.contentContext) : null, - inline.data.refDark && context.contentContext - ? resolveContentRef(inline.data.refDark, context.contentContext) - : null, + context.getContentRef(inline.data.ref), + inline.data.refDark ? context.getContentRef(inline.data.refDark) : null, ]); if (!src) { diff --git a/packages/gitbook/src/components/DocumentView/InlineLink.tsx b/packages/gitbook/src/components/DocumentView/InlineLink.tsx index f5bd7f798b..bc3cc67f06 100644 --- a/packages/gitbook/src/components/DocumentView/InlineLink.tsx +++ b/packages/gitbook/src/components/DocumentView/InlineLink.tsx @@ -1,6 +1,4 @@ import { type DocumentInlineLink, SiteInsightsLinkPosition } from '@gitbook/api'; - -import { resolveContentRef } from '@/lib/references'; import { Icon } from '@gitbook/icons'; import { StyledLink } from '../primitives'; import type { InlineProps } from './Inline'; @@ -10,12 +8,7 @@ import { Inlines } from './Inlines'; export async function InlineLink(props: InlineProps) { const { inline, document, context, ancestorInlines } = props; - const resolved = context.contentContext - ? await resolveContentRef(inline.data.ref, context.contentContext, { - // We don't want to resolve the anchor text here, as it can be very expensive and will block rendering if there is a lot of anchors link. - resolveAnchorText: false, - }) - : null; + const resolved = await context.getContentRef(inline.data.ref); if (!context.contentContext || !resolved) { return ( diff --git a/packages/gitbook/src/components/DocumentView/Mention.tsx b/packages/gitbook/src/components/DocumentView/Mention.tsx index 7d4db51786..b013ab9214 100644 --- a/packages/gitbook/src/components/DocumentView/Mention.tsx +++ b/packages/gitbook/src/components/DocumentView/Mention.tsx @@ -1,18 +1,15 @@ import { type DocumentInlineMention, SiteInsightsLinkPosition } from '@gitbook/api'; import { StyledLink } from '@/components/primitives'; -import { resolveContentRef } from '@/lib/references'; import type { InlineProps } from './Inline'; export async function Mention(props: InlineProps) { const { inline, context } = props; - const resolved = context.contentContext - ? await resolveContentRef(inline.data.ref, context.contentContext, { - resolveAnchorText: true, - }) - : null; + const resolved = await context.getContentRef(inline.data.ref, { + resolveAnchorText: true, + }); if (!resolved) { return null; diff --git a/packages/gitbook/src/components/DocumentView/ReusableContent.tsx b/packages/gitbook/src/components/DocumentView/ReusableContent.tsx index ccb66babd4..8af7f78321 100644 --- a/packages/gitbook/src/components/DocumentView/ReusableContent.tsx +++ b/packages/gitbook/src/components/DocumentView/ReusableContent.tsx @@ -18,6 +18,7 @@ export async function ReusableContent(props: BlockProps( case 'files': { const files = await Promise.all( (value as string[]).map((fileId) => - context.contentContext - ? resolveContentRef( - { - kind: 'file', - file: fileId, - }, - context.contentContext - ) - : null + context.getContentRef({ + kind: 'file', + file: fileId, + }) ) ); @@ -217,13 +211,12 @@ export async function RecordColumnValue( } case 'content-ref': { const contentRef = value ? (value as ContentRef) : null; - const resolved = - contentRef && context.contentContext - ? await resolveContentRef(contentRef, context.contentContext, { - resolveAnchorText: true, - iconStyle: ['mr-2', 'text-tint-subtle'], - }) - : null; + const resolved = contentRef + ? await context.getContentRef(contentRef, { + resolveAnchorText: true, + iconStyle: ['mr-2', 'text-tint-subtle'], + }) + : null; return ( ( kind: 'user', user: userId, }; - const resolved = context.contentContext - ? await resolveContentRef(contentRef, context.contentContext) - : null; + const resolved = await context.getContentRef(contentRef); if (!resolved) { return null; } diff --git a/packages/gitbook/src/components/Footer/FooterLinksGroup.tsx b/packages/gitbook/src/components/Footer/FooterLinksGroup.tsx index dd2de59ebe..e7bc3b7533 100644 --- a/packages/gitbook/src/components/Footer/FooterLinksGroup.tsx +++ b/packages/gitbook/src/components/Footer/FooterLinksGroup.tsx @@ -34,6 +34,7 @@ export function FooterLinksGroup(props: { async function FooterLink(props: { link: CustomizationContentLink; context: GitBookAnyContext }) { const { link, context } = props; + // TODO: prefetch content ref outside of the main document const resolved = await resolveContentRef(link.to, context); if (!resolved) { diff --git a/packages/gitbook/src/components/PDF/PDFPage.tsx b/packages/gitbook/src/components/PDF/PDFPage.tsx index f70b340b41..201abea150 100644 --- a/packages/gitbook/src/components/PDF/PDFPage.tsx +++ b/packages/gitbook/src/components/PDF/PDFPage.tsx @@ -28,6 +28,7 @@ import { PageControlButtons } from './PageControlButtons'; import { PrintButton } from './PrintButton'; import './pdf.css'; import { sanitizeGitBookAppURL } from '@/lib/app'; +import { resolveContentRef } from '@/lib/references'; import { getPageDocument } from '@v2/lib/data'; const DEFAULT_LIMIT = 100; @@ -244,6 +245,8 @@ async function PDFPageDocument(props: { ...context, page, }, + //TODO: Use prefetchedRef to avoid fetching the same content multiple times + getContentRef: (ref, options) => resolveContentRef(ref, context, options), getId: (id) => getPagePDFContainerId(page, id), }} // We consider all pages as offscreen in PDF mode diff --git a/packages/gitbook/src/components/PageBody/PageBody.tsx b/packages/gitbook/src/components/PageBody/PageBody.tsx index 801db03ce2..569a271581 100644 --- a/packages/gitbook/src/components/PageBody/PageBody.tsx +++ b/packages/gitbook/src/components/PageBody/PageBody.tsx @@ -1,4 +1,4 @@ -import type { JSONDocument, RevisionPageDocument } from '@gitbook/api'; +import type { ContentRef, JSONDocument, RevisionPageDocument } from '@gitbook/api'; import type { GitBookSiteContext } from '@v2/lib/context'; import React from 'react'; @@ -6,6 +6,7 @@ import { getSpaceLanguage } from '@/intl/server'; import { t } from '@/intl/translate'; import { hasFullWidthBlock, isNodeEmpty } from '@/lib/document'; import type { AncestorRevisionPage } from '@/lib/pages'; +import type { ResolveContentRefOptions, ResolvedContentRef } from '@/lib/references'; import { tcls } from '@/lib/tailwind'; import { DocumentView, DocumentViewSkeleton } from '../DocumentView'; import { TrackPageViewEvent } from '../Insights'; @@ -22,6 +23,10 @@ export function PageBody(props: { page: RevisionPageDocument; ancestors: AncestorRevisionPage[]; document: JSONDocument | null; + getContentRef: ( + ref: ContentRef, + options?: ResolveContentRefOptions + ) => Promise; withPageFeedback: boolean; }) { const { page, context, ancestors, document, withPageFeedback } = props; @@ -68,6 +73,7 @@ export function PageBody(props: { context={{ mode: 'default', contentContext: context, + getContentRef: props.getContentRef, }} /> diff --git a/packages/gitbook/src/components/Search/server-actions.tsx b/packages/gitbook/src/components/Search/server-actions.tsx index b45bc0aba1..3b02bbcf32 100644 --- a/packages/gitbook/src/components/Search/server-actions.tsx +++ b/packages/gitbook/src/components/Search/server-actions.tsx @@ -20,6 +20,7 @@ import { createStreamableValue } from 'ai/rsc'; import type * as React from 'react'; import { joinPathWithBaseURL } from '@/lib/paths'; +import { resolveContentRef } from '@/lib/references'; import { isV2 } from '@/lib/v2'; import type { IconName } from '@gitbook/icons'; import { throwIfDataError } from '@v2/lib/data'; @@ -345,6 +346,9 @@ async function transformAnswer( mode: 'default', contentContext: undefined, wrapBlocksInSuspense: false, + // TODO: Use prefetched content references + getContentRef: async (ref, options) => + resolveContentRef(ref, context, options), }} style={['space-y-5']} /> diff --git a/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx b/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx index 83211354f1..36ac720ccc 100644 --- a/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx +++ b/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx @@ -13,8 +13,8 @@ import { buildVersion } from '@/lib/build'; import { isSiteIndexable } from '@/lib/seo'; import type { VisitorAuthClaims } from '@/lib/adaptive'; +import type { PrefetchedLayoutData } from '@v2/lib/data/prefetch'; import { GITBOOK_API_PUBLIC_URL, GITBOOK_ASSETS_URL, GITBOOK_ICONS_URL } from '@v2/lib/env'; -import { getResizedImageURL } from '@v2/lib/images'; import { ClientContexts } from './ClientContexts'; import { RocketLoaderDetector } from './RocketLoaderDetector'; @@ -97,50 +97,24 @@ export async function generateSiteLayoutViewport(context: GitBookSiteContext): P }; } -export async function generateSiteLayoutMetadata(context: GitBookSiteContext): Promise { - const { site, customization, linker, imageResizer } = context; - const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null; +export async function generateSiteLayoutMetadata({ + staticSiteContext, + icons, +}: PrefetchedLayoutData): Promise { + const siteContext = await staticSiteContext; + const { site } = siteContext.context; - const faviconSize = 48; - const icons = await Promise.all( - [ - { - url: customIcon?.light - ? getResizedImageURL(imageResizer, customIcon.light, { - width: faviconSize, - height: faviconSize, - }) - : linker.toAbsoluteURL( - linker.toPathInSpace('~gitbook/icon?size=small&theme=light') - ), - type: 'image/png', - media: '(prefers-color-scheme: light)', - }, - { - url: customIcon?.dark - ? getResizedImageURL(imageResizer, customIcon.dark, { - width: faviconSize, - height: faviconSize, - }) - : linker.toAbsoluteURL( - linker.toPathInSpace('~gitbook/icon?size=small&theme=dark') - ), - type: 'image/png', - media: '(prefers-color-scheme: dark)', - }, - ].map(async (icon) => ({ - ...icon, - url: await icon.url, - })) - ); + const iconsUrls = await icons; return { title: site.title, generator: `GitBook (${buildVersion()})`, icons: { - icon: icons, - apple: icons, + icon: iconsUrls, + apple: iconsUrls, }, - robots: (await isSiteIndexable(context)) ? 'index, follow' : 'noindex, nofollow', + robots: (await isSiteIndexable(siteContext.context)) + ? 'index, follow' + : 'noindex, nofollow', }; } diff --git a/packages/gitbook/src/components/SitePage/SitePage.tsx b/packages/gitbook/src/components/SitePage/SitePage.tsx index 9e7c08515f..29534852aa 100644 --- a/packages/gitbook/src/components/SitePage/SitePage.tsx +++ b/packages/gitbook/src/components/SitePage/SitePage.tsx @@ -1,6 +1,5 @@ -import { CustomizationHeaderPreset, CustomizationThemeMode } from '@gitbook/api'; +import { type ContentRef, CustomizationHeaderPreset, CustomizationThemeMode } from '@gitbook/api'; import type { GitBookSiteContext } from '@v2/lib/context'; -import { getPageDocument } from '@v2/lib/data'; import type { Metadata, Viewport } from 'next'; import { notFound, redirect } from 'next/navigation'; import React from 'react'; @@ -10,26 +9,44 @@ import { PageBody, PageCover } from '@/components/PageBody'; import { getPagePath } from '@/lib/pages'; import { isPageIndexable, isSiteIndexable } from '@/lib/seo'; +import { type ResolveContentRefOptions, resolveContentRef } from '@/lib/references'; +import type { RouteParams } from '@v2/app/utils'; +import { getPageDocument } from '@v2/lib/data/pages'; +import { getPageDataWithFallback, getPrefetchedDataFromPageParams } from '@v2/lib/data/prefetch'; import { getResizedImageURL } from '@v2/lib/images'; import { PageContextProvider } from '../PageContext'; import { PageClientLayout } from './PageClientLayout'; -import { type PagePathParams, fetchPageData, getPathnameParam } from './fetch'; +import { type PagePathParams, getPathnameParam } from './fetch'; export type SitePageProps = { context: GitBookSiteContext; - pageParams: PagePathParams; + /** + * `RouteParams` is used in V2, `PagePathParams` is used in V1. + */ + pageParams: RouteParams | PagePathParams; }; +function isV2(params: RouteParams | PagePathParams): params is RouteParams { + return 'pagePath' in params; +} + /** * Fetch and render a page. */ export async function SitePage(props: SitePageProps) { - const { context, pageTarget } = await getPageDataWithFallback({ - context: props.context, - pagePathParams: props.pageParams, + const prefetchedData = isV2(props.pageParams) + ? getPrefetchedDataFromPageParams(props.pageParams) + : null; + const { context, pageTarget } = prefetchedData + ? await prefetchedData.pageData + : await getPageDataWithFallback({ + context: props.context, + pagePathParams: props.pageParams as PagePathParams, + }); + + const rawPathname = getPathnameParam({ + pathname: isV2(props.pageParams) ? props.pageParams.pagePath : props.pageParams.pathname, }); - - const rawPathname = getPathnameParam(props.pageParams); if (!pageTarget) { const pathname = rawPathname.toLowerCase(); if (pathname !== rawPathname) { @@ -40,12 +57,13 @@ export async function SitePage(props: SitePageProps) { notFound(); } } else if (getPagePath(context.pages, pageTarget.page) !== rawPathname) { - redirect( - context.linker.toPathForPage({ - pages: context.pages, - page: pageTarget.page, - }) - ); + //TODO: Don't forget to uncomment the redirect when i'm done + // redirect( + // context.linker.toPathForPage({ + // pages: context.pages, + // page: pageTarget.page, + // }) + // ); } const { customization, sections } = context; @@ -62,7 +80,16 @@ export async function SitePage(props: SitePageProps) { const withSections = Boolean(sections && sections.list.length > 0); const headerOffset = { sectionsHeader: withSections, topHeader: withTopHeader }; - const document = await getPageDocument(context, page); + const document = prefetchedData + ? await prefetchedData.document + : await getPageDocument(context, page); + + const getContentRef = async (ref: ContentRef, options?: ResolveContentRefOptions) => { + if (prefetchedData) { + return prefetchedData.getPrefetchedRef(ref, options); + } + return resolveContentRef(ref, context, options); + }; return ( @@ -85,6 +112,7 @@ export async function SitePage(props: SitePageProps) { ancestors={ancestors} document={document} withPageFeedback={withPageFeedback} + getContentRef={getContentRef} /> @@ -107,10 +135,12 @@ export async function generateSitePageViewport(context: GitBookSiteContext): Pro } export async function generateSitePageMetadata(props: SitePageProps): Promise { - const { context, pageTarget } = await getPageDataWithFallback({ - context: props.context, - pagePathParams: props.pageParams, - }); + const { context, pageTarget } = isV2(props.pageParams) + ? await getPrefetchedDataFromPageParams(props.pageParams).pageData + : await getPageDataWithFallback({ + context: props.context, + pagePathParams: props.pageParams as PagePathParams, + }); if (!pageTarget) { notFound(); @@ -144,22 +174,3 @@ export async function generateSitePageMetadata(props: SitePageProps): Promise