(siteURLData.apiToken);
+ if (decoded.exp && decoded.exp < Date.now() / 1000 + 120) {
+ forbidden();
+ }
+
+ const context = await fetchSiteContextByURLLookup(
+ getBaseContext({
+ siteURL,
+ siteURLData,
+ urlMode: getModeFromParams(params.mode),
+ }),
+ siteURLData
+ );
+
+ return {
+ context,
+ visitorAuthClaims: getVisitorAuthClaimsFromToken(decoded),
+ };
+ }
+);
+
/**
* Get the site context when rendering dynamically.
* The context will depend on the request.
*/
export async function getDynamicSiteContext(params: RouteLayoutParams) {
- const siteURL = getSiteURLFromParams(params);
- const siteURLData = getSiteURLDataFromParams(params);
-
- const context = await fetchSiteContextByURLLookup(
- getBaseContext({
- siteURL,
- siteURLData,
- urlMode: getModeFromParams(params.mode),
- }),
- siteURLData
- );
-
- context.customization = await getDynamicCustomizationSettings(context.customization);
-
- return {
- context,
- visitorAuthClaims: getVisitorAuthClaims(siteURLData),
- };
+ return fetchDynamicSiteContext(params.mode, params.siteURL, params.siteData);
}
+const fetchDynamicSiteContext = cache(
+ async (mode: string, encodedSiteURL: string, encodedSiteData: string) => {
+ const params: RouteLayoutParams = {
+ mode,
+ siteURL: encodedSiteURL,
+ siteData: encodedSiteData,
+ };
+ const siteURL = getSiteURLFromParams(params);
+ const siteURLData = getSiteURLDataFromParams(params);
+
+ const context = await fetchSiteContextByURLLookup(
+ getBaseContext({
+ siteURL,
+ siteURLData,
+ urlMode: getModeFromParams(params.mode),
+ }),
+ siteURLData
+ );
+
+ context.customization = await getDynamicCustomizationSettings(context.customization);
+
+ return {
+ context,
+ visitorAuthClaims: getVisitorAuthClaims(siteURLData),
+ };
+ }
+);
+
/**
* Get the decoded page path from the params.
*/
diff --git a/packages/gitbook/src/components/PageBody/PageCoverImage.tsx b/packages/gitbook/src/components/PageBody/PageCoverImage.tsx
index ae2d18c4b..bfa70e8e0 100644
--- a/packages/gitbook/src/components/PageBody/PageCoverImage.tsx
+++ b/packages/gitbook/src/components/PageBody/PageCoverImage.tsx
@@ -29,15 +29,10 @@ interface PageCoverImageProps {
export function PageCoverImage(props: PageCoverImageProps) {
const { imgs, y, height, mask } = props;
- const { containerRef, objectPositionY, isLoading } = useCoverPosition(imgs, y);
-
- if (isLoading) {
- return (
-
- );
- }
+ // The image is always rendered server-side (reserving space via aspect-ratio) so it stays
+ // discoverable by the preload scanner as the LCP element; the client probe only refines
+ // `objectPositionY` once real dimensions are known.
+ const { containerRef, objectPositionY } = useCoverPosition(imgs, y);
return (
diff --git a/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx b/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx
index 26983d017..ec3edcc64 100644
--- a/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx
+++ b/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx
@@ -1,6 +1,7 @@
import type { GitBookSiteContext } from '@/lib/context';
import { CustomizationDefaultThemeMode } from '@gitbook/api';
import type { Metadata, Viewport } from 'next';
+import Script from 'next/script';
import React from 'react';
import * as ReactDOM from 'react-dom';
@@ -39,12 +40,6 @@ export async function SiteLayout(props: {
ReactDOM.preconnect(GITBOOK_ASSETS_URL);
}
- scripts.forEach(({ script }) => {
- ReactDOM.preload(script, {
- as: 'script',
- });
- });
-
return (
{scripts.length > 0
- ? scripts.map(({ script }) => )
+ ? scripts.map(({ script }) => (
+
+ ))
: null}
{scripts.some((script) => script.cookies) || customization.privacyPolicy.url ? (
diff --git a/packages/gitbook/src/components/SitePage/SitePage.tsx b/packages/gitbook/src/components/SitePage/SitePage.tsx
index 02b3083f6..d854a4f81 100644
--- a/packages/gitbook/src/components/SitePage/SitePage.tsx
+++ b/packages/gitbook/src/components/SitePage/SitePage.tsx
@@ -159,7 +159,7 @@ export async function generateSitePageViewport(context: GitBookSiteContext): Pro
}
export async function generateSitePageMetadata(props: SitePageProps): Promise {
- const { context, pageTarget, pageMetaLinks } = await getPageDataWithFallback({
+ const { context, pageTarget } = await getPageDataWithFallback({
context: props.context,
pagePathParams: props.pageParams,
});
@@ -174,6 +174,8 @@ export async function generateSitePageMetadata(props: SitePageProps): Promise 0);
- const document = await getPageDocument(context, page);
+ // The page document and its meta links are independent; resolve them concurrently to
+ // avoid stacking two round trips in front of the LCP content.
+ const [document, pageMetaLinks] = await Promise.all([
+ getPageDocument(context, page),
+ resolvePageMetaLinks(context, page.id),
+ ]);
const iconStyle = getCustomizationIconStyle(customization);
const iconSources = await getInlineIconSources(
getContentInlineIconSourceRequests({
@@ -319,9 +326,6 @@ async function getPageDataWithFallback(args: {
}) {
const { context: baseContext, pagePathParams } = args;
const { context, pageTarget } = await fetchPageData(baseContext, pagePathParams);
- const pageMetaLinks = await (pageTarget?.page
- ? resolvePageMetaLinks(context, pageTarget.page.id)
- : null);
return {
context: {
@@ -329,7 +333,6 @@ async function getPageDataWithFallback(args: {
page: pageTarget?.page,
},
pageTarget,
- pageMetaLinks,
};
}