Skip to content

[WIP] Prefetch data on GBO v2 #3324

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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 <SitePage context={context} pageParams={{ pathname }} />;
return <SitePage context={context} pageParams={params} />;
}

export async function generateViewport(props: PageProps): Promise<Viewport> {
Expand All @@ -27,10 +26,9 @@ export async function generateViewport(props: PageProps): Promise<Viewport> {
export async function generateMetadata(props: PageProps): Promise<Metadata> {
const params = await props.params;
const { context } = await getDynamicSiteContext(params);
const pathname = getPagePathFromParams(params);

return generateSitePageMetadata({
context,
pageParams: { pathname },
pageParams: params,
});
}
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
// }
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 <SitePage context={context} pageParams={{ pathname }} />;
return <SitePage context={context} pageParams={params} />;
}

export async function generateViewport(props: PageProps): Promise<Viewport> {
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<Metadata> {
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,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
20 changes: 19 additions & 1 deletion packages/gitbook-v2/src/app/utils.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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'));
};
220 changes: 220 additions & 0 deletions packages/gitbook-v2/src/lib/data/prefetch.ts
Original file line number Diff line number Diff line change
@@ -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<JSONDocument | null>;
getPrefetchedRef: (
ref?: ContentRef,
options?: ResolveContentRefOptions
) => Promise<ResolvedContentRef | null>;
}

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<string, Promise<ResolvedContentRef | null>>();
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<ResolvedContentRef | null> => {
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,
};
});
10 changes: 9 additions & 1 deletion packages/gitbook/src/app/middleware/(site)/(content)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -44,7 +45,14 @@ export async function generateViewport(): Promise<Viewport> {

export async function generateMetadata(): Promise<Metadata> {
const context = await fetchLayoutData();
return generateSiteLayoutMetadata(context);
const icons = getIcons(context);
return generateSiteLayoutMetadata({
staticSiteContext: Promise.resolve({
context,
visitorAuthClaims: {},
}),
icons,
});
}

async function fetchLayoutData() {
Expand Down
Loading
Loading