From 67e8d69932b9bb7313d762205ea081f8834b2496 Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Mon, 4 Aug 2025 20:01:40 +0200 Subject: [PATCH 1/3] Fix dpr and max size for avif/webp image format --- packages/gitbook/src/routes/image.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/gitbook/src/routes/image.ts b/packages/gitbook/src/routes/image.ts index 7ee31e1333..004ebc15cc 100644 --- a/packages/gitbook/src/routes/image.ts +++ b/packages/gitbook/src/routes/image.ts @@ -87,6 +87,8 @@ export async function serveResizedImage( options.height = Number(height); } + const longestEdgeValue = Math.max(options.width || 0, options.height || 0); + const dpr = requestURL.searchParams.get('dpr'); if (dpr) { options.dpr = Number(dpr); @@ -99,10 +101,14 @@ export async function serveResizedImage( // Check the Accept header to handle content negotiation const accept = request.headers.get('accept'); - if (accept && /image\/avif/.test(accept)) { + // We use transform image, max size for avif should be 1600 + // https://developers.cloudflare.com/images/transform-images/#limits-per-format + if (accept && /image\/avif/.test(accept) && longestEdgeValue <= 1600) { options.format = 'avif'; - } else if (accept && /image\/webp/.test(accept)) { + options.dpr = chooseDPR(longestEdgeValue, 1600, options.dpr); + } else if (accept && /image\/webp/.test(accept) && longestEdgeValue <= 1920) { options.format = 'webp'; + options.dpr = chooseDPR(longestEdgeValue, 1920, options.dpr); } try { @@ -119,6 +125,18 @@ export async function serveResizedImage( } } +/** + * Choose the appropriate device pixel ratio (DPR) based on the longest edge of the image. + * This function ensures that the DPR is within a reasonable range (1 to 3). + * This is only used for AVIF/WebP formats to avoid issues with Cloudflare resizing. + * It means that dpr may not be respected for avif/webp formats, but it will also improve the cache hit ratio. + */ +function chooseDPR(longestEdgeValue: number, maxAllowedSize: number, wantedDpr?: number): number { + const dpr = Math.floor(maxAllowedSize / longestEdgeValue); + const maxDpr = Math.min(wantedDpr ?? 1, 3); // Limit to a maximum of 3 + return Math.min(Math.max(dpr, 1), maxDpr); +} + /** * Parse the image signature version from a query param. Returns null if the version is invalid. */ From 31d34ba37aa16d932e8967db2c89a2d609768c5e Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Mon, 4 Aug 2025 20:22:35 +0200 Subject: [PATCH 2/3] Ensure that PageCoverImage uses a srcSet whenever posible --- .../src/components/PageBody/PageCover.tsx | 43 +++++++++---------- .../components/PageBody/PageCoverImage.tsx | 4 ++ 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/packages/gitbook/src/components/PageBody/PageCover.tsx b/packages/gitbook/src/components/PageBody/PageCover.tsx index ecaf4d9439..a9256cb406 100644 --- a/packages/gitbook/src/components/PageBody/PageCover.tsx +++ b/packages/gitbook/src/components/PageBody/PageCover.tsx @@ -43,31 +43,30 @@ export async function PageCover(props: { const getImage = async (resolved: ResolvedContentRef | null, returnNull = false) => { if (!resolved && returnNull) return; - const [attrs, size] = await Promise.all([ - getImageAttributes({ - sizes, - source: resolved - ? { - src: resolved.href, - size: resolved.file?.dimensions ?? null, - } - : { - src: defaultPageCover.src, - size: { - width: defaultPageCover.width, - height: defaultPageCover.height, - }, + // If we don't have a size for the image, we want to calculate it so that we can use srcSet + const size = + resolved?.file?.dimensions ?? + (await context.imageResizer?.getImageSize(resolved?.href || defaultPageCover.src, {})); + const attrs = await getImageAttributes({ + sizes, + source: resolved + ? { + src: resolved.href, + size: size ?? null, + } + : { + src: defaultPageCover.src, + size: { + width: defaultPageCover.width, + height: defaultPageCover.height, }, - quality: 100, - resize: context.imageResizer ?? false, - }), - context.imageResizer - ?.getImageSize(resolved?.href || defaultPageCover.src, {}) - .then((size) => size ?? undefined), - ]); + }, + quality: 100, + resize: context.imageResizer ?? false, + }); return { ...attrs, - size, + size: size ?? undefined, }; }; diff --git a/packages/gitbook/src/components/PageBody/PageCoverImage.tsx b/packages/gitbook/src/components/PageBody/PageCoverImage.tsx index d4f1c2b11a..64fb3b3464 100644 --- a/packages/gitbook/src/components/PageBody/PageCoverImage.tsx +++ b/packages/gitbook/src/components/PageBody/PageCoverImage.tsx @@ -44,6 +44,8 @@ export function PageCoverImage({ imgs, y }: { imgs: Images; y: number }) {
Page cover Date: Tue, 5 Aug 2025 10:20:41 +0200 Subject: [PATCH 3/3] review --- packages/gitbook/src/routes/image.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/gitbook/src/routes/image.ts b/packages/gitbook/src/routes/image.ts index 004ebc15cc..c2878846f0 100644 --- a/packages/gitbook/src/routes/image.ts +++ b/packages/gitbook/src/routes/image.ts @@ -132,9 +132,10 @@ export async function serveResizedImage( * It means that dpr may not be respected for avif/webp formats, but it will also improve the cache hit ratio. */ function chooseDPR(longestEdgeValue: number, maxAllowedSize: number, wantedDpr?: number): number { - const dpr = Math.floor(maxAllowedSize / longestEdgeValue); - const maxDpr = Math.min(wantedDpr ?? 1, 3); // Limit to a maximum of 3 - return Math.min(Math.max(dpr, 1), maxDpr); + const maxDprBySize = Math.floor(maxAllowedSize / longestEdgeValue); + const clampedDpr = Math.min(wantedDpr ?? 1, 3); // Limit to a maximum of 3, default to 1 if not specified + // Ensure that the DPR is within the allowed range + return Math.max(1, Math.min(maxDprBySize, clampedDpr)); } /**