Skip to content

Commit 28f7fba

Browse files
conico974Nicolas Dorseuil
andauthored
Fix image format handling and enhance PageCoverImage component (#3527)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
1 parent ff96bb5 commit 28f7fba

File tree

3 files changed

+46
-24
lines changed

3 files changed

+46
-24
lines changed

packages/gitbook/src/components/PageBody/PageCover.tsx

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -43,31 +43,30 @@ export async function PageCover(props: {
4343

4444
const getImage = async (resolved: ResolvedContentRef | null, returnNull = false) => {
4545
if (!resolved && returnNull) return;
46-
const [attrs, size] = await Promise.all([
47-
getImageAttributes({
48-
sizes,
49-
source: resolved
50-
? {
51-
src: resolved.href,
52-
size: resolved.file?.dimensions ?? null,
53-
}
54-
: {
55-
src: defaultPageCover.src,
56-
size: {
57-
width: defaultPageCover.width,
58-
height: defaultPageCover.height,
59-
},
46+
// If we don't have a size for the image, we want to calculate it so that we can use srcSet
47+
const size =
48+
resolved?.file?.dimensions ??
49+
(await context.imageResizer?.getImageSize(resolved?.href || defaultPageCover.src, {}));
50+
const attrs = await getImageAttributes({
51+
sizes,
52+
source: resolved
53+
? {
54+
src: resolved.href,
55+
size: size ?? null,
56+
}
57+
: {
58+
src: defaultPageCover.src,
59+
size: {
60+
width: defaultPageCover.width,
61+
height: defaultPageCover.height,
6062
},
61-
quality: 100,
62-
resize: context.imageResizer ?? false,
63-
}),
64-
context.imageResizer
65-
?.getImageSize(resolved?.href || defaultPageCover.src, {})
66-
.then((size) => size ?? undefined),
67-
]);
63+
},
64+
quality: 100,
65+
resize: context.imageResizer ?? false,
66+
});
6867
return {
6968
...attrs,
70-
size,
69+
size: size ?? undefined,
7170
};
7271
};
7372

packages/gitbook/src/components/PageBody/PageCoverImage.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export function PageCoverImage({ imgs, y }: { imgs: Images; y: number }) {
4444
<div className="h-full w-full overflow-hidden" ref={containerRef}>
4545
<img
4646
src={imgs.light.src}
47+
srcSet={imgs.light.srcSet}
48+
sizes={imgs.light.sizes}
4749
fetchPriority="high"
4850
alt="Page cover"
4951
className={tcls('w-full', 'object-cover', imgs.dark ? 'dark:hidden' : '')}
@@ -55,6 +57,8 @@ export function PageCoverImage({ imgs, y }: { imgs: Images; y: number }) {
5557
{imgs.dark && (
5658
<img
5759
src={imgs.dark.src}
60+
srcSet={imgs.dark.srcSet}
61+
sizes={imgs.dark.sizes}
5862
fetchPriority="low"
5963
alt="Page cover"
6064
className={tcls('w-full', 'object-cover', 'dark:inline', 'hidden')}

packages/gitbook/src/routes/image.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ export async function serveResizedImage(
8787
options.height = Number(height);
8888
}
8989

90+
const longestEdgeValue = Math.max(options.width || 0, options.height || 0);
91+
9092
const dpr = requestURL.searchParams.get('dpr');
9193
if (dpr) {
9294
options.dpr = Number(dpr);
@@ -99,10 +101,14 @@ export async function serveResizedImage(
99101

100102
// Check the Accept header to handle content negotiation
101103
const accept = request.headers.get('accept');
102-
if (accept && /image\/avif/.test(accept)) {
104+
// We use transform image, max size for avif should be 1600
105+
// https://developers.cloudflare.com/images/transform-images/#limits-per-format
106+
if (accept && /image\/avif/.test(accept) && longestEdgeValue <= 1600) {
103107
options.format = 'avif';
104-
} else if (accept && /image\/webp/.test(accept)) {
108+
options.dpr = chooseDPR(longestEdgeValue, 1600, options.dpr);
109+
} else if (accept && /image\/webp/.test(accept) && longestEdgeValue <= 1920) {
105110
options.format = 'webp';
111+
options.dpr = chooseDPR(longestEdgeValue, 1920, options.dpr);
106112
}
107113

108114
try {
@@ -119,6 +125,19 @@ export async function serveResizedImage(
119125
}
120126
}
121127

128+
/**
129+
* Choose the appropriate device pixel ratio (DPR) based on the longest edge of the image.
130+
* This function ensures that the DPR is within a reasonable range (1 to 3).
131+
* This is only used for AVIF/WebP formats to avoid issues with Cloudflare resizing.
132+
* It means that dpr may not be respected for avif/webp formats, but it will also improve the cache hit ratio.
133+
*/
134+
function chooseDPR(longestEdgeValue: number, maxAllowedSize: number, wantedDpr?: number): number {
135+
const maxDprBySize = Math.floor(maxAllowedSize / longestEdgeValue);
136+
const clampedDpr = Math.min(wantedDpr ?? 1, 3); // Limit to a maximum of 3, default to 1 if not specified
137+
// Ensure that the DPR is within the allowed range
138+
return Math.max(1, Math.min(maxDprBySize, clampedDpr));
139+
}
140+
122141
/**
123142
* Parse the image signature version from a query param. Returns null if the version is invalid.
124143
*/

0 commit comments

Comments
 (0)