Improve default site icon (#3679)

This commit is contained in:
Zeno Kapitein
2025-09-24 10:22:49 +02:00
committed by GitHub
parent f173e014db
commit c06b3dd60d
6 changed files with 249 additions and 117 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Improve default site icon
@@ -42,11 +42,11 @@ export function CurrentContentIcon(
}
: {
light: {
src: linker.toPathInSpace('~gitbook/icon?size=medium&theme=light'),
src: linker.toPathInSpace('~gitbook/icon?size=large&theme=light'),
size: { width: 256, height: 256 },
},
dark: {
src: linker.toPathInSpace('~gitbook/icon?size=medium&theme=dark'),
src: linker.toPathInSpace('~gitbook/icon?size=large&theme=dark'),
size: { width: 256, height: 256 },
},
}
@@ -108,6 +108,8 @@ export async function generateSiteLayoutMetadata(context: GitBookSiteContext): P
const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null;
const faviconSize = 48;
const appIconSize = 180;
const icons = await Promise.all(
[
{
@@ -140,12 +142,44 @@ export async function generateSiteLayoutMetadata(context: GitBookSiteContext): P
}))
);
const appIcons = await Promise.all(
[
{
url: customIcon?.light
? getResizedImageURL(imageResizer, customIcon.light, {
width: appIconSize,
height: appIconSize,
})
: linker.toAbsoluteURL(
linker.toPathInSpace('~gitbook/icon?size=medium&theme=light&border=false')
),
type: 'image/png',
media: '(prefers-color-scheme: light)',
},
{
url: customIcon?.dark
? getResizedImageURL(imageResizer, customIcon.dark, {
width: appIconSize,
height: appIconSize,
})
: linker.toAbsoluteURL(
linker.toPathInSpace('~gitbook/icon?size=medium&theme=dark&border=false')
),
type: 'image/png',
media: '(prefers-color-scheme: dark)',
},
].map(async (icon) => ({
...icon,
url: await icon.url,
}))
);
return {
title: site.title,
generator: `GitBook (${buildVersion()})`,
icons: {
icon: icons,
apple: icons,
apple: appIcons,
},
appleWebApp: {
capable: true,
+123
View File
@@ -0,0 +1,123 @@
import { CustomizationDefaultFont } from '@gitbook/api';
import { type FontWeight, getDefaultFont } from '@gitbook/fonts';
import { getFontSourcesToPreload } from '@/fonts/custom';
import type { GitBookSiteContext } from '@/lib/context';
import { filterOutNullable } from '@/lib/typescript';
type ComputeFontsInput = {
regularText: string;
boldText: string;
};
export async function computeImageFonts(
customization: GitBookSiteContext['customization'],
input: ComputeFontsInput
) {
// Google fonts
if (typeof customization.styling.font === 'string') {
const fontFamily = customization.styling.font ?? CustomizationDefaultFont.Inter;
const fonts = (
await Promise.all([
loadGoogleFont({ font: fontFamily, text: input.regularText, weight: 400 }),
loadGoogleFont({ font: fontFamily, text: input.boldText, weight: 700 }),
])
).filter(filterOutNullable);
return { fontFamily, fonts } as const;
}
// Custom fonts
// We only load the primary font weights for now
const primaryFontWeights = getFontSourcesToPreload(customization.styling.font);
const fonts = (
await Promise.all(
primaryFontWeights.map((face) => {
const { weight, sources } = face;
const source = sources[0];
// Satori doesn't support WOFF2, so we skip it
// https://github.com/vercel/satori?tab=readme-ov-file#fonts
if (!source || source.format === 'woff2' || source.url.endsWith('.woff2')) {
return null;
}
return loadCustomFont({ url: source.url, weight: weight as 400 | 700 });
})
)
).filter(filterOutNullable);
return { fontFamily: 'CustomFont', fonts } as const;
}
async function loadGoogleFont(input: {
font: CustomizationDefaultFont;
text: string;
weight: FontWeight;
}) {
const lookup = getDefaultFont({
font: input.font,
text: input.text,
weight: input.weight,
});
// If we found a font file, load it
if (lookup) {
return getWithCache(`google-font-files:${lookup.url}`, async () => {
const response = await fetch(lookup.url);
if (response.ok) {
const data = await response.arrayBuffer();
return {
name: lookup.font,
data,
style: 'normal' as const,
weight: input.weight,
};
}
});
}
// If for some reason we can't load the font, we'll just use the default one
return null;
}
async function loadCustomFont(input: { url: string; weight: 400 | 700 }) {
const { url, weight } = input;
const response = await fetch(url);
if (!response.ok) {
return null;
}
const data = await response.arrayBuffer();
return {
name: 'CustomFont',
data,
style: 'normal' as const,
weight,
} as const;
}
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
const staticCache = new Map<string, any>();
async function getWithCache<T>(key: string, fn: () => Promise<T>) {
const cached = staticCache.get(key) as Promise<T>;
if (cached) {
return cached;
}
const promise = fn();
staticCache.set(key, promise);
try {
const result = await promise;
return result;
} catch (error) {
// Remove the failed promise from cache so it can be retried
staticCache.delete(key);
throw error;
}
}
+76 -21
View File
@@ -3,9 +3,11 @@ import { ImageResponse } from 'next/og';
import type { GitBookSiteContext } from '@/lib/context';
import { getEmojiForCode } from '@/lib/emojis';
import { computeImageFonts } from '@/lib/imageFonts';
import { getResizedImageURL } from '@/lib/images';
import { tcls } from '@/lib/tailwind';
import { getCacheTag } from '@gitbook/cache-tags';
import { colorScale } from '@gitbook/colors';
const SIZES = {
/** Size for a favicon */
@@ -13,20 +15,39 @@ const SIZES = {
width: 48,
height: 48,
textSize: 'text-[32px]',
boxStyle: 'rounded-[8px]',
boxStyle: 8,
borderWidth: 1,
},
/** Size for display as an app icon or in the header */
/** Size for display as iOS app icon */
medium: {
width: 180,
height: 180,
textSize: 'text-[115px]',
boxStyle: 22.5,
borderWidth: 3,
},
/** Size for display in the site header */
large: {
width: 256,
height: 256,
textSize: 'text-[164px]',
boxStyle: 'rounded-[32px]',
boxStyle: 32,
borderWidth: 4,
},
/** Size for display as Android app icon */
xlarge: {
width: 512,
height: 512,
textSize: 'text-[328px]',
boxStyle: 64,
borderWidth: 8,
},
};
type RenderIconOptions = {
size: keyof typeof SIZES;
theme: 'light' | 'dark';
border: boolean;
};
/**
@@ -50,18 +71,33 @@ export async function serveIcon(context: GitBookSiteContext, req: Request) {
);
}
return new ImageResponse(<SiteDefaultIcon context={context} options={options} />, {
width: size.width,
height: size.height,
headers: {
'cache-tag': [
getCacheTag({
tag: 'site',
site: context.site.id,
}),
].join(','),
},
// Compute font loading based on site title or emoji
const regularText = '';
const boldText =
'emoji' in customization.favicon ? '' : context.site.title.slice(0, 1).toUpperCase();
const { fontFamily, fonts } = await computeImageFonts(customization, {
regularText,
boldText,
});
return new ImageResponse(
<div style={{ fontFamily, width: '100%', height: '100%', display: 'flex' }}>
<SiteDefaultIcon context={context} options={options} />
</div>,
{
width: size.width,
height: size.height,
fonts: fonts.length ? fonts : undefined,
headers: {
'cache-tag': [
getCacheTag({
tag: 'site',
site: context.site.id,
}),
].join(','),
},
}
);
}
/**
@@ -79,25 +115,40 @@ export function SiteDefaultIcon(props: {
const { site, customization } = context;
const contentTitle = site.title;
const primaryScale = colorScale(customization.styling.primaryColor[options.theme], {
darkMode: options.theme === 'dark',
});
console.log('options', options);
return (
<div
tw={tcls(options.theme === 'light' ? 'bg-white' : 'bg-black', size.boxStyle, tw)}
tw={tcls(tw)}
style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderWidth: options.border ? size.borderWidth : 0,
borderColor: primaryScale[7],
borderRadius: options.border
? customization.styling.corners === 'rounded'
? `${size.boxStyle}px`
: customization.styling.corners === 'circular'
? '50%'
: '0px'
: 0,
background: `linear-gradient(to bottom, ${primaryScale[options.theme === 'light' ? 2 : 6]} 0%, ${primaryScale[4]} 100%)`,
...style,
}}
>
<h2
tw={tcls(
size.textSize,
'font-bold',
'tracking-tight',
options.theme === 'light' ? 'text-black' : 'text-white'
)}
tw={tcls(size.textSize, 'font-bold', 'tracking-tight')}
style={{
color: primaryScale[10],
textShadow: `0 .05em .1em ${primaryScale[7]}`,
}}
>
{'emoji' in customization.favicon
? getEmojiForCode(customization.favicon.emoji)
@@ -110,10 +161,12 @@ export function SiteDefaultIcon(props: {
function getOptions(inputUrl: string): {
size: keyof typeof SIZES;
theme: 'light' | 'dark';
border: boolean;
} {
const url = new URL(inputUrl);
const sizeParam = (url.searchParams.get('size') ?? 'small') as keyof typeof SIZES;
const themeParam = url.searchParams.get('theme') ?? 'light';
const borderParam = url.searchParams.get('border') !== 'false';
if (!SIZES[sizeParam] || !['light', 'dark'].includes(themeParam)) {
notFound();
@@ -124,5 +177,7 @@ function getOptions(inputUrl: string): {
size: sizeParam,
// @ts-ignore
theme: themeParam,
// @ts-ignore
border: borderParam,
};
}
+8 -93
View File
@@ -1,15 +1,14 @@
import { CustomizationDefaultFont, CustomizationHeaderPreset } from '@gitbook/api';
import { CustomizationHeaderPreset } from '@gitbook/api';
import { colorContrast } from '@gitbook/colors';
import { type FontWeight, getDefaultFont } from '@gitbook/fonts';
import { direction } from 'direction';
import { imageSize } from 'image-size';
import { redirect } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { type PageParams, fetchPageData } from '@/components/SitePage';
import { getFontSourcesToPreload } from '@/fonts/custom';
import { getAssetURL } from '@/lib/assets';
import type { GitBookSiteContext } from '@/lib/context';
import { computeImageFonts } from '@/lib/imageFonts';
import {
type ResizeImageOptions,
SizableImageAction,
@@ -18,7 +17,6 @@ import {
resizeImage,
} from '@/lib/images';
import { getExtension } from '@/lib/paths';
import { filterOutNullable } from '@/lib/typescript';
import { getCacheTag } from '@gitbook/cache-tags';
import { SiteDefaultIcon } from './icon';
@@ -54,47 +52,11 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
: '';
// Load the fonts
const fontLoader = async () => {
// google fonts
if (typeof customization.styling.font === 'string') {
const fontFamily = customization.styling.font ?? CustomizationDefaultFont.Inter;
const regularText = pageDescription;
const boldText = `${site.title} ${pageTitle}`;
const fonts = (
await Promise.all([
loadGoogleFont({ font: fontFamily, text: regularText, weight: 400 }),
loadGoogleFont({ font: fontFamily, text: boldText, weight: 700 }),
])
).filter(filterOutNullable);
return { fontFamily, fonts };
}
// custom fonts
// We only load the primary font weights for now
const primaryFontWeights = getFontSourcesToPreload(customization.styling.font);
const fonts = (
await Promise.all(
primaryFontWeights.map((face) => {
const { weight, sources } = face;
const source = sources[0];
// Satori doesn't support WOFF2, so we skip it
// https://github.com/vercel/satori?tab=readme-ov-file#fonts
if (!source || source.format === 'woff2' || source.url.endsWith('.woff2')) {
return null;
}
return loadCustomFont({ url: source.url, weight });
})
)
).filter(filterOutNullable);
return { fontFamily: 'CustomFont', fonts };
};
const fontLoader = async () =>
computeImageFonts(customization, {
regularText: pageDescription,
boldText: `${site.title} ${pageTitle}`,
});
const theme = customization.themes.default;
const useLightTheme = theme === 'light';
@@ -173,6 +135,7 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
options={{
size: 'small',
theme,
border: true,
}}
style={faviconSize}
/>
@@ -269,54 +232,6 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
);
}
async function loadGoogleFont(input: {
font: CustomizationDefaultFont;
text: string;
weight: FontWeight;
}) {
const lookup = getDefaultFont({
font: input.font,
text: input.text,
weight: input.weight,
});
// If we found a font file, load it
if (lookup) {
return getWithCache(`google-font-files:${lookup.url}`, async () => {
const response = await fetch(lookup.url);
if (response.ok) {
const data = await response.arrayBuffer();
return {
name: lookup.font,
data,
style: 'normal' as const,
weight: input.weight,
};
}
});
}
// If for some reason we can't load the font, we'll just use the default one
return null;
}
async function loadCustomFont(input: { url: string; weight: 400 | 700 }) {
const { url, weight } = input;
const response = await fetch(url);
if (!response.ok) {
return null;
}
const data = await response.arrayBuffer();
return {
name: 'CustomFont',
data,
style: 'normal' as const,
weight,
};
}
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
const staticCache = new Map<string, any>();