diff --git a/README.md b/README.md
index c810a28..80a7aab 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ Plug-and-play custom web font optimization and configuration for Nuxt apps.
## Features
- ✨ zero-configuration required
-- 🔡 built-in providers (`google`, `bunny`, `fontshare`, `adobe`, `local` - more welcome!)
+- 🔡 built-in providers (`google`, `bunny`, `fontshare`, `fontsource`, `adobe`, `local` - more welcome!)
- 💪 custom providers for full control
- ⏬ local download support (until `nuxt/assets` lands)
- ⚡️ automatic font metric optimisation powered by [**fontaine**](https://github.com/unjs/fontaine) and [**capsize**](https://github.com/seek-oss/capsize)
@@ -187,6 +187,10 @@ Then, when you use a `font-family` in your CSS, we check to see whether it match
You should read [their terms in full](https://www.fontshare.com/licenses/itf-ffl) before using a font through `fontshare`.
+### `fontsource`
+
+[Fontsource](https://fontsource.org/docs/getting-started/introduction) is a collection of open-source fonts that are designed for self-hosting in web applications.
+
### `adobe`
[Adobe Fonts](https://fonts.adobe.com/) is a font service for both personal and commercial use included with Creative Cloud subscriptions.
diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts
index d650bba..abf5208 100644
--- a/playground/nuxt.config.ts
+++ b/playground/nuxt.config.ts
@@ -17,7 +17,8 @@ export default defineNuxtConfig({
{ name: 'CustomGlobal', global: true, src: '/font-global.woff2' },
{ name: 'Oswald', fallbacks: ['Times New Roman'] },
{ name: 'Aleo', provider: 'adobe'},
- { name: 'Barlow Semi Condensed', provider: 'adobe' }
+ { name: 'Barlow Semi Condensed', provider: 'adobe' },
+ { name: 'Roboto Mono', provider: 'fontsource' },
],
adobe: {
id: ['sij5ufr', 'grx7wdj'],
diff --git a/playground/pages/providers/fontsource.vue b/playground/pages/providers/fontsource.vue
new file mode 100644
index 0000000..0eac40a
--- /dev/null
+++ b/playground/pages/providers/fontsource.vue
@@ -0,0 +1,11 @@
+
+
+ fontsource
+
+
+
+
diff --git a/src/module.ts b/src/module.ts
index a5d755c..17a33eb 100644
--- a/src/module.ts
+++ b/src/module.ts
@@ -8,6 +8,7 @@ import google from './providers/google'
import bunny from './providers/bunny'
import fontshare from './providers/fontshare'
import adobe from './providers/adobe'
+import fontsource from './providers/fontsource'
import { FontFamilyInjectionPlugin, type FontFaceResolution } from './plugins/transform'
import { generateFontFace } from './css/render'
@@ -95,6 +96,7 @@ export default defineNuxtModule({
google,
bunny,
fontshare,
+ fontsource,
},
},
async setup (options, nuxt) {
diff --git a/src/providers/fontsource.ts b/src/providers/fontsource.ts
new file mode 100644
index 0000000..80ba6dc
--- /dev/null
+++ b/src/providers/fontsource.ts
@@ -0,0 +1,127 @@
+import { $fetch } from 'ofetch'
+import { hash } from 'ohash'
+
+import type { FontProvider, NormalizedFontFaceData, ResolveFontFacesOptions } from '../types'
+import { addLocalFallbacks } from '../css/parse'
+import { cachedData } from '../cache'
+import { logger } from '../logger'
+
+export default {
+ async setup () {
+ await initialiseFontMeta()
+ },
+ async resolveFontFaces (fontFamily, defaults) {
+ if (!isFontsourceFont(fontFamily)) { return }
+
+ return {
+ fonts: await cachedData(`fontsource:${fontFamily}-${hash(defaults)}-data.json`, () => getFontDetails(fontFamily, defaults), {
+ onError (err) {
+ logger.error(`Could not fetch metadata for \`${fontFamily}\` from \`fontsource\`.`, err)
+ return []
+ }
+ })
+ }
+ },
+} satisfies FontProvider
+
+/** internal */
+
+const fontAPI = $fetch.create({
+ baseURL: 'https://api.fontsource.org/v1'
+})
+
+export interface FontsourceFontMeta {
+ [key: string]: {
+ id: string
+ family: string
+ subsets: string[]
+ weights: number[]
+ styles: string[]
+ defSubset: string
+ variable: boolean
+ lastModified: string
+ category: string
+ version: string
+ type: string
+ }
+}
+
+interface FontsourceFontFile {
+ url: {
+ woff2?: string
+ woff?: string
+ ttf?: string
+ }
+}
+
+interface FontsourceFontVariant {
+ [key: string]: {
+ [key: string]: {
+ [key: string]: FontsourceFontFile
+ }
+ }
+}
+
+interface FontsourceFontDetail {
+ id: string
+ family: string
+ subsets: string[]
+ weights: number[]
+ styles: string[]
+ unicodeRange: Record
+ defSubset: string
+ variable: boolean
+ lastModified: string
+ category: string
+ version: string
+ type: string
+ variants: FontsourceFontVariant
+}
+
+let fonts: FontsourceFontMeta
+const familyMap = new Map()
+
+async function initialiseFontMeta () {
+ fonts = await cachedData('fontsource:meta.json', () => fontAPI('/fonts', { responseType: 'json' }), {
+ onError () {
+ logger.error('Could not download `fontsource` font metadata. `@nuxt/fonts` will not be able to inject `@font-face` rules for fontsource.')
+ return {}
+ }
+ })
+ for (const id in fonts) {
+ familyMap.set(fonts[id]!.family!, id)
+ }
+}
+
+function isFontsourceFont (family: string) {
+ return familyMap.has(family)
+}
+
+
+async function getFontDetails (family: string, variants: ResolveFontFacesOptions) {
+ const id = familyMap.get(family) as keyof typeof fonts
+ const font = fonts[id]!
+ const weights = variants.weights.filter(weight => font.weights.includes(Number(weight)))
+ const styles = variants.styles.filter(style => font.styles.includes(style))
+ if (weights.length === 0 || styles.length === 0) return []
+
+ const fontDetail = await fontAPI(`/fonts/${font.id}`, { responseType: 'json' })
+ const fontFaceData: NormalizedFontFaceData[] = []
+
+ // TODO: support subsets apart from default
+ const defaultSubset = fontDetail.defSubset
+
+ for (const weight of weights) {
+ for (const style of styles) {
+ const variantUrl = fontDetail.variants[weight]![style]![defaultSubset]!.url
+ fontFaceData.push({
+ style,
+ weight,
+ src: Object.entries(variantUrl).map(([format, url]) => ({ url, format })),
+ unicodeRange: fontDetail.unicodeRange[defaultSubset]?.split(',')
+ })
+ }
+ }
+
+ return addLocalFallbacks(family, fontFaceData)
+}
diff --git a/test/basic.test.ts b/test/basic.test.ts
index e52f374..cd32695 100644
--- a/test/basic.test.ts
+++ b/test/basic.test.ts
@@ -34,6 +34,16 @@ describe('providers', async () => {
`)
})
+ it('generates inlined font face rules for `fontsource` provider', async () => {
+ const html = await $fetch('/providers/fontsource')
+ expect(extractFontFaces('Roboto Mono', html)).toMatchInlineSnapshot(`
+ [
+ "@font-face{font-family:Roboto Mono;src:local("Roboto Mono"),url(/_fonts/file.woff2) format(woff2),url(/_fonts/file.woff) format(woff),url(/_fonts/file.ttf) format(ttf);font-display:swap;unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-weight:400;font-style:italic}",
+ "@font-face{font-family:Roboto Mono;src:local("Roboto Mono"),url(/_fonts/file.woff2) format(woff2),url(/_fonts/file.woff) format(woff),url(/_fonts/file.ttf) format(ttf);font-display:swap;unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-weight:400;font-style:normal}",
+ ]
+ `)
+ })
+
it('generates inlined font face rules for `google` provider', async () => {
const html = await $fetch('/providers/google')
const poppins = extractFontFaces('Poppins', html)