diff --git a/.changeset/reduce-client-bundle.md b/.changeset/reduce-client-bundle.md
index b34978d5b..b0e41572a 100644
--- a/.changeset/reduce-client-bundle.md
+++ b/.changeset/reduce-client-bundle.md
@@ -2,4 +2,4 @@
"gitbook": patch
---
-Reduce the JavaScript and CSS loaded on published site pages: the search index and its UI now load only when search is opened, the admin toolbar and OpenAPI/ContentKit styles are no longer shipped to every visitor, and the client bundle targets modern browsers.
+Reduce the JavaScript and CSS loaded on published site pages: the search index and its UI now load only when search is opened, and the admin toolbar and OpenAPI/ContentKit styles are no longer shipped to every visitor.
diff --git a/packages/gitbook/next.config.mjs b/packages/gitbook/next.config.mjs
index 0a4fc9d78..992281a11 100644
--- a/packages/gitbook/next.config.mjs
+++ b/packages/gitbook/next.config.mjs
@@ -37,16 +37,6 @@ const nextConfig = {
optimisticClientCache: false,
// Disable splitting the RSC in like 5 chunks
prefetchInlining: true,
-
- // Tree-shake barrel imports from these packages so only the used entrypoints ship
- // in the client bundle (notably `motion`, which is otherwise pulled in wholesale).
- optimizePackageImports: [
- 'motion',
- '@gitbook/icons',
- 'react-aria',
- 'react-aria-components',
- 'react-stately',
- ],
},
env: {
diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json
index 7cbc03ff7..8afd96e79 100644
--- a/packages/gitbook/package.json
+++ b/packages/gitbook/package.json
@@ -138,7 +138,9 @@
"e2e-browserless": "bun test ./tests/",
"typecheck": "tsc --noEmit"
},
- "browserslist": ["chrome >= 93, edge >= 93, firefox >= 92, safari >= 15.4, not dead"],
+ "browserslist": [
+ ">0.3%, chrome >= 64, edge >= 79, firefox >= 67, opera >= 51, safari >= 12 and not dead"
+ ],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
diff --git a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/site-index/route.ts b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/site-index/route.ts
index 780c80207..7eeffbb1f 100644
Binary files a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/site-index/route.ts and b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/site-index/route.ts differ
diff --git a/packages/gitbook/src/app/utils.ts b/packages/gitbook/src/app/utils.ts
index e219e0b1d..ca0c944fc 100644
--- a/packages/gitbook/src/app/utils.ts
+++ b/packages/gitbook/src/app/utils.ts
@@ -1,5 +1,4 @@
import { getVisitorAuthClaims, getVisitorAuthClaimsFromToken } from '@/lib/adaptive';
-import { cache } from '@/lib/cache';
import { type SiteURLData, fetchSiteContextByURLLookup, getBaseContext } from '@/lib/context';
import { getDynamicCustomizationSettings } from '@/lib/customization';
import type { SiteAPIToken } from '@gitbook/api';
@@ -27,16 +26,6 @@ export type RouteParams = RouteLayoutParams & {
* Get the static context when rendering statically a site.
*/
export async function getStaticSiteContext(params: RouteLayoutParams) {
- // Only the fields the context depends on — dropping pagePath so the layout, page and their
- // metadata/viewport generators all share a single cached execution per request.
- return fetchStaticSiteContext({
- mode: params.mode,
- siteURL: params.siteURL,
- siteData: params.siteData,
- });
-}
-
-const fetchStaticSiteContext = cache(async (params: RouteLayoutParams) => {
const siteURL = getSiteURLFromParams(params);
const siteURLData = getSiteURLDataFromParams(params);
@@ -60,21 +49,13 @@ const fetchStaticSiteContext = cache(async (params: RouteLayoutParams) => {
context,
visitorAuthClaims: getVisitorAuthClaimsFromToken(decoded),
};
-});
+}
/**
* Get the site context when rendering dynamically.
* The context will depend on the request.
*/
export async function getDynamicSiteContext(params: RouteLayoutParams) {
- return fetchDynamicSiteContext({
- mode: params.mode,
- siteURL: params.siteURL,
- siteData: params.siteData,
- });
-}
-
-const fetchDynamicSiteContext = cache(async (params: RouteLayoutParams) => {
const siteURL = getSiteURLFromParams(params);
const siteURLData = getSiteURLDataFromParams(params);
@@ -93,7 +74,7 @@ const fetchDynamicSiteContext = cache(async (params: RouteLayoutParams) => {
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 bfa70e8e0..ae2d18c4b 100644
--- a/packages/gitbook/src/components/PageBody/PageCoverImage.tsx
+++ b/packages/gitbook/src/components/PageBody/PageCoverImage.tsx
@@ -29,10 +29,15 @@ interface PageCoverImageProps {
export function PageCoverImage(props: PageCoverImageProps) {
const { imgs, y, height, mask } = props;
- // 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);
+ const { containerRef, objectPositionY, isLoading } = useCoverPosition(imgs, y);
+
+ if (isLoading) {
+ return (
+
+ );
+ }
return (
diff --git a/packages/gitbook/src/components/PageBody/useCoverPosition.ts b/packages/gitbook/src/components/PageBody/useCoverPosition.ts
index 63de39f77..8814c2a92 100644
--- a/packages/gitbook/src/components/PageBody/useCoverPosition.ts
+++ b/packages/gitbook/src/components/PageBody/useCoverPosition.ts
@@ -28,6 +28,7 @@ interface Images {
export function useCoverPosition(imgs: Images, y: number) {
const containerRef = useRef(null);
const [loadedDimensions, setLoadedDimensions] = useState(null);
+ const [isLoading, setIsLoading] = useState(!imgs.light.size && !imgs.dark?.size);
const container = useResizeObserver({
// @ts-expect-error wrong types
@@ -43,6 +44,8 @@ export function useCoverPosition(imgs: Images, y: number) {
return; // Already have dimensions
}
+ setIsLoading(true);
+
// Load the original image (using src, not srcSet) to get true dimensions
// Use dark image if available, otherwise fall back to light
const imageToLoad = imgs.dark || imgs.light;
@@ -52,6 +55,11 @@ export function useCoverPosition(imgs: Images, y: number) {
width: img.naturalWidth,
height: img.naturalHeight,
});
+ setIsLoading(false);
+ };
+ img.onerror = () => {
+ // If image fails to load, use a fallback
+ setIsLoading(false);
};
img.src = imageToLoad.src;
}, [imgs.light, imgs.dark]);
@@ -96,5 +104,6 @@ export function useCoverPosition(imgs: Images, y: number) {
return {
containerRef,
objectPositionY,
+ isLoading: !imageDimensions || isLoading,
};
}
diff --git a/packages/gitbook/src/components/Search/useLocalSearchResults.tsx b/packages/gitbook/src/components/Search/useLocalSearchResults.tsx
index 5d95cc77c..2dc7e30e8 100644
--- a/packages/gitbook/src/components/Search/useLocalSearchResults.tsx
+++ b/packages/gitbook/src/components/Search/useLocalSearchResults.tsx
@@ -20,10 +20,7 @@ interface RawIndexPage {
icon?: string;
emoji?: string;
description?: string;
- /** Inlined breadcrumbs (version 1 responses). */
breadcrumbs?: Breadcrumb[];
- /** Indices into the response-level `crumbs` table (version 2 responses). */
- breadcrumbRefs?: number[];
}
/** FlexSearch-compatible document type — satisfies DocumentData via explicit index signature */
@@ -123,21 +120,7 @@ async function getOrBuildIndexes(indexURL: string): Promise