@@ -8,6 +8,7 @@ import { ImageResponse } from 'next/og';
8
8
import { type PageParams , fetchPageData } from '@/components/SitePage' ;
9
9
import { getFontSourcesToPreload } from '@/fonts/custom' ;
10
10
import { getAssetURL } from '@/lib/assets' ;
11
+ import { getExtension } from '@/lib/paths' ;
11
12
import { filterOutNullable } from '@/lib/typescript' ;
12
13
import { getCacheTag } from '@gitbook/cache-tags' ;
13
14
import type { GitBookSiteContext } from '@v2/lib/context' ;
@@ -32,7 +33,6 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
32
33
}
33
34
34
35
// Compute all text to load only the necessary fonts
35
- const contentTitle = customization . header . logo ? '' : site . title ;
36
36
const pageTitle = page
37
37
? page . title . length > 64
38
38
? `${ page . title . slice ( 0 , 64 ) } ...`
@@ -52,7 +52,7 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
52
52
const fontFamily = customization . styling . font ?? CustomizationDefaultFont . Inter ;
53
53
54
54
const regularText = pageDescription ;
55
- const boldText = `${ contentTitle } ${ pageTitle } ` ;
55
+ const boldText = `${ site . title } ${ pageTitle } ` ;
56
56
57
57
const fonts = (
58
58
await Promise . all ( [
@@ -164,10 +164,28 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
164
164
)
165
165
)
166
166
) ;
167
+ if ( ! iconImage ) {
168
+ throw new Error ( 'Icon image should always be fetchable' ) ;
169
+ }
170
+
167
171
return < img { ...iconImage } alt = "Icon" width = { 40 } height = { 40 } tw = "mr-4" /> ;
168
172
} ;
169
173
170
- const [ favicon , { fontFamily, fonts } ] = await Promise . all ( [ faviconLoader ( ) , fontLoader ( ) ] ) ;
174
+ const logoLoader = async ( ) => {
175
+ if ( ! customization . header . logo ) {
176
+ return null ;
177
+ }
178
+
179
+ return await fetchImage (
180
+ useLightTheme ? customization . header . logo . light : customization . header . logo . dark
181
+ ) ;
182
+ } ;
183
+
184
+ const [ favicon , logo , { fontFamily, fonts } ] = await Promise . all ( [
185
+ faviconLoader ( ) ,
186
+ logoLoader ( ) ,
187
+ fontLoader ( ) ,
188
+ ] ) ;
171
189
172
190
return new ImageResponse (
173
191
< div
@@ -193,22 +211,14 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
193
211
/>
194
212
195
213
{ /* Logo */ }
196
- { customization . header . logo ? (
214
+ { logo ? (
197
215
< 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
- />
216
+ < img { ...logo } alt = "Logo" tw = "h-[60px]" />
207
217
</ div >
208
218
) : (
209
219
< div tw = "flex" >
210
220
{ favicon }
211
- < h3 tw = "text-4xl my-0 font-bold" > { contentTitle } </ h3 >
221
+ < h3 tw = "text-4xl my-0 font-bold" > { site . title } </ h3 >
212
222
</ div >
213
223
) }
214
224
@@ -295,7 +305,9 @@ async function loadCustomFont(input: { url: string; weight: 400 | 700 }) {
295
305
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
296
306
const staticCache = new Map < string , any > ( ) ;
297
307
298
- // Do we need to limit the in-memory cache size? I think given the usage, we should be fine.
308
+ /**
309
+ * Get or initialize a value in the static cache.
310
+ */
299
311
async function getWithCache < T > ( key : string , fn : ( ) => Promise < T > ) {
300
312
const cached = staticCache . get ( key ) as T ;
301
313
if ( cached ) {
@@ -311,19 +323,46 @@ async function getWithCache<T>(key: string, fn: () => Promise<T>) {
311
323
* Read a static image and cache it in memory.
312
324
*/
313
325
async function fetchStaticImage ( url : string ) {
314
- return getWithCache ( `static-image:${ url } ` , ( ) => fetchImage ( url ) ) ;
326
+ return getWithCache ( `static-image:${ url } ` , async ( ) => {
327
+ const image = await fetchImage ( url ) ;
328
+ if ( ! image ) {
329
+ throw new Error ( 'Failed to fetch static image' ) ;
330
+ }
331
+
332
+ return image ;
333
+ } ) ;
315
334
}
316
335
336
+ /**
337
+ * @vercel /og supports the following image formats:
338
+ * Extracted from https://github.com/vercel/next.js/blob/canary/packages/next/src/compiled/%40vercel/og/index.node.js
339
+ */
340
+ const UNSUPPORTED_IMAGE_EXTENSIONS = [ '.avif' , '.webp' ] ;
341
+ const SUPPORTED_IMAGE_TYPES = [
342
+ 'image/png' ,
343
+ 'image/apng' ,
344
+ 'image/jpeg' ,
345
+ 'image/gif' ,
346
+ 'image/svg+xml' ,
347
+ ] ;
348
+
317
349
/**
318
350
* Fetch an image from a URL and return a base64 encoded string.
319
351
* We do this as @vercel/og is otherwise failing on SVG images referenced by a URL.
320
352
*/
321
353
async function fetchImage ( url : string ) {
354
+ // Skip early some images to avoid fetching them
355
+ const parsedURL = new URL ( url ) ;
356
+ if ( UNSUPPORTED_IMAGE_EXTENSIONS . includes ( getExtension ( parsedURL . pathname ) . toLowerCase ( ) ) ) {
357
+ return null ;
358
+ }
359
+
322
360
const response = await fetch ( url ) ;
323
361
362
+ // Filter out unsupported image types
324
363
const contentType = response . headers . get ( 'content-type' ) ;
325
- if ( ! contentType || ! contentType . startsWith ( 'image/' ) ) {
326
- throw new Error ( `Invalid content type: ${ contentType } ` ) ;
364
+ if ( ! contentType || ! SUPPORTED_IMAGE_TYPES . some ( ( type ) => contentType . includes ( type ) ) ) {
365
+ return null ;
327
366
}
328
367
329
368
const arrayBuffer = await response . arrayBuffer ( ) ;
@@ -334,8 +373,7 @@ async function fetchImage(url: string) {
334
373
try {
335
374
const { width, height } = imageSize ( buffer ) ;
336
375
return { src, width, height } ;
337
- } catch ( error ) {
338
- console . error ( `Error reading image size: ${ error } ` ) ;
339
- return { src } ;
376
+ } catch {
377
+ return null ;
340
378
}
341
379
}
0 commit comments