Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"resolutions": {
"@nuxt/fonts>fontaine": "latest",
"fontaine": "workspace:*",
"fontless": "workspace:*"
"fontless": "workspace:*",
"unifont": "0.6.0"
},
"simple-git-hooks": {
"pre-commit": "npx lint-staged"
Expand Down
2 changes: 1 addition & 1 deletion packages/fontless/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ fontless({

// Default font settings
defaults: {
preload: true,
preload: { subsets: ['latin'] }, // select preload fonts by subset
weights: [400, 700],
styles: ['normal', 'italic'],
fallbacks: {
Expand Down
6 changes: 6 additions & 0 deletions packages/fontless/examples/tailwind/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ export default defineConfig({
tailwindcss(),
fontless({
provider: 'google',
families: [
{
name: 'Geist',
preload: { subsets: ['latin'] },
}
]
}),
],
})
2 changes: 1 addition & 1 deletion packages/fontless/examples/vanilla-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"private": true,
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/fontless/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"ufo": "^1.6.1",
"unifont": "^0.5.2",
"unifont": "^0.6.0",
"unstorage": "^1.17.1"
},
"devDependencies": {
Expand Down
16 changes: 10 additions & 6 deletions packages/fontless/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,7 @@ export interface FontFamilyOverrides {
name: string
/** Inject `@font-face` regardless of usage in project. */
global?: boolean
/**
* Enable or disable adding preload links to the initially rendered HTML.
* This is true by default for the highest priority format unless a font is subsetted (to avoid over-preloading).
*/
preload?: boolean
preload?: PreloadOption

// TODO:
// as?: string
Expand All @@ -66,6 +62,14 @@ export interface FontFamilyManualOverride extends FontFamilyOverrides, RawFontFa

type ProviderOption = ((options: any) => Provider) | string | false

/**
* Enable adding preload links to the initially rendered HTML.
* With `subsets`, you can specify which subsets to preload.
* @default false
* @example { subsets: ['latin'] }
*/
type PreloadOption = boolean | { subsets: string[] }
Copy link
Member

Choose a reason for hiding this comment

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

we might also want to choose preload based on font file extensions (defaulting to only .woff2). maybe we could support just passing a function directly? e.g. preload?: boolean | ((fontFamily: string, data: FontFaceData) => boolean)

Copy link
Collaborator Author

@hi-ogawa hi-ogawa Sep 30, 2025

Choose a reason for hiding this comment

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

Yes, making it more flexible sounds good to me, but my intention was that subsets based preload selection should provide optional most of the case, so simple option preload.subset might be good enough to start with. Btw, woff (meta.priority = 1) returned by google provider is not a subsetted font, which is why it's large and thus they don't get included in current preload.subsets.

That said, I was also thinking that maybe we can later expand option to { subsets: string[], weights: string[], etc. }, which may become tricky, so I agree with exposing boolean function. However, I think we should also keep simple preload.subsets option.


export interface FontlessOptions {
/**
* Specify overrides for individual font families.
Expand All @@ -85,7 +89,7 @@ export interface FontlessOptions {
*/
families?: Array<FontFamilyManualOverride | FontFamilyProviderOverride>
defaults?: Partial<{
preload: boolean
preload?: PreloadOption
weights: Array<string | number>
styles: ResolveFontOptions['styles']
subsets: ResolveFontOptions['subsets']
Expand Down
11 changes: 10 additions & 1 deletion packages/fontless/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ export interface FontFamilyInjectionPluginOptions {
resolveFontFace: (fontFamily: string, fallbackOptions?: { fallbacks: string[], generic?: GenericCSSFamily }) => Awaitable<undefined | FontFaceResolution>
dev: boolean
processCSSVariables?: boolean | 'font-prefixed-only'
/** @deprecated use `filterFontsToPreload` instead */
shouldPreload: (fontFamily: string, font: FontFaceData) => boolean
filterFontsToPreload?: (fontFamily: string, fonts: FontFaceData[]) => FontFaceData[]
fontsToPreload: Map<string, Set<string>>
}

Expand Down Expand Up @@ -96,8 +98,15 @@ export async function transformCSS(options: FontFamilyInjectionPluginOptions, co
let insertFontFamilies = false

const [topPriorityFont] = result.fonts.sort((a, b) => (a.meta?.priority || 0) - (b.meta?.priority || 0))
const fontsToPreload: FontFaceData[] = []
if (topPriorityFont && options.shouldPreload(fontFamily, topPriorityFont)) {
const fontToPreload = topPriorityFont.src.find((s): s is RemoteFontSource => 'url' in s)?.url
fontsToPreload.push(topPriorityFont)
}
if (options.filterFontsToPreload) {
fontsToPreload.push(...options.filterFontsToPreload(fontFamily, result.fonts))
}
for (const font of fontsToPreload) {
const fontToPreload = font.src.find((s): s is RemoteFontSource => 'url' in s)?.url
if (fontToPreload) {
const urls = options.fontsToPreload.get(id) || new Set()
options.fontsToPreload.set(id, urls.add(fontToPreload))
Expand Down
14 changes: 12 additions & 2 deletions packages/fontless/src/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,19 @@ export function fontless(_options?: FontlessOptions): Plugin {

cssTransformOptions = {
processCSSVariables: options.processCSSVariables,
shouldPreload(fontFamily, _fontFace) {
shouldPreload: () => false,
filterFontsToPreload(fontFamily, fonts) {
const override = options.families?.find(f => f.name === fontFamily)
return override?.preload ?? options.defaults?.preload ?? false
const preload = override?.preload ?? options.defaults?.preload
// pick by priority
if (preload === true) {
return fonts.sort((a, b) => (a.meta?.priority || 0) - (b.meta?.priority || 0)).slice(0, 1)
}
// filter by subset
if (preload) {
return fonts.filter(f => f.meta?.subset && preload.subsets.includes(f.meta.subset))
}
return []
},
fontsToPreload: new Map(),
dev: config.mode === 'development',
Expand Down
2 changes: 2 additions & 0 deletions packages/fontless/test/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ describe.each(fixtures)('e2e %s', (fixture) => {
const woff2 = content.indexOf('format(woff2)')
expect(woff >= 0 && woff2 >= 0).toBe(true)
expect(woff).lessThan(woff2)
const html = files.find(file => file.endsWith('.html'))!
expect(await readFile(join(outputDir!, html), 'utf-8')).toContain('rel="preload"')
}
}

Expand Down
21 changes: 7 additions & 14 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading