mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-16 07:35:16 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 44ae7afc37 | |||
| b2859600bc | |||
| aabdd3f4d7 | |||
| a2fc076acb | |||
| 82ed911cd8 | |||
| 7b4908c8fd | |||
| de19ada4fa | |||
| 47c80991d3 | |||
| 9e582ed9f0 |
+3
-5
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
+5
-9
@@ -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);
|
||||
// }
|
||||
|
||||
+9
-8
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user