Compare commits

...

9 Commits

Author SHA1 Message Date
Nicolas Dorseuil 44ae7afc37 resolve more things at the beginning 2025-06-16 16:32:42 +02:00
Nicolas Dorseuil b2859600bc Merge remote-tracking branch 'origin/main' into gbo/prefetch-data-rnd-7317 2025-06-16 15:04:46 +02:00
Nicolas Dorseuil aabdd3f4d7 fix v1 2025-06-16 11:48:57 +02:00
Nicolas Dorseuil a2fc076acb fix getContentRef 2025-06-16 10:54:01 +02:00
Nicolas Dorseuil 82ed911cd8 Refactor content reference resolution to use context.getContentRef method across components 2025-06-16 10:12:40 +02:00
Nicolas Dorseuil 7b4908c8fd Refactor content reference handling to use getContentRef method across components 2025-06-16 09:47:21 +02:00
Nicolas Dorseuil de19ada4fa move to prefetch 2025-06-14 12:01:02 +02:00
Nicolas Dorseuil 47c80991d3 try using prefetched data 2025-06-14 11:49:16 +02:00
Nicolas Dorseuil 9e582ed9f0 basic dirty implementation of prefetching 2025-06-14 11:49:16 +02:00
27 changed files with 390 additions and 177 deletions
@@ -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 <SitePage context={context} pageParams={{ pathname }} />;
return <SitePage context={context} pageParams={params} />;
}
export async function generateViewport(props: PageProps): Promise<Viewport> {
@@ -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,
});
}
@@ -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);
// }
@@ -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 <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,
});
}
@@ -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);
}
+19 -1
View File
@@ -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'));
};
@@ -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,
};
});
@@ -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<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() {
@@ -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<DocumentBlockContentRef>) {
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;
@@ -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={[]}
@@ -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<ResolvedContentRef | null>;
}
export interface DocumentContextProps {
@@ -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<DocumentBlockDrawing>) {
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;
}
@@ -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<DocumentBlockFile>) {
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) {
@@ -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) {
@@ -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<api.DocumentInlineButton>)
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;
@@ -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<DocumentInlineImage>) {
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) {
@@ -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<DocumentInlineLink>) {
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 (
@@ -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<DocumentInlineMention>) {
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;
@@ -18,6 +18,7 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon
? context.contentContext.dataFetcher.withToken({ apiToken: block.meta.token })
: context.contentContext.dataFetcher;
// TODO: prefetch the reusable content as well
const resolved = await resolveContentRef(block.data.ref, {
...context.contentContext,
dataFetcher,
@@ -6,7 +6,6 @@ import {
import { LinkBox, LinkOverlay } from '@/components/primitives';
import { Image } from '@/components/utils';
import { resolveContentRef } from '@/lib/references';
import { type ClassValue, tcls } from '@/lib/tailwind';
import { RecordColumnValue } from './RecordColumnValue';
@@ -28,12 +27,8 @@ export async function RecordCard(
: null;
const [cover, target] = await Promise.all([
coverFile && context.contentContext
? resolveContentRef({ kind: 'file', file: coverFile }, context.contentContext)
: null,
targetRef && context.contentContext
? resolveContentRef(targetRef, context.contentContext)
: null,
coverFile ? context.getContentRef({ kind: 'file', file: coverFile }) : null,
targetRef && context.getContentRef(targetRef),
]);
const coverIsSquareOrPortrait =
@@ -12,7 +12,6 @@ import { StyledLink } from '@/components/primitives';
import { Image } from '@/components/utils';
import { getNodeFragmentByName } from '@/lib/document';
import { getSimplifiedContentType } from '@/lib/files';
import { resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import { filterOutNullable } from '@/lib/typescript';
@@ -145,15 +144,10 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
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<Tag extends React.ElementType = 'div'>(
}
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 (
<Tag
className={tcls('text-base', 'text-balance', 'flex', 'items-center')}
@@ -258,9 +251,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
kind: 'user',
user: userId,
};
const resolved = context.contentContext
? await resolveContentRef(contentRef, context.contentContext)
: null;
const resolved = await context.getContentRef(contentRef);
if (!resolved) {
return null;
}
@@ -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) {
@@ -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
@@ -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<ResolvedContentRef | null>;
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,
}}
/>
</React.Suspense>
@@ -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']}
/>
@@ -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<Metadata> {
const { site, customization, linker, imageResizer } = context;
const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;
export async function generateSiteLayoutMetadata({
staticSiteContext,
icons,
}: PrefetchedLayoutData): Promise<Metadata> {
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',
};
}
@@ -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(props.pageParams);
const rawPathname = getPathnameParam({
pathname: isV2(props.pageParams) ? props.pageParams.pagePath : props.pageParams.pathname,
});
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 (
<PageContextProvider pageId={page.id} spaceId={context.space.id} title={page.title}>
@@ -85,6 +112,7 @@ export async function SitePage(props: SitePageProps) {
ancestors={ancestors}
document={document}
withPageFeedback={withPageFeedback}
getContentRef={getContentRef}
/>
</div>
<React.Suspense fallback={null}>
@@ -107,10 +135,12 @@ export async function generateSitePageViewport(context: GitBookSiteContext): Pro
}
export async function generateSitePageMetadata(props: SitePageProps): Promise<Metadata> {
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<Me
: 'noindex, nofollow',
};
}
/**
* Fetches the page data matching the requested pathname and fallback to root page when page is not found.
*/
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,
};
}
-1
View File
@@ -14,7 +14,6 @@ import assertNever from 'assert-never';
import type React from 'react';
import { PageIcon } from '@/components/PageIcon';
import { getGitBookAppHref } from './app';
import { getBlockById, getBlockTitle } from './document';
import { resolvePageId } from './pages';