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