Try to fix error on og image generation (#2722)

This commit is contained in:
Greg Bergé
2025-01-10 21:38:25 +01:00
committed by GitHub
parent fde32e26bf
commit 09c7c30779
3 changed files with 125 additions and 98 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Try to fix error on og image generation
@@ -5,15 +5,31 @@ import { NextRequest } from 'next/server';
import colorContrast from 'postcss-color-contrast/js';
import React from 'react';
import { googleFontsMap } from '@/fonts';
import { getGitBookContextFromHeaders } from '@/lib/gitbook-context';
import { getAbsoluteHref } from '@/lib/links';
import { tcls } from '@/lib/tailwind';
import { filterOutNullable } from '@/lib/typescript';
import { getContentTitle } from '@/lib/utils';
import { PageIdParams, fetchPageData } from '../../../../fetch';
export const runtime = 'edge';
async function loadGoogleFont(font: string, text: string) {
const url = `https://fonts.googleapis.com/css2?family=${font}&text=${encodeURIComponent(text)}`;
const css = await (await fetch(url)).text();
const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/);
if (resource) {
const response = await fetch(resource[1]);
if (response.status == 200) {
return await response.arrayBuffer();
}
}
throw new Error('failed to load font data');
}
/**
* Render the OpenGraph image for a space.
*/
@@ -21,30 +37,60 @@ export async function GET(req: NextRequest, { params }: { params: Promise<PageId
const ctx = getGitBookContextFromHeaders(req.headers);
const { space, page, customization, site } = await fetchPageData(ctx, await params);
// If user configured a custom social preview, we redirect to it.
if (customization.socialPreview.url) {
// If user configured a custom social preview, we redirect to it.
redirect(customization.socialPreview.url);
}
// TODO: Support all fonts available in GitBook
// Right now this is impossible since next/font/google does not expose the cached font file
// Another option would be to use the Satori prop `loadAdditionalAsset` [example](https://github.com/vercel/satori/blob/main/playground/pages/index.tsx),
// but this prop isn't (yet) exposed through `ImageResponse`.
const interRegular = await fetch(
new URL('../../../../../../fonts/Inter/Inter-Regular.ttf', import.meta.url),
).then((res) => res.arrayBuffer());
const interBold = await fetch(
new URL('../../../../../../fonts/Inter/Inter-Bold.ttf', import.meta.url),
).then((res) => res.arrayBuffer());
// Compute all text to load only the necessary fonts
const contentTitle = customization.header.logo
? ''
: getContentTitle(space, customization, site ?? null);
const pageTitle = page
? page.title.length > 64
? page.title.slice(0, 64) + '...'
: page.title
: 'Not found';
const pageDescription =
page?.description && page?.title.length <= 64
? page.description.length > 164
? page.description.slice(0, 164) + '...'
: page.description
: '';
const fontFamily = googleFontsMap[customization.styling.font] ?? 'Inter';
const regularText = pageDescription;
const boldText = `${contentTitle}${pageTitle}`;
const fonts = (
await Promise.all([
regularText
? loadGoogleFont(`${fontFamily}:wght@400`, regularText).then((data) => ({
name: fontFamily,
data,
style: 'normal' as const,
weight: 400 as const,
}))
: null,
boldText
? loadGoogleFont(`${fontFamily}:wght@700`, `${contentTitle}${pageTitle}`).then(
(data) => ({
name: fontFamily,
data,
style: 'normal' as const,
weight: 700 as const,
}),
)
: null,
])
).filter(filterOutNullable);
const theme = customization.themes.default;
const useLightTheme = theme === 'light';
// We have no access to CSS variables, so we'll have to hardcode some values
const baseColors = {
light: '#ffffff',
dark: '#111827',
};
const baseColors = { light: '#ffffff', dark: '#111827' };
let colors = {
background: baseColors[theme],
@@ -92,49 +138,17 @@ export async function GET(req: NextRequest, { params }: { params: Promise<PageId
break;
}
const favicon = (() => {
if ('icon' in customization.favicon)
return (
<img
src={customization.favicon.icon[theme]}
width={40}
height={40}
tw={tcls('mr-4')}
alt="Icon"
/>
);
if ('emoji' in customization.favicon)
return (
<span tw={tcls('text-4xl', 'mr-4')}>
{String.fromCodePoint(parseInt('0x' + customization.favicon.emoji))}
</span>
);
const src = getAbsoluteHref(
ctx,
`~gitbook/icon?size=medium&theme=${customization.themes.default}`,
true,
);
return <img src={src} alt="Icon" width={40} height={40} tw={tcls('mr-4')} />;
})();
return new ImageResponse(
(
<div
tw={tcls(
'justify-between',
'p-20',
'relative',
'w-full',
'h-full',
'flex',
'flex-col',
`bg-[${colors.background}]`,
`text-[${colors.body}]`,
)}
tw={`justify-between p-20 relative w-full h-full flex flex-col bg-[${colors.background}] text-[${colors.body}]`}
style={{
fontFamily,
}}
>
{/* Gradient */}
<div
tw={tcls('absolute', 'inset-0')}
tw="absolute inset-0"
style={{
backgroundImage: `radial-gradient(ellipse 100% 100% at top right , ${colors.gradient}, ${colors.gradient}00)`,
opacity: 0.5,
@@ -142,11 +156,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<PageId
></div>
{/* Grid */}
<img
tw={tcls('absolute', 'inset-0', 'w-[100vw]', 'h-[100vh]')}
src={gridAsset}
alt="Grid"
/>
<img tw="absolute inset-0 w-[100vw] h-[100vh]" src={gridAsset} alt="Grid" />
{/* Logo */}
{customization.header.logo ? (
@@ -160,39 +170,46 @@ export async function GET(req: NextRequest, { params }: { params: Promise<PageId
}
/>
) : (
<div tw={tcls('flex')}>
{favicon}
<h3 tw={tcls('text-4xl', 'my-0')}>
{getContentTitle(space, customization, site ?? null)}
</h3>
<div tw="flex">
{(() => {
if ('icon' in customization.favicon)
return (
<img
src={customization.favicon.icon[theme]}
width={40}
height={40}
tw="mr-4"
alt="Icon"
/>
);
if ('emoji' in customization.favicon)
return (
<span tw="text-4xl mr-4">
{String.fromCodePoint(
parseInt('0x' + customization.favicon.emoji),
)}
</span>
);
const src = getAbsoluteHref(
ctx,
`~gitbook/icon?size=medium&theme=${customization.themes.default}`,
true,
);
return <img src={src} alt="Icon" width={40} height={40} tw="mr-4" />;
})()}
<h3 tw="text-4xl my-0 font-bold">{contentTitle}</h3>
</div>
)}
{/* Title and description */}
<div tw={tcls('flex', 'flex-col')}>
<div tw="flex flex-col">
<h1
tw={tcls(
'text-8xl',
'my-0',
'tracking-tight',
'leading-none',
'text-left',
`text-[${colors.title}]`,
'font-bold',
)}
tw={`text-8xl my-0 tracking-light leading-none text-left text-[${colors.title}] font-bold`}
>
{page
? page.title.length > 64
? page.title.slice(0, 64) + '...'
: page.title
: 'Not found'}
{pageTitle}
</h1>
{page?.description && page?.title.length <= 64 ? (
<h2 tw={tcls('text-4xl', 'mb-0', 'mt-8', 'w-[75%]', 'font-normal')}>
{page.description.length > 164
? page.description.slice(0, 164) + '...'
: page.description}
</h2>
{pageDescription ? (
<h2 tw="text-4xl mb-0 mt-8 w-[75%] font-normal">{pageDescription}</h2>
) : null}
</div>
</div>
@@ -200,20 +217,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<PageId
{
width: 1200,
height: 630,
fonts: [
{
name: 'Inter',
data: interRegular,
weight: 400,
style: 'normal',
},
{
name: 'Inter',
data: interBold,
weight: 700,
style: 'normal',
},
],
fonts,
},
);
}
+18
View File
@@ -203,3 +203,21 @@ export const fonts: { [fontName in CustomizationFont]: { className: string } } =
[CustomizationFont.Ubuntu]: ubuntu,
[CustomizationFont.ABCFavorit]: abcFavorit,
};
export const googleFontsMap: { [fontName in CustomizationFont]: string } = {
[CustomizationFont.Inter]: 'Inter',
[CustomizationFont.FiraSans]: 'Fira Sans Extra Condensed',
[CustomizationFont.IBMPlexSerif]: 'IBM Plex Serif',
[CustomizationFont.Lato]: 'Lato',
[CustomizationFont.Merriweather]: 'Merriweather',
[CustomizationFont.NotoSans]: 'Noto Sans',
[CustomizationFont.OpenSans]: 'Open Sans',
[CustomizationFont.Overpass]: 'Overpass',
[CustomizationFont.Poppins]: 'Poppins',
[CustomizationFont.Raleway]: 'Raleway',
[CustomizationFont.Roboto]: 'Roboto',
[CustomizationFont.RobotoSlab]: 'Roboto Slab',
[CustomizationFont.SourceSansPro]: 'Source Sans 3',
[CustomizationFont.Ubuntu]: 'Ubuntu',
[CustomizationFont.ABCFavorit]: 'Inter',
};