diff --git a/.changeset/green-clouds-cough.md b/.changeset/green-clouds-cough.md new file mode 100644 index 0000000000..0988603814 --- /dev/null +++ b/.changeset/green-clouds-cough.md @@ -0,0 +1,5 @@ +--- +"gitbook": minor +--- + +Fix rendering of ogimage with SVG logos. diff --git a/bun.lock b/bun.lock index b4d2b49d1f..1c192ca717 100644 --- a/bun.lock +++ b/bun.lock @@ -87,6 +87,7 @@ "classnames": "^2.5.1", "event-iterator": "^2.0.0", "framer-motion": "^10.16.14", + "image-size": "^2.0.2", "js-cookie": "^3.0.5", "jsontoxml": "^1.0.1", "jwt-decode": "^4.0.0", @@ -2067,6 +2068,8 @@ "ignore": ["ignore@7.0.3", "", {}, "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA=="], + "image-size": ["image-size@2.0.2", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="], + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "import-lazy": ["import-lazy@2.1.0", "", {}, "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A=="], diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index 379f975a1e..2be76259d9 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -81,7 +81,8 @@ "usehooks-ts": "^3.1.0", "zod": "^3.24.2", "zod-to-json-schema": "^3.24.5", - "zustand": "^5.0.3" + "zustand": "^5.0.3", + "image-size": "^2.0.2" }, "devDependencies": { "@argos-ci/playwright": "^5.0.3", diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx index e213ee2238..653fc8fc3b 100644 --- a/packages/gitbook/src/routes/ogimage.tsx +++ b/packages/gitbook/src/routes/ogimage.tsx @@ -1,6 +1,7 @@ import { CustomizationDefaultFont, CustomizationHeaderPreset } from '@gitbook/api'; import { colorContrast } from '@gitbook/colors'; import { type FontWeight, getDefaultFont } from '@gitbook/fonts'; +import { imageSize } from 'image-size'; import { redirect } from 'next/navigation'; import { ImageResponse } from 'next/og'; @@ -156,14 +157,14 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page {String.fromCodePoint(Number.parseInt(`0x${customization.favicon.emoji}`))} ); - const src = await readSelfImage( + const iconImage = await fetchImage( linker.toAbsoluteURL( linker.toPathInSpace( `~gitbook/icon?size=medium&theme=${customization.themes.default}` ) ) ); - return Icon; + return Icon; }; const [favicon, { fontFamily, fonts }] = await Promise.all([faviconLoader(), fontLoader()]); @@ -187,21 +188,23 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page {/* Grid */} Grid {/* Logo */} {customization.header.logo ? ( - Logo +
+ Logo +
) : (
{favicon} @@ -289,34 +292,6 @@ async function loadCustomFont(input: { url: string; weight: 400 | 700 }) { }; } -/** - * Temporary function to log some data on Cloudflare. - * TODO: remove this when we found the issue - */ -function logOnCloudflareOnly(message: string) { - if (process.env.DEBUG_CLOUDFLARE === 'true') { - // biome-ignore lint/suspicious/noConsole: - console.log(message); - } -} - -/** - * Read an image from a response as a base64 encoded string. - */ -async function readImage(response: Response) { - const contentType = response.headers.get('content-type'); - if (!contentType || !contentType.startsWith('image/')) { - logOnCloudflareOnly(`Invalid content type: ${contentType}, - status: ${response.status} - rayId: ${response.headers.get('cf-ray')}`); - throw new Error(`Invalid content type: ${contentType}`); - } - - const arrayBuffer = await response.arrayBuffer(); - const base64 = Buffer.from(arrayBuffer).toString('base64'); - return `data:${contentType};base64,${base64}`; -} - // biome-ignore lint/suspicious/noExplicitAny: const staticCache = new Map(); @@ -335,16 +310,32 @@ async function getWithCache(key: string, fn: () => Promise) { /** * Read a static image and cache it in memory. */ -async function readStaticImage(url: string) { - logOnCloudflareOnly(`Reading static image: ${url}, cache size: ${staticCache.size}`); - return getWithCache(`static-image:${url}`, () => readSelfImage(url)); +async function fetchStaticImage(url: string) { + return getWithCache(`static-image:${url}`, () => fetchImage(url)); } /** - * Read an image from GitBook itself. + * Fetch an image from a URL and return a base64 encoded string. + * We do this as @vercel/og is otherwise failing on SVG images referenced by a URL. */ -async function readSelfImage(url: string) { +async function fetchImage(url: string) { const response = await fetch(url); - const image = await readImage(response); - return image; + + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.startsWith('image/')) { + throw new Error(`Invalid content type: ${contentType}`); + } + + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + const base64 = buffer.toString('base64'); + const src = `data:${contentType};base64,${base64}`; + + try { + const { width, height } = imageSize(buffer); + return { src, width, height }; + } catch (error) { + console.error(`Error reading image size: ${error}`); + return { src }; + } }