Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/preload-fonts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@doc-kit/generator-react': patch
---

Preload the theme's fonts
14 changes: 14 additions & 0 deletions packages/react/src/html/bundlers/vite.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
mergeConfig,
} from 'vite';

import { FONT_DIRECTORY } from '../constants.mjs';

const VIRTUAL_PREFIX = 'virtual:doc-kit/';
const RESOLVED_VIRTUAL_PREFIX = '\0doc-kit:';

Expand Down Expand Up @@ -252,6 +254,18 @@ export const createViteConfig = ({
output: {
...vite.build?.rolldownOptions?.output,
format: 'es',

/**
* Determine the asset names for different files
*/
assetFileNames: asset =>
Comment thread
avivkeller marked this conversation as resolved.
asset.names.some(name => name.endsWith('.woff2'))
? // We need to know where the fonts are to preload
// them. Using a dynamic hash would make this
// difficult.
`${FONT_DIRECTORY}/[name][extname]`
: 'assets/[name]-[hash][extname]',

...(server
? {
entryFileNames: '[name].mjs',
Expand Down
14 changes: 14 additions & 0 deletions packages/react/src/html/constants.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,20 @@ export const JSX_IMPORTS = {
},
};

/**
* Where the bundler emits fonts
*/
export const FONT_DIRECTORY = 'assets/fonts';

/**
* Fonts to preload
*/
export const FONTS = [
Comment thread
avivkeller marked this conversation as resolved.
'open-sans-latin-wght-normal.woff2',
'open-sans-latin-wght-italic.woff2',
'ibm-plex-mono-latin-400-normal.woff2',
];

/**
* Specification rules for resource hints like prerendering and prefetching.
* @see https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/html/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
<meta property="og:title" content="${title}" />
<meta property="og:type" content="website" />

${preloads}

${head}

<!-- Apply theme before paint to avoid Flash of Unstyled Content -->
Expand Down
31 changes: 28 additions & 3 deletions packages/react/src/html/ui/index.css
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
@import '@fontsource-variable/open-sans/wght.css';
@import '@fontsource-variable/open-sans/wght-italic.css';
@import '@fontsource/ibm-plex-mono/400.css';
@import '@node-core/ui-components/styles/index.css';
@import '@node-core/rehype-shiki/index.css';

/* Fonts (We only load the three fonts that we preload) */
@font-face {
font-family: 'Open Sans Variable';
font-style: normal;
font-weight: 300 800;
font-display: swap;
src: url('@fontsource-variable/open-sans/files/open-sans-latin-wght-normal.woff2')
format('woff2-variations');
}

@font-face {
font-family: 'Open Sans Variable';
font-style: italic;
font-weight: 300 800;
font-display: swap;
src: url('@fontsource-variable/open-sans/files/open-sans-latin-wght-italic.woff2')
format('woff2-variations');
}

@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-400-normal.woff2')
format('woff2');
}

/* Variables */
:root {
--font-open-sans: 'Open Sans Variable', sans-serif;
Expand Down
32 changes: 32 additions & 0 deletions packages/react/src/html/utils/__tests__/processing.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import {
setConfig,
} from '@doc-kit/core/utils/configuration/index.mjs';

import { FONTS } from '../../constants.mjs';
import {
buildPreloads,
buildHead,
populateWithEvaluation,
resolvePageRoot,
Expand Down Expand Up @@ -111,6 +113,36 @@ describe('resolvePageRoot', () => {
});
});

describe('buildPreloads', () => {
it('resolves every shipped font against the page root', () => {
const result = buildPreloads('../');

// A hint per shipped face, or the unlisted ones load late after all.
assert.strictEqual(result.match(/rel="preload"/g).length, FONTS.length);

for (const font of FONTS) {
assert.ok(result.includes(`href="../assets/fonts/${font}"`));
}
});

it('keeps an absolute root absolute', () => {
const result = buildPreloads('https://nodejs.org/docs/');

assert.ok(
result.includes(`href="https://nodejs.org/docs/assets/fonts/${FONTS[0]}"`)
);
});

it('renders crossorigin valueless, since fonts are fetched in CORS mode', () => {
// Without it the stylesheet re-fetches the font instead of reusing it.
const hints = buildPreloads('./').split('\n');

for (const hint of hints) {
assert.match(hint, /as="font" type="font\/woff2" crossorigin \/>$/);
}
});
});

describe('buildHead', () => {
it('renders meta tags from attribute bags', () => {
const result = buildHead({
Expand Down
17 changes: 16 additions & 1 deletion packages/react/src/html/utils/processing.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import createConfigSource from './config.mjs';
import createProgramBuilder from './generate.mjs';
import { relativeOrAbsolute } from './relativeOrAbsolute.mjs';
import { resolveBundler } from '../bundlers/index.mjs';
import { SPECULATION_RULES } from '../constants.mjs';
import { FONT_DIRECTORY, FONTS, SPECULATION_RULES } from '../constants.mjs';
import { THEME_SCRIPT } from '../ui/theme-script.mjs';

/**
Expand Down Expand Up @@ -71,6 +71,20 @@ const renderTag = (tag, attrs) => {
return `<${tag}${rendered} />`;
};

/**
* Renders the preload hints for a page
*/
export const buildPreloads = root =>
FONTS.map(font =>
renderTag('link', {
rel: 'preload',
href: `${root}${FONT_DIRECTORY}/${font}`,
as: 'font',
type: 'font/woff2',
crossorigin: true,
})
).join('\n ');

/**
* Builds the configurable `<head>` markup shared by every page from the
* structured `head` config: `<meta>` tags, `<link>` tags, and raw HTML. None
Expand Down Expand Up @@ -183,6 +197,7 @@ export async function processBundles({
entrypoint: bundler.getEntryId(data.api),
speculationRules: SPECULATION_RULES,
themeScript: THEME_SCRIPT,
preloads: buildPreloads(root),
root,
metadata: data,
config,
Expand Down
Loading