Fix rendering of ogimage with SVG logos (#3328)

This commit is contained in:
Samy Pessé
2025-06-16 16:59:54 +02:00
committed by GitHub
parent 382a19885b
commit b7a0db3339
4 changed files with 48 additions and 48 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": minor
---
Fix rendering of ogimage with SVG logos.
+3
View File
@@ -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=="],
+2 -1
View File
@@ -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",
+38 -47
View File
@@ -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}`))}
</span>
);
const src = await readSelfImage(
const iconImage = await fetchImage(
linker.toAbsoluteURL(
linker.toPathInSpace(
`~gitbook/icon?size=medium&theme=${customization.themes.default}`
)
)
);
return <img src={src} alt="Icon" width={40} height={40} tw="mr-4" />;
return <img {...iconImage} alt="Icon" width={40} height={40} tw="mr-4" />;
};
const [favicon, { fontFamily, fonts }] = await Promise.all([faviconLoader(), fontLoader()]);
@@ -187,21 +188,23 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
{/* Grid */}
<img
tw="absolute inset-0 w-[100vw] h-[100vh]"
src={await readStaticImage(gridAsset)}
src={(await fetchStaticImage(gridAsset)).src}
alt="Grid"
/>
{/* Logo */}
{customization.header.logo ? (
<img
alt="Logo"
height={60}
src={
useLightTheme
? customization.header.logo.light
: customization.header.logo.dark
}
/>
<div tw="flex flex-row">
<img
{...(await fetchImage(
useLightTheme
? customization.header.logo.light
: customization.header.logo.dark
))}
alt="Logo"
tw="h-[60px]"
/>
</div>
) : (
<div tw="flex">
{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: <explanation>
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: <explanation>
const staticCache = new Map<string, any>();
@@ -335,16 +310,32 @@ async function getWithCache<T>(key: string, fn: () => Promise<T>) {
/**
* 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 };
}
}