@@ -8,6 +8,7 @@ import { ImageResponse } from 'next/og';
88import { type PageParams , fetchPageData } from '@/components/SitePage' ;
99import { getFontSourcesToPreload } from '@/fonts/custom' ;
1010import { getAssetURL } from '@/lib/assets' ;
11+ import { getExtension } from '@/lib/paths' ;
1112import { filterOutNullable } from '@/lib/typescript' ;
1213import { getCacheTag } from '@gitbook/cache-tags' ;
1314import type { GitBookSiteContext } from '@v2/lib/context' ;
@@ -32,7 +33,6 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
3233 }
3334
3435 // Compute all text to load only the necessary fonts
35- const contentTitle = customization . header . logo ? '' : site . title ;
3636 const pageTitle = page
3737 ? page . title . length > 64
3838 ? `${ page . title . slice ( 0 , 64 ) } ...`
@@ -52,7 +52,7 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
5252 const fontFamily = customization . styling . font ?? CustomizationDefaultFont . Inter ;
5353
5454 const regularText = pageDescription ;
55- const boldText = `${ contentTitle } ${ pageTitle } ` ;
55+ const boldText = `${ site . title } ${ pageTitle } ` ;
5656
5757 const fonts = (
5858 await Promise . all ( [
@@ -164,10 +164,28 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
164164 )
165165 )
166166 ) ;
167+ if ( ! iconImage ) {
168+ throw new Error ( 'Icon image should always be fetchable' ) ;
169+ }
170+
167171 return < img { ...iconImage } alt = "Icon" width = { 40 } height = { 40 } tw = "mr-4" /> ;
168172 } ;
169173
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+ ] ) ;
171189
172190 return new ImageResponse (
173191 < div
@@ -193,22 +211,14 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
193211 />
194212
195213 { /* Logo */ }
196- { customization . header . logo ? (
214+ { logo ? (
197215 < 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]" />
207217 </ div >
208218 ) : (
209219 < div tw = "flex" >
210220 { favicon }
211- < h3 tw = "text-4xl my-0 font-bold" > { contentTitle } </ h3 >
221+ < h3 tw = "text-4xl my-0 font-bold" > { site . title } </ h3 >
212222 </ div >
213223 ) }
214224
@@ -295,7 +305,9 @@ async function loadCustomFont(input: { url: string; weight: 400 | 700 }) {
295305// biome-ignore lint/suspicious/noExplicitAny: <explanation>
296306const staticCache = new Map < string , any > ( ) ;
297307
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+ */
299311async function getWithCache < T > ( key : string , fn : ( ) => Promise < T > ) {
300312 const cached = staticCache . get ( key ) as T ;
301313 if ( cached ) {
@@ -311,19 +323,46 @@ async function getWithCache<T>(key: string, fn: () => Promise<T>) {
311323 * Read a static image and cache it in memory.
312324 */
313325async 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+ } ) ;
315334}
316335
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+
317349/**
318350 * Fetch an image from a URL and return a base64 encoded string.
319351 * We do this as @vercel/og is otherwise failing on SVG images referenced by a URL.
320352 */
321353async 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+
322360 const response = await fetch ( url ) ;
323361
362+ // Filter out unsupported image types
324363 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 ;
327366 }
328367
329368 const arrayBuffer = await response . arrayBuffer ( ) ;
@@ -334,8 +373,7 @@ async function fetchImage(url: string) {
334373 try {
335374 const { width, height } = imageSize ( buffer ) ;
336375 return { src, width, height } ;
337- } catch ( error ) {
338- console . error ( `Error reading image size: ${ error } ` ) ;
339- return { src } ;
376+ } catch {
377+ return null ;
340378 }
341379}
0 commit comments