Skip to content

Commit b7a0db3

Browse files
authored
Fix rendering of ogimage with SVG logos (#3328)
1 parent 382a198 commit b7a0db3

File tree

4 files changed

+48
-48
lines changed

4 files changed

+48
-48
lines changed

.changeset/green-clouds-cough.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"gitbook": minor
3+
---
4+
5+
Fix rendering of ogimage with SVG logos.

bun.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
"classnames": "^2.5.1",
8888
"event-iterator": "^2.0.0",
8989
"framer-motion": "^10.16.14",
90+
"image-size": "^2.0.2",
9091
"js-cookie": "^3.0.5",
9192
"jsontoxml": "^1.0.1",
9293
"jwt-decode": "^4.0.0",
@@ -2067,6 +2068,8 @@
20672068

20682069
"ignore": ["[email protected]", "", {}, "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA=="],
20692070

2071+
"image-size": ["[email protected]", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="],
2072+
20702073
"import-fresh": ["[email protected]", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
20712074

20722075
"import-lazy": ["[email protected]", "", {}, "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A=="],

packages/gitbook/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@
8181
"usehooks-ts": "^3.1.0",
8282
"zod": "^3.24.2",
8383
"zod-to-json-schema": "^3.24.5",
84-
"zustand": "^5.0.3"
84+
"zustand": "^5.0.3",
85+
"image-size": "^2.0.2"
8586
},
8687
"devDependencies": {
8788
"@argos-ci/playwright": "^5.0.3",

packages/gitbook/src/routes/ogimage.tsx

Lines changed: 38 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { CustomizationDefaultFont, CustomizationHeaderPreset } from '@gitbook/api';
22
import { colorContrast } from '@gitbook/colors';
33
import { type FontWeight, getDefaultFont } from '@gitbook/fonts';
4+
import { imageSize } from 'image-size';
45
import { redirect } from 'next/navigation';
56
import { ImageResponse } from 'next/og';
67

@@ -156,14 +157,14 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
156157
{String.fromCodePoint(Number.parseInt(`0x${customization.favicon.emoji}`))}
157158
</span>
158159
);
159-
const src = await readSelfImage(
160+
const iconImage = await fetchImage(
160161
linker.toAbsoluteURL(
161162
linker.toPathInSpace(
162163
`~gitbook/icon?size=medium&theme=${customization.themes.default}`
163164
)
164165
)
165166
);
166-
return <img src={src} alt="Icon" width={40} height={40} tw="mr-4" />;
167+
return <img {...iconImage} alt="Icon" width={40} height={40} tw="mr-4" />;
167168
};
168169

169170
const [favicon, { fontFamily, fonts }] = await Promise.all([faviconLoader(), fontLoader()]);
@@ -187,21 +188,23 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
187188
{/* Grid */}
188189
<img
189190
tw="absolute inset-0 w-[100vw] h-[100vh]"
190-
src={await readStaticImage(gridAsset)}
191+
src={(await fetchStaticImage(gridAsset)).src}
191192
alt="Grid"
192193
/>
193194

194195
{/* Logo */}
195196
{customization.header.logo ? (
196-
<img
197-
alt="Logo"
198-
height={60}
199-
src={
200-
useLightTheme
201-
? customization.header.logo.light
202-
: customization.header.logo.dark
203-
}
204-
/>
197+
<div tw="flex flex-row">
198+
<img
199+
{...(await fetchImage(
200+
useLightTheme
201+
? customization.header.logo.light
202+
: customization.header.logo.dark
203+
))}
204+
alt="Logo"
205+
tw="h-[60px]"
206+
/>
207+
</div>
205208
) : (
206209
<div tw="flex">
207210
{favicon}
@@ -289,34 +292,6 @@ async function loadCustomFont(input: { url: string; weight: 400 | 700 }) {
289292
};
290293
}
291294

292-
/**
293-
* Temporary function to log some data on Cloudflare.
294-
* TODO: remove this when we found the issue
295-
*/
296-
function logOnCloudflareOnly(message: string) {
297-
if (process.env.DEBUG_CLOUDFLARE === 'true') {
298-
// biome-ignore lint/suspicious/noConsole: <explanation>
299-
console.log(message);
300-
}
301-
}
302-
303-
/**
304-
* Read an image from a response as a base64 encoded string.
305-
*/
306-
async function readImage(response: Response) {
307-
const contentType = response.headers.get('content-type');
308-
if (!contentType || !contentType.startsWith('image/')) {
309-
logOnCloudflareOnly(`Invalid content type: ${contentType},
310-
status: ${response.status}
311-
rayId: ${response.headers.get('cf-ray')}`);
312-
throw new Error(`Invalid content type: ${contentType}`);
313-
}
314-
315-
const arrayBuffer = await response.arrayBuffer();
316-
const base64 = Buffer.from(arrayBuffer).toString('base64');
317-
return `data:${contentType};base64,${base64}`;
318-
}
319-
320295
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
321296
const staticCache = new Map<string, any>();
322297

@@ -335,16 +310,32 @@ async function getWithCache<T>(key: string, fn: () => Promise<T>) {
335310
/**
336311
* Read a static image and cache it in memory.
337312
*/
338-
async function readStaticImage(url: string) {
339-
logOnCloudflareOnly(`Reading static image: ${url}, cache size: ${staticCache.size}`);
340-
return getWithCache(`static-image:${url}`, () => readSelfImage(url));
313+
async function fetchStaticImage(url: string) {
314+
return getWithCache(`static-image:${url}`, () => fetchImage(url));
341315
}
342316

343317
/**
344-
* Read an image from GitBook itself.
318+
* Fetch an image from a URL and return a base64 encoded string.
319+
* We do this as @vercel/og is otherwise failing on SVG images referenced by a URL.
345320
*/
346-
async function readSelfImage(url: string) {
321+
async function fetchImage(url: string) {
347322
const response = await fetch(url);
348-
const image = await readImage(response);
349-
return image;
323+
324+
const contentType = response.headers.get('content-type');
325+
if (!contentType || !contentType.startsWith('image/')) {
326+
throw new Error(`Invalid content type: ${contentType}`);
327+
}
328+
329+
const arrayBuffer = await response.arrayBuffer();
330+
const buffer = Buffer.from(arrayBuffer);
331+
const base64 = buffer.toString('base64');
332+
const src = `data:${contentType};base64,${base64}`;
333+
334+
try {
335+
const { width, height } = imageSize(buffer);
336+
return { src, width, height };
337+
} catch (error) {
338+
console.error(`Error reading image size: ${error}`);
339+
return { src };
340+
}
350341
}

0 commit comments

Comments
 (0)