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
20 changes: 20 additions & 0 deletions .changeset/tricky-books-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'astro': patch
---

Adds the option to specify in the `preload` directive which weights, styles, or subsets to preload for a given font family when using the experimental Fonts API:

```astro
---
import { Font } from 'astro:assets';
---
<Font
cssVariable="--font-roboto"
preload={[
{ subset: 'latin', style: 'normal' },
{ weight: '400' },
]}
/>
```

Variable weight font files will be preloaded if any weight within its range is requested. For example, a font file for font weight `100 900` will be included when `400` is specified in a `preload` object.
12 changes: 7 additions & 5 deletions packages/astro/components/Font.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
import * as mod from 'virtual:astro:assets/fonts/internal';
import { filterPreloads } from 'astro/assets/fonts/runtime';
import { AstroError, AstroErrorData } from '../dist/core/errors/index.js';

// TODO: remove check when fonts are stabilized
Expand All @@ -12,7 +13,7 @@ interface Props {
/** The `cssVariable` registered in your Astro configuration. */
cssVariable: import('astro:assets').CssVariable;
/** Whether it should output [preload links](https://web.dev/learn/performance/optimize-web-fonts#preload) or not. */
preload?: boolean;
preload?: import('astro:assets').FontPreloadFilter;
}

const { cssVariable, preload = false } = Astro.props as Props;
Expand All @@ -23,12 +24,13 @@ if (!data) {
message: AstroErrorData.FontFamilyNotFound.message(cssVariable),
});
}

const filteredPreloadData = filterPreloads(data.preloadData, preload);
---

<style set:html={data.css}></style>
{
preload &&
data.preloadData.map(({ url, type }) => (
<link rel="preload" href={url} as="font" type={`font/${type}`} crossorigin />
))
filteredPreloadData?.map(({ url, type }) => (
<link rel="preload" href={url} as="font" type={`font/${type}`} crossorigin />
))
}
11 changes: 10 additions & 1 deletion packages/astro/src/assets/fonts/implementations/url-proxy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { DataCollector, UrlProxy, UrlProxyHashResolver, UrlResolver } from '../definitions.js';
import { renderFontWeight } from '../utils.js';

