Skip to content

Commit fbfcca5

Browse files
authored
Fix rendering of font in ogimage (#3299)
1 parent 664ae8b commit fbfcca5

24 files changed

+624
-72
lines changed

.changeset/poor-dodos-lick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@gitbook/fonts": minor
3+
---
4+
5+
Initial version of the package

.changeset/warm-roses-sleep.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"gitbook": patch
3+
---
4+
5+
Fix ogimage using incorrect Google Font depending on language.

bun.lock

Lines changed: 91 additions & 7 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@
77
"turbo": "^2.5.0",
88
"vercel": "^39.3.0"
99
},
10-
"packageManager": "[email protected].11",
10+
"packageManager": "[email protected].15",
1111
"overrides": {
1212
"@codemirror/state": "6.4.1",
13-
"@gitbook/api": "^0.120.0",
1413
"react": "^19.0.0",
1514
"react-dom": "^19.0.0"
1615
},
@@ -34,7 +33,12 @@
3433
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local",
3534
"clean": "turbo run clean"
3635
},
37-
"workspaces": ["packages/*"],
36+
"workspaces": {
37+
"packages": ["packages/*"],
38+
"catalog": {
39+
"@gitbook/api": "^0.120.0"
40+
}
41+
},
3842
"patchedDependencies": {
3943
4044
"@vercel/[email protected]": "patches/@vercel%[email protected]"

packages/cache-tags/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
},
1111
"version": "0.3.1",
1212
"dependencies": {
13-
"@gitbook/api": "^0.120.0",
13+
"@gitbook/api": "catalog:",
1414
"assert-never": "^1.2.1"
1515
},
1616
"devDependencies": {

packages/fonts/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
dist/
2+
src/data/*.json

packages/fonts/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# `@gitbook/fonts`
2+
3+
Utilities to lookup default fonts supported by GitBook.

packages/fonts/bin/generate.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import fs from 'node:fs/promises';
2+
import path from 'node:path';
3+
4+
import { APIv2 } from 'google-font-metadata';
5+
6+
import { CustomizationDefaultFont } from '@gitbook/api';
7+
8+
import type { FontDefinitions } from '../src/types';
9+
10+
const googleFontsMap: { [fontName in CustomizationDefaultFont]: string } = {
11+
[CustomizationDefaultFont.Inter]: 'inter',
12+
[CustomizationDefaultFont.FiraSans]: 'fira-sans-extra-condensed',
13+
[CustomizationDefaultFont.IBMPlexSerif]: 'ibm-plex-serif',
14+
[CustomizationDefaultFont.Lato]: 'lato',
15+
[CustomizationDefaultFont.Merriweather]: 'merriweather',
16+
[CustomizationDefaultFont.NotoSans]: 'noto-sans',
17+
[CustomizationDefaultFont.OpenSans]: 'open-sans',
18+
[CustomizationDefaultFont.Overpass]: 'overpass',
19+
[CustomizationDefaultFont.Poppins]: 'poppins',
20+
[CustomizationDefaultFont.Raleway]: 'raleway',
21+
[CustomizationDefaultFont.Roboto]: 'roboto',
22+
[CustomizationDefaultFont.RobotoSlab]: 'roboto-slab',
23+
[CustomizationDefaultFont.SourceSansPro]: 'source-sans-3',
24+
[CustomizationDefaultFont.Ubuntu]: 'ubuntu',
25+
[CustomizationDefaultFont.ABCFavorit]: 'inter',
26+
};
27+
28+
/**
29+
* Scripts to generate the list of all icons.
30+
*/
31+
async function main() {
32+
// @ts-expect-error - we build the object
33+
const output: FontDefinitions = {};
34+
35+
for (const font of Object.values(CustomizationDefaultFont)) {
36+
const googleFontName = googleFontsMap[font];
37+
const fontMetadata = APIv2[googleFontName.toLowerCase()];
38+
if (!fontMetadata) {
39+
throw new Error(`Font ${googleFontName} not found`);
40+
}
41+
42+
output[font] = {
43+
font: googleFontName,
44+
unicodeRange: fontMetadata.unicodeRange,
45+
variants: {
46+
'400': {},
47+
'700': {},
48+
},
49+
};
50+
51+
Object.keys(output[font].variants).forEach((weight) => {
52+
const variants = fontMetadata.variants[weight];
53+
const normalVariant = variants.normal;
54+
if (!normalVariant) {
55+
throw new Error(`Font ${googleFontName} has no normal variant`);
56+
}
57+
58+
output[font].variants[weight] = {};
59+
Object.entries(normalVariant).forEach(([script, url]) => {
60+
output[font].variants[weight][script] = url.url.woff;
61+
});
62+
});
63+
}
64+
65+
await writeDataFile('fonts', JSON.stringify(output, null, 2));
66+
}
67+
68+
/**
69+
* We write both in dist and src as the build process might have happen already
70+
* and tsc doesn't copy the files.
71+
*/
72+
async function writeDataFile(name, content) {
73+
const srcData = path.resolve(__dirname, '../src/data');
74+
const distData = path.resolve(__dirname, '../dist/data');
75+
76+
// Ensure the directories exists
77+
await Promise.all([
78+
fs.mkdir(srcData, { recursive: true }),
79+
fs.mkdir(distData, { recursive: true }),
80+
]);
81+
82+
await Promise.all([
83+
fs.writeFile(path.resolve(srcData, `${name}.json`), content),
84+
fs.writeFile(path.resolve(distData, `${name}.json`), content),
85+
]);
86+
}
87+
88+
main().catch((error) => {
89+
console.error(`Error generating icons list: ${error}`);
90+
process.exit(1);
91+
});

packages/fonts/package.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "@gitbook/fonts",
3+
"type": "module",
4+
"exports": {
5+
".": {
6+
"types": "./dist/index.d.ts",
7+
"development": "./src/index.ts",
8+
"default": "./dist/index.js"
9+
}
10+
},
11+
"version": "0.0.0",
12+
"dependencies": {
13+
"@gitbook/api": "catalog:"
14+
},
15+
"devDependencies": {
16+
"google-font-metadata": "^6.0.3",
17+
"typescript": "^5.5.3"
18+
},
19+
"scripts": {
20+
"generate": "bun ./bin/generate.js",
21+
"build": "tsc --project tsconfig.build.json",
22+
"typecheck": "tsc --noEmit",
23+
"dev": "tsc -w",
24+
"clean": "rm -rf ./dist && rm -rf ./src/data",
25+
"unit": "bun test"
26+
},
27+
"files": ["dist", "src", "bin", "README.md", "CHANGELOG.md"],
28+
"engines": {
29+
"node": ">=20.0.0"
30+
}
31+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Bun Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`getDefaultFont should return correct object for Latin text 1`] = `
4+
{
5+
"font": "Inter",
6+
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
7+
}
8+
`;
9+
10+
exports[`getDefaultFont should return correct object for Cyrillic text 1`] = `
11+
{
12+
"font": "Inter",
13+
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
14+
}
15+
`;
16+
17+
exports[`getDefaultFont should return correct object for Greek text 1`] = `
18+
{
19+
"font": "Inter",
20+
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZxhjp-Ek-_0ewmM.woff",
21+
}
22+
`;
23+
24+
exports[`getDefaultFont should handle mixed script text 1`] = `
25+
{
26+
"font": "Inter",
27+
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff",
28+
}
29+
`;
30+
31+
exports[`getDefaultFont should handle different font weights: regular 1`] = `
32+
{
33+
"font": "Inter",
34+
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
35+
}
36+
`;
37+
38+
exports[`getDefaultFont should handle different font weights: bold 1`] = `
39+
{
40+
"font": "Inter",
41+
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYAZ9hjp-Ek-_0ew.woff",
42+
}
43+
`;
44+
45+
exports[`getDefaultFont should handle different fonts: inter 1`] = `
46+
{
47+
"font": "Inter",
48+
"url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff",
49+
}
50+
`;
51+
52+
exports[`getDefaultFont should handle different fonts: roboto 1`] = `
53+
{
54+
"font": "Roboto",
55+
"url": "https://fonts.gstatic.com/s/roboto/v32/KFOmCnqEu92Fr1Mu4mxMKTU1Kg.woff",
56+
}
57+
`;

