Skip to content

Fix rendering of ogimage with SVG logos #3328

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 2 commits into from
Jun 16, 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
5 changes: 5 additions & 0 deletions .changeset/green-clouds-cough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"gitbook": minor
---

Fix rendering of ogimage with SVG logos.
3 changes: 3 additions & 0 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -2067,6 +2068,8 @@

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

"image-size": ["[email protected]", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="],

"import-fresh": ["[email protected]", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],

"import-lazy": ["[email protected]", "", {}, "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A=="],
Expand Down
3 changes: 2 additions & 1 deletion packages/gitbook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
85 changes: 38 additions & 47 deletions packages/gitbook/src/routes/ogimage.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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()]);
Expand All @@ -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}
Expand Down Expand Up @@ -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>();

Expand All @@ -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 };
}
}