Skip to content

Fix image format handling and enhance PageCoverImage component #3527

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 21 additions & 22 deletions packages/gitbook/src/components/PageBody/PageCover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
};

Expand Down
4 changes: 4 additions & 0 deletions packages/gitbook/src/components/PageBody/PageCoverImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export function PageCoverImage({ imgs, y }: { imgs: Images; y: number }) {
<div className="h-full w-full overflow-hidden" ref={containerRef}>
<img
src={imgs.light.src}
srcSet={imgs.light.srcSet}
sizes={imgs.light.sizes}
fetchPriority="high"
alt="Page cover"
className={tcls('w-full', 'object-cover', imgs.dark ? 'dark:hidden' : '')}
Expand All @@ -55,6 +57,8 @@ export function PageCoverImage({ imgs, y }: { imgs: Images; y: number }) {
{imgs.dark && (
<img
src={imgs.dark.src}
srcSet={imgs.dark.srcSet}
sizes={imgs.dark.sizes}
fetchPriority="low"
alt="Page cover"
className={tcls('w-full', 'object-cover', 'dark:inline', 'hidden')}
Expand Down
23 changes: 21 additions & 2 deletions packages/gitbook/src/routes/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 {
Expand All @@ -119,6 +125,19 @@ 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 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));
}

/**
* Parse the image signature version from a query param. Returns null if the version is invalid.
*/
Expand Down