Skip to content
Open
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 =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just ooc: how Next.js preloads fonts?

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 = [

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid doing static lists? Maybe a function that we define per font we need that creates the css block and the import and the loading like Next.ja?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do it that way, it's slower, since we need to minify the dynamically created HTML. This way we can do one minification pass

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why it would be slower? 🤔

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, maybe you're not visualizing the idea:

const [codeToAddToHead, fontFamilySnippet] = generateFontSnippet('open-sans-latin-wght-italic', ['woff']);

fgirst argument being the font name, 2nd being the supported formats.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, maybe you're not visualizing the idea:

const [codeToAddToHead, fontFamilySnippet] = generateFontSnippet('open-sans-latin-wght-italic', ['woff']);

fgirst argument being the font name, 2nd being the supported formats.

Each font has a different snippet, so that's not an option

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, each font has a different snippet. So you append instead of https://github.com/nodejs/doc-kit/pull/1018/changes#diff-71b846ed4741eff9604ce9e99abc324eb6dafbb8724dc337f6eefde00dae282aR10

Youll have

${fontOnePreload}
${fontTwoPreload}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is genuinely what Next.js does, annd makes maintainability much easier/reduces hard-coded pieces.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to do it like that. I'd prefer a preloads section with a hardcoded font list so that the bundler can decide how it does prelaods.

Doing ${preloadA} ${preloadB} makes the template not very overrideable, should we add or remove fonts

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel we're at an impass 😅 -- I feel we should avoid hard-coded list of fonts and allow this to be customizable/used by the end-user rather than the way it is lied out currently.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw we need to reach consensus before RenderATL 🙈

'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