Skip to content

Commit

Permalink
Google fonts single request (#42406)
Browse files Browse the repository at this point in the history
Make a single request when using several weights and/or styles for
google fonts instead of one for each variation.

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the
feature request has been accepted for implementation before opening a
PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm build && pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing
doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
  • Loading branch information
Hannes Bornö committed Nov 3, 2022
1 parent 6edeb9d commit 8fa78a5
Show file tree
Hide file tree
Showing 7 changed files with 501 additions and 121 deletions.
34 changes: 14 additions & 20 deletions packages/font/src/google/loader.ts
Expand Up @@ -49,27 +49,21 @@ const downloadGoogleFonts: FontLoader = async ({
)
}

let fontFaceDeclarations = ''
for (const weight of weights) {
for (const style of styles) {
const fontAxes = getFontAxes(
fontFamily,
weight,
style,
selectedVariableAxes
)
const url = getUrl(fontFamily, fontAxes, display)
const fontAxes = getFontAxes(
fontFamily,
weights,
styles,
selectedVariableAxes
)
const url = getUrl(fontFamily, fontAxes, display)

let cachedCssRequest = cssCache.get(url)
const fontFaceDeclaration =
cachedCssRequest ?? (await fetchCSSFromGoogleFonts(url, fontFamily))
if (!cachedCssRequest) {
cssCache.set(url, fontFaceDeclaration)
} else {
cssCache.delete(url)
}
fontFaceDeclarations += `${fontFaceDeclaration}\n`
}
let cachedCssRequest = cssCache.get(url)
const fontFaceDeclarations =
cachedCssRequest ?? (await fetchCSSFromGoogleFonts(url, fontFamily))
if (!cachedCssRequest) {
cssCache.set(url, fontFaceDeclarations)
} else {
cssCache.delete(url)
}

// Find font files to download
Expand Down
90 changes: 66 additions & 24 deletions packages/font/src/google/utils.ts
Expand Up @@ -126,25 +126,50 @@ export function validateData(functionName: string, data: any): FontOptions {

export function getUrl(
fontFamily: string,
axes: [string, string][],
axes: {
wght: string[]
ital: string[]
variableAxes?: [string, string][]
},
display: string
) {
// Variants are all combinations of weight and style, each variant will result in a separate font file
const variants: Array<[string, string][]> = []
for (const wgth of axes.wght) {
if (axes.ital.length === 0) {
variants.push([['wght', wgth], ...(axes.variableAxes ?? [])])
} else {
for (const ital of axes.ital) {
variants.push([
['ital', ital],
['wght', wgth],
...(axes.variableAxes ?? []),
])
}
}
}

// Google api requires the axes to be sorted, starting with lowercase words
axes.sort(([a], [b]) => {
const aIsLowercase = a.charCodeAt(0) > 96
const bIsLowercase = b.charCodeAt(0) > 96
if (aIsLowercase && !bIsLowercase) return -1
if (bIsLowercase && !aIsLowercase) return 1
if (axes.variableAxes) {
variants.forEach((variant) => {
variant.sort(([a], [b]) => {
const aIsLowercase = a.charCodeAt(0) > 96
const bIsLowercase = b.charCodeAt(0) > 96
if (aIsLowercase && !bIsLowercase) return -1
if (bIsLowercase && !aIsLowercase) return 1

return a > b ? 1 : -1
})
return a > b ? 1 : -1
})
})
}

return `https://fonts.googleapis.com/css2?family=${fontFamily.replace(
/ /g,
'+'
)}:${axes.map(([key]) => key).join(',')}@${axes
.map(([, val]) => val)
.join(',')}&display=${display}`
)}:${variants[0].map(([key]) => key).join(',')}@${variants
.map((variant) => variant.map(([, val]) => val).join(','))
.sort()
.join(';')}&display=${display}`
}

export async function fetchCSSFromGoogleFonts(url: string, fontFamily: string) {
Expand Down Expand Up @@ -192,17 +217,23 @@ export async function fetchFontFile(url: string) {

export function getFontAxes(
fontFamily: string,
weight: string,
style: string,
weights: string[],
styles: string[],
selectedVariableAxes?: string[]
): [string, string][] {
): {
wght: string[]
ital: string[]
variableAxes?: [string, string][]
} {
const allAxes: Array<{ tag: string; min: number; max: number }> = (
fontData as any
)[fontFamily].axes
const italicAxis: [string, string][] =
style === 'italic' ? [['ital', '1']] : []
const hasItalic = styles.includes('italic')
const hasNormal = styles.includes('normal')
const ital = hasItalic ? [...(hasNormal ? ['0'] : []), '1'] : []

if (weight === 'variable') {
// Weights will always contain one element if it's a variable font
if (weights[0] === 'variable') {
if (selectedVariableAxes) {
const defineAbleAxes: string[] = allAxes
.map(({ tag }) => tag)
Expand All @@ -228,14 +259,25 @@ export function getFontAxes(
})
}

const variableAxes: [string, string][] = allAxes
.filter(
({ tag }) => tag === 'wght' || selectedVariableAxes?.includes(tag)
)
.map(({ tag, min, max }) => [tag, `${min}..${max}`])
let weightAxis: string
const variableAxes: [string, string][] = []
for (const { tag, min, max } of allAxes) {
if (tag === 'wght') {
weightAxis = `${min}..${max}`
} else if (selectedVariableAxes?.includes(tag)) {
variableAxes.push([tag, `${min}..${max}`])
}
}

return [...italicAxis, ...variableAxes]
return {
wght: [weightAxis!],
ital,
variableAxes,
}
} else {
return [...italicAxis, ['wght', weight]]
return {
ital,
wght: weights,
}
}
}
21 changes: 18 additions & 3 deletions test/e2e/next-font/app/pages/with-google-fonts.js
@@ -1,7 +1,16 @@
import { Fraunces, Indie_Flower } from '@next/font/google'
import { Fraunces, Indie_Flower, Roboto } from '@next/font/google'

const indieFlower = Indie_Flower({ weight: '400' })
const fraunces = Fraunces({ weight: '400' })
const indieFlower = Indie_Flower({ weight: '400', preload: false })
const fraunces = Fraunces({ weight: '400', preload: false })

const robotoMultiple = Roboto({
weight: ['900', '100'],
style: ['normal', 'italic'],
})
const frauncesMultiple = Fraunces({
style: ['italic', 'normal'],
axes: ['SOFT', 'WONK', 'opsz'],
})

export default function WithFonts() {
return (
Expand All @@ -12,6 +21,12 @@ export default function WithFonts() {
<div id="second-google-font" className={fraunces.className}>
{JSON.stringify(fraunces)}
</div>
<div id="multiple-roboto" className={robotoMultiple.className}>
{JSON.stringify(robotoMultiple)}
</div>
<div id="multiple-fraunces" className={frauncesMultiple.className}>
{JSON.stringify(frauncesMultiple)}
</div>
</>
)
}
31 changes: 31 additions & 0 deletions test/e2e/next-font/app/pages/with-local-fonts.js
Expand Up @@ -99,6 +99,31 @@ const robotoVar2 = localFont({
],
})

const robotoWithPreload = localFont({
src: [
{
path: '../fonts/roboto/roboto-100.woff2',
weight: '100',
style: 'normal',
},
{
path: '../fonts/roboto/roboto-900-italic.woff2',
weight: '900',
style: 'italic',
},
{
path: '../fonts/roboto/roboto-100.woff2',
weight: '100',
style: 'normal',
},
{
path: '../fonts/roboto/roboto-100-italic.woff2',
weight: '900',
style: 'italic',
},
],
})

export default function WithFonts() {
return (
<>
Expand All @@ -117,6 +142,12 @@ export default function WithFonts() {
<div id="roboto-local-font-var2" className={robotoVar2.className}>
{JSON.stringify(robotoVar2)}
</div>
<div
id="roboto-local-font-preload"
className={robotoWithPreload.className}
>
{JSON.stringify(robotoWithPreload)}
</div>
</>
)
}

0 comments on commit 8fa78a5

Please sign in to comment.