Skip to content

Commit 0cbf588

Browse files
committed
chore: preload fonts
1 parent cffd211 commit 0cbf588

7 files changed

Lines changed: 111 additions & 4 deletions

File tree

.changeset/preload-fonts.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@doc-kit/generator-react': patch
3+
---
4+
5+
Preload the theme's fonts

packages/react/src/html/bundlers/vite.mjs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
mergeConfig,
1212
} from 'vite';
1313

14+
import { FONT_DIRECTORY } from '../constants.mjs';
15+
1416
const VIRTUAL_PREFIX = 'virtual:doc-kit/';
1517
const RESOLVED_VIRTUAL_PREFIX = '\0doc-kit:';
1618

@@ -252,6 +254,18 @@ export const createViteConfig = ({
252254
output: {
253255
...vite.build?.rolldownOptions?.output,
254256
format: 'es',
257+
258+
/**
259+
*
260+
*/
261+
assetFileNames: asset =>
262+
asset.names.some(name => name.endsWith('.woff2'))
263+
? // We need to know where the fonts are to preload
264+
// them. Using a dynamic hash would make this
265+
// difficult.
266+
`${FONT_DIRECTORY}/[name][extname]`
267+
: 'assets/[name]-[hash][extname]',
268+
255269
...(server
256270
? {
257271
entryFileNames: '[name].mjs',

packages/react/src/html/constants.mjs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,20 @@ export const JSX_IMPORTS = {
7171
},
7272
};
7373

74+
/**
75+
* Where the bundler emits fonts
76+
*/
77+
export const FONT_DIRECTORY = 'assets/fonts';
78+
79+
/**
80+
* Fonts to preload
81+
*/
82+
export const FONTS = [
83+
'open-sans-latin-wght-normal.woff2',
84+
'open-sans-latin-wght-italic.woff2',
85+
'ibm-plex-mono-latin-400-normal.woff2',
86+
];
87+
7488
/**
7589
* Specification rules for resource hints like prerendering and prefetching.
7690
* @see https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API

packages/react/src/html/template.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
<meta property="og:title" content="${title}" />
88
<meta property="og:type" content="website" />
99

10+
${preloads}
11+
1012
${head}
1113

1214
<!-- Apply theme before paint to avoid Flash of Unstyled Content -->

packages/react/src/html/ui/index.css

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,34 @@
1-
@import '@fontsource-variable/open-sans/wght.css';
2-
@import '@fontsource-variable/open-sans/wght-italic.css';
3-
@import '@fontsource/ibm-plex-mono/400.css';
41
@import '@node-core/ui-components/styles/index.css';
52
@import '@node-core/rehype-shiki/index.css';
63

4+
/* Fonts (We only load the three fonts that we preload) */
5+
@font-face {
6+
font-family: 'Open Sans Variable';
7+
font-style: normal;
8+
font-weight: 300 800;
9+
font-display: swap;
10+
src: url('@fontsource-variable/open-sans/files/open-sans-latin-wght-normal.woff2')
11+
format('woff2-variations');
12+
}
13+
14+
@font-face {
15+
font-family: 'Open Sans Variable';
16+
font-style: italic;
17+
font-weight: 300 800;
18+
font-display: swap;
19+
src: url('@fontsource-variable/open-sans/files/open-sans-latin-wght-italic.woff2')
20+
format('woff2-variations');
21+
}
22+
23+
@font-face {
24+
font-family: 'IBM Plex Mono';
25+
font-style: normal;
26+
font-weight: 400;
27+
font-display: swap;
28+
src: url('@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-400-normal.woff2')
29+
format('woff2');
30+
}
31+
732
/* Variables */
833
:root {
934
--font-open-sans: 'Open Sans Variable', sans-serif;

packages/react/src/html/utils/__tests__/processing.test.mjs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import {
66
setConfig,
77
} from '@doc-kit/core/utils/configuration/index.mjs';
88

9+
import { FONTS } from '../../constants.mjs';
910
import {
11+
buildPreloads,
1012
buildHead,
1113
populateWithEvaluation,
1214
resolvePageRoot,
@@ -111,6 +113,36 @@ describe('resolvePageRoot', () => {
111113
});
112114
});
113115

116+
describe('buildPreloads', () => {
117+
it('resolves every shipped font against the page root', () => {
118+
const result = buildPreloads('../');
119+
120+
// A hint per shipped face, or the unlisted ones load late after all.
121+
assert.strictEqual(result.match(/rel="preload"/g).length, FONTS.length);
122+
123+
for (const font of FONTS) {
124+
assert.ok(result.includes(`href="../assets/fonts/${font}"`));
125+
}
126+
});
127+
128+
it('keeps an absolute root absolute', () => {
129+
const result = buildPreloads('https://nodejs.org/docs/');
130+
131+
assert.ok(
132+
result.includes(`href="https://nodejs.org/docs/assets/fonts/${FONTS[0]}"`)
133+
);
134+
});
135+
136+
it('renders crossorigin valueless, since fonts are fetched in CORS mode', () => {
137+
// Without it the stylesheet re-fetches the font instead of reusing it.
138+
const hints = buildPreloads('./').split('\n');
139+
140+
for (const hint of hints) {
141+
assert.match(hint, /as="font" type="font\/woff2" crossorigin \/>$/);
142+
}
143+
});
144+
});
145+
114146
describe('buildHead', () => {
115147
it('renders meta tags from attribute bags', () => {
116148
const result = buildHead({

packages/react/src/html/utils/processing.mjs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import createConfigSource from './config.mjs';
55
import createProgramBuilder from './generate.mjs';
66
import { relativeOrAbsolute } from './relativeOrAbsolute.mjs';
77
import { resolveBundler } from '../bundlers/index.mjs';
8-
import { SPECULATION_RULES } from '../constants.mjs';
8+
import { FONT_DIRECTORY, FONTS, SPECULATION_RULES } from '../constants.mjs';
99
import { THEME_SCRIPT } from '../ui/theme-script.mjs';
1010

1111
/**
@@ -71,6 +71,20 @@ const renderTag = (tag, attrs) => {
7171
return `<${tag}${rendered} />`;
7272
};
7373

74+
/**
75+
* Renders the preload hints for a page
76+
*/
77+
export const buildPreloads = root =>
78+
FONTS.map(font =>
79+
renderTag('link', {
80+
rel: 'preload',
81+
href: `${root}${FONT_DIRECTORY}/${font}`,
82+
as: 'font',
83+
type: 'font/woff2',
84+
crossorigin: true,
85+
})
86+
).join('\n ');
87+
7488
/**
7589
* Builds the configurable `<head>` markup shared by every page from the
7690
* structured `head` config: `<meta>` tags, `<link>` tags, and raw HTML. None
@@ -183,6 +197,7 @@ export async function processBundles({
183197
entrypoint: bundler.getEntryId(data.api),
184198
speculationRules: SPECULATION_RULES,
185199
themeScript: THEME_SCRIPT,
200+
preloads: buildPreloads(root),
186201
root,
187202
metadata: data,
188203
config,

0 commit comments

Comments
 (0)