export function createUrlProxy({
hashResolver,
Expand All @@ -19,7 +20,15 @@ export function createUrlProxy({
dataCollector.collect({
url: originalUrl,
hash,
preload: collectPreload ? { url, type } : null,
preload: collectPreload
? {
url,
type,
weight: renderFontWeight(data.weight),
style: data.style,
subset: data.subset
}
: null,
data,
init,
});
Expand Down
49 changes: 48 additions & 1 deletion packages/astro/src/assets/fonts/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { AstroError, AstroErrorData } from '../../core/errors/index.js';
import type { ConsumableMap } from './types.js';
import type { ConsumableMap, PreloadData, PreloadFilter } from './types.js';

export function createGetFontData({ consumableMap }: { consumableMap?: ConsumableMap }) {
return function getFontData(cssVariable: string) {
// TODO: remove once fonts are stabilized
if (!consumableMap) {
throw new AstroError(AstroErrorData.ExperimentalFontsNotEnabled);
}
Expand All @@ -16,3 +17,49 @@ export function createGetFontData({ consumableMap }: { consumableMap?: Consumabl
return data;
};
}

export function filterPreloads(
data: Array<PreloadData>,
preload: PreloadFilter,
): Array<PreloadData> | null {
if (!preload) {
return null;
}
if (preload === true) {
// Preload everything
return data;
}
// Only preload urls based on weight, style and subset
return data.filter(({ weight, style, subset }) =>
preload.some((p) => {
// Always check the weight
if (p.weight !== undefined && weight !== undefined && !checkWeight(p.weight.toString(), weight)) {
return false;
}
// Only check the style if specified
if (p.style !== undefined && p.style !== style) {
return false;
}
// Only check the subset if specified
if (p.subset !== undefined && p.subset !== subset) {
return false;
}
return true;
}),
);
}

function checkWeight(input: string, target: string): boolean {
// If the input looks like "100 900", we check it as is
const trimmedInput = input.trim();
if (trimmedInput.includes(' ')) {
return trimmedInput === target;
}
// If the target looks like "100 900", we check if the input is between the values
if (target.includes(' ')) {
const [a, b] = target.split(' ');
const parsedInput = Number.parseInt(input);
return parsedInput >= Number.parseInt(a) && parsedInput <= Number.parseInt(b);
}
return input === target;
}
7 changes: 7 additions & 0 deletions packages/astro/src/assets/fonts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ export interface PreloadData {
* A font type, eg. woff2, woff, ttf...
*/
type: FontType;
weight: string | undefined;
style: string | undefined;
subset: string | undefined;
}

export type FontFaceMetrics = Pick<
Expand Down Expand Up @@ -116,3 +119,7 @@ export interface FontData {
export type ConsumableMap = Map<string, Array<FontData>>;

export type Style = z.output<typeof styleSchema>;

export type PreloadFilter =
| boolean
| Array<{ weight?: string | number; style?: string; subset?: string }>;
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
import { getFontData } from 'astro:assets'
---

<pre id="data">{JSON.stringify(getFontData('--font-roboto'), null, 2)}</pre>
<pre id="data">{JSON.stringify(getFontData('--font-test'), null, 2)}</pre>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
import { Font } from 'astro:assets'
---

<Font cssVariable='--font-test' preload={[{ weight: 400 }]} />
2 changes: 1 addition & 1 deletion packages/astro/test/fixtures/fonts/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Font } from 'astro:assets'

<html lang="en">
<head>
<Font cssVariable='--font-roboto' />
<Font cssVariable='--font-test' />
</head>
<body>
</body>
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/test/fixtures/fonts/src/pages/preload.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Font } from 'astro:assets'

<html lang="en">
<head>
<Font cssVariable='--font-roboto' preload />
<Font cssVariable='--font-test' preload />
</head>
<body>
</body>
Expand Down
96 changes: 78 additions & 18 deletions packages/astro/test/fonts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ describe('astro fonts', () => {
experimental: {
fonts: [
{
name: 'Roboto',
cssVariable: '--font-roboto',
name: 'Poppins',
cssVariable: '--font-test',
provider: fontProviders.fontsource(),
weights: [400, 500],
},
],
},
Expand All @@ -65,9 +66,10 @@ describe('astro fonts', () => {
experimental: {
fonts: [
{
name: 'Roboto',
cssVariable: '--font-roboto',
name: 'Poppins',
cssVariable: '--font-test',
provider: fontProviders.fontsource(),
weights: [400, 500],
},
],
},
Expand All @@ -81,14 +83,43 @@ describe('astro fonts', () => {
});
});

it('Can filter preloads', async () => {
const { fixture, run } = await createDevFixture({
experimental: {
fonts: [
{
name: 'Poppins',
cssVariable: '--font-test',
provider: fontProviders.fontsource(),
weights: [400, 500],
},
],
},
});
await run(async () => {
let res = await fixture.fetch('/preload');
let html = await res.text();
let $ = cheerio.load(html);
const allPreloads = $('link[rel=preload][type=font/woff2]');

res = await fixture.fetch('/granular-preload');
html = await res.text();
$ = cheerio.load(html);
const filteredPreloads = $('link[rel=preload][type=font/woff2]');

assert.equal(filteredPreloads.length < allPreloads.length, true);
});
});

it('Has correct headers in dev', async () => {
const { fixture, run } = await createDevFixture({
experimental: {
fonts: [
{
name: 'Roboto',
cssVariable: '--font-roboto',
name: 'Poppins',
cssVariable: '--font-test',
provider: fontProviders.fontsource(),
weights: [400, 500],
},
],
},
Expand Down Expand Up @@ -126,9 +157,10 @@ describe('astro fonts', () => {
experimental: {
fonts: [
{
name: 'Roboto',
cssVariable: '--font-roboto',
name: 'Poppins',
cssVariable: '--font-test',
provider: fontProviders.fontsource(),
weights: [400, 500],
},
],
},
Expand All @@ -147,8 +179,8 @@ describe('astro fonts', () => {
experimental: {
fonts: [
{
name: 'Roboto',
cssVariable: '--font-roboto',
name: 'Poppins',
cssVariable: '--font-test',
provider: fontProviders.fontsource(),
},
],
Expand Down Expand Up @@ -176,9 +208,10 @@ describe('astro fonts', () => {
experimental: {
fonts: [
{
name: 'Roboto',
cssVariable: '--font-roboto',
name: 'Poppins',
cssVariable: '--font-test',
provider: fontProviders.fontsource(),
weights: [400, 500],
},
],
},
Expand All @@ -194,9 +227,10 @@ describe('astro fonts', () => {
experimental: {
fonts: [
{
name: 'Roboto',
cssVariable: '--font-roboto',
name: 'Poppins',
cssVariable: '--font-test',
provider: fontProviders.fontsource(),
weights: [400, 500],
},
],
},
Expand All @@ -207,6 +241,31 @@ describe('astro fonts', () => {
assert.equal(href?.startsWith('/_astro/fonts/'), true);
});

it('Can filter preloads', async () => {
const { fixture } = await createBuildFixture({
experimental: {
fonts: [
{
name: 'Poppins',
cssVariable: '--font-test',
provider: fontProviders.fontsource(),
weights: [400, 500],
},
],
},
});

let html = await fixture.readFile('/preload/index.html');
let $ = cheerio.load(html);
const allPreloads = $('link[rel=preload][type=font/woff2]');

html = await fixture.readFile('/granular-preload/index.html');
$ = cheerio.load(html);
const filteredPreloads = $('link[rel=preload][type=font/woff2]');

assert.equal(filteredPreloads.length < allPreloads.length, true);
});

it('Respects config to build links', async () => {
const { fixture } = await createBuildFixture({
base: '/my-base',
Expand All @@ -217,9 +276,10 @@ describe('astro fonts', () => {
experimental: {
fonts: [
{
name: 'Roboto',
cssVariable: '--font-roboto',
name: 'Poppins',
cssVariable: '--font-test',
provider: fontProviders.fontsource(),
weights: [400, 500],
},
],
},
Expand All @@ -237,8 +297,8 @@ describe('astro fonts', () => {
experimental: {
fonts: [
{
name: 'Roboto',
cssVariable: '--font-roboto',
name: 'Poppins',
cssVariable: '--font-test',
provider: fontProviders.fontsource(),
},
],
Expand Down
18 changes: 16 additions & 2 deletions packages/astro/test/units/assets/fonts/implementations.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,13 @@ describe('fonts implementations', () => {
dataCollector.collect({
hash: 'yyy',
url: 'def',
preload: { type: 'woff2', url: 'def' },
preload: {
type: 'woff2',
url: 'def',
weight: undefined,
style: 'normal',
subset: undefined,
},
data: {
weight: undefined,
style: undefined,
Expand All @@ -124,7 +130,15 @@ describe('fonts implementations', () => {
['yyy', { url: 'def', init: null }],
],
);
assert.deepStrictEqual(preloadData, [{ type: 'woff2', url: 'def' }]);
assert.deepStrictEqual(preloadData, [
{
type: 'woff2',
url: 'def',
weight: undefined,
style: 'normal',
subset: undefined,
},
]);
assert.deepStrictEqual(collectedFonts, [
{
hash: 'xxx',
Expand Down
Loading
Loading