packages/fonts/src/fonts.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { FontDefinitions } from './types';
2+
3+
import rawFonts from './data/fonts.json' with { type: 'json' };
4+
5+
export const fonts: FontDefinitions = rawFonts;
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { describe, expect, it } from 'bun:test';
2+
import { CustomizationDefaultFont } from '@gitbook/api';
3+
import { getDefaultFont } from './getDefaultFont';
4+
5+
describe('getDefaultFont', () => {
6+
it('should return null for invalid font', () => {
7+
const result = getDefaultFont({
8+
font: 'invalid-font' as CustomizationDefaultFont,
9+
text: 'Hello',
10+
weight: 400,
11+
});
12+
expect(result).toBeNull();
13+
});
14+
15+
it('should return null for invalid weight', () => {
16+
const result = getDefaultFont({
17+
font: CustomizationDefaultFont.Inter,
18+
text: 'Hello',
19+
weight: 999 as any,
20+
});
21+
expect(result).toBeNull();
22+
});
23+
24+
it('should return null for text not supported by any script', () => {
25+
const result = getDefaultFont({
26+
font: CustomizationDefaultFont.Inter,
27+
text: '😀', // Emoji not supported by Inter
28+
weight: 400,
29+
});
30+
expect(result).toBeNull();
31+
});
32+
33+
it('should return correct object for Latin text', () => {
34+
const result = getDefaultFont({
35+
font: CustomizationDefaultFont.Inter,
36+
text: 'Hello World',
37+
weight: 400,
38+
});
39+
expect(result).not.toBeNull();
40+
expect(result?.font).toBe(CustomizationDefaultFont.Inter);
41+
expect(result).toMatchSnapshot();
42+
});
43+
44+
it('should return correct object for Cyrillic text', () => {
45+
const result = getDefaultFont({
46+
font: CustomizationDefaultFont.Inter,
47+
text: 'Привет мир',
48+
weight: 400,
49+
});
50+
expect(result).not.toBeNull();
51+
expect(result?.font).toBe(CustomizationDefaultFont.Inter);
52+
expect(result).toMatchSnapshot();
53+
});
54+
55+
it('should return correct object for Greek text', () => {
56+
const result = getDefaultFont({
57+
font: CustomizationDefaultFont.Inter,
58+
text: 'Γεια σας',
59+
weight: 400,
60+
});
61+
expect(result).not.toBeNull();
62+
expect(result?.font).toBe(CustomizationDefaultFont.Inter);
63+
expect(result).toMatchSnapshot();
64+
});
65+
66+
it('should handle mixed script text', () => {
67+
const result = getDefaultFont({
68+
font: CustomizationDefaultFont.Inter,
69+
text: 'Hello Привет',
70+
weight: 400,
71+
});
72+
expect(result).not.toBeNull();
73+
expect(result?.font).toBe(CustomizationDefaultFont.Inter);
74+
expect(result).toMatchSnapshot();
75+
});
76+
77+
it('should handle different font weights', () => {
78+
const regular = getDefaultFont({
79+
font: CustomizationDefaultFont.Inter,
80+
text: 'Hello',
81+
weight: 400,
82+
});
83+
const bold = getDefaultFont({
84+
font: CustomizationDefaultFont.Inter,
85+
text: 'Hello',
86+
weight: 700,
87+
});
88+
expect(regular).not.toBeNull();
89+
expect(bold).not.toBeNull();
90+
expect(regular).toMatchSnapshot('regular');
91+
expect(bold).toMatchSnapshot('bold');
92+
});
93+
94+
it('should handle empty string', () => {
95+
const result = getDefaultFont({
96+
font: CustomizationDefaultFont.Inter,
97+
text: '',
98+
weight: 400,
99+
});
100+
expect(result).toBeNull();
101+
});
102+
103+
it('should handle different fonts', () => {
104+
const inter = getDefaultFont({
105+
font: CustomizationDefaultFont.Inter,
106+
text: 'Hello',
107+
weight: 400,
108+
});
109+
const roboto = getDefaultFont({
110+
font: CustomizationDefaultFont.Roboto,
111+
text: 'Hello',
112+
weight: 400,
113+
});
114+
expect(inter).not.toBeNull();
115+
expect(roboto).not.toBeNull();
116+
expect(inter).toMatchSnapshot('inter');
117+
expect(roboto).toMatchSnapshot('roboto');
118+
});
119+
});

0 commit comments

Comments
 (0)