Fonts
Summary
Have first-party support for fonts in Astro:
// astro config
export default defineConfig({
fonts: {
families: ["Roboto", "Lato"]
}
})
---
// layouts/Layout.astro
import { Font } from "astro:fonts"
---
<head>
<Font family="Inter" preload />
<Font family="Lato" />
<style>
h1 {
font-family: var(--astro-font-inter);
}
p {
font-family: var(--astro-font-lato);
}
</style>
</head>
Links
How does preloading pick the src when the font provides multiple files for multiple unicode ranges? See IBM Plex Mono on font source for example.
What's the reasoning behind considering subsetting a non-goal? It helps a lot when the font participates in LCP and is trivial to do when using google fonts directly. Moving to astro fonts in that case would be a downgrade.
How does preloading pick the
srcwhen the font provides multiple files for multiple unicode ranges? See IBM Plex Mono on font source for example.
I think we can't choose the src to pick automatically, so that would preload everything? Not sure, have ideas in mind?
What's the reasoning behind considering subsetting a non-goal? It helps a lot when the font participates in LCP and is trivial to do when using google fonts directly. Moving to astro fonts in that case would be a downgrade.
Only automatic subsetting is a non goal (eg. by analyzing the static content), subsetting is actually supported. I'll clarify the non goal
Feedback
First, I’m glad to see font optimizations coming to Astro! That will really help with layout shifts. This feedback is meant to improve an already great start on the implementation.
I recently implemented my own version of it, so I have a few suggestions that could improve the Fonts API, based on my own experiences.
Remove @capsizecss/metrics from the default bundle
It’s massive—22.5 MB of lots of small files. It takes forever to install if you don’t have a fast machine, fast internet, or want to spin up a fast playground. Please don’t make it part of the default Astro installation. By optimizing for fonts, we’ve significantly de-optimized the Astro installation for everyone who doesn’t use it.
Solution 1
Instead, generate metrics on the fly from the font using import { fromFile } from '@capsizecss/unpack';
Copy over any fallback metrics you need from @capsizecss/metrics like Times New Roman, Arial, and Courier New, before publishing the fonts package.
Solution 2
Move the Fonts API to its own package. Yes, it’s large, but now it’s opt-in, not installed by default without an opt-out.
Fix the issues in astro-capo
What does this have to do with fonts? Well, ordering the tags in the <head /> is important. For example, the order of preload links and inline styles is reversed in the Font component.
When combining multiple tags into a single component to render in the <head />, it become impossible to manually render them in the correct order. And without fixing the issues in astro-capo, it may be impossible to fix the order of tags in <head /> in user land.
Improve fallback weights and italics
Add the bold and italic versions of fallback fonts to the fallback css. For an example, see this blog post.
It’s particularly great for headline fallbacks and in cases where fonts never load.
Avoids font synthesis and works when users disable it font-synthesis: none; too.
Thanks for the feedback!
- Remove
@capsizecss/metrics: definitely agree! - Head ordering: I can swap the order but as soon as you use several times the
Fontcomponent, you'll have to getastro-capofixed I think - Fallbacks for weights/italics: sure!
Glad to hear! I think the Fonts API will be very helpful for most Astro use cases. Great work on the implementation.
you'll have to get astro-capo fixed I think
I’ve looked into fixing it before, but I wasn’t familiar enough with it to contribute.
Personally, I think it’s such an important feature that it should be added to core as a built-in component, but that’s a different discussion.
Other built-in Astro components like the View transitions <ClientRouter /> would benefit from it too.
I've sent a few PRs already to address your feedback
https://github.com/withastro/astro/pull/13626 At first glance, it wasn’t immediately obvious to me where you load metrics for fallback fonts like Times New Roman, Arial, and Courier New, which you may not be able to read the raw font file from any of the configured font providers (e.g. Google Fonts, as those are commercial fonts). But, you probably have that covered 🚀
https://github.com/withastro/astro/pull/13625 nice, simple fix for head ordering 👍
We don't read metrics for system fonts
When reading the docs:
If the last font in the fallbacks array is a generic family name, an optimized fallback using font metrics will be generated. To disable this optimization, set optimizedFallbacks to false.
I was under the impression that the optimized font family overrides (ascent-override, descent-override, line-gap-override, size-adjust) are generated when I use generic family names, which are mapped to system fonts here.
That way you end up with something like:
/*
font-family:
Open Sans,
"Open Sans Fallback: Arial",
sans-serif;
*/
@font-face {
ascent-override: 101.6519%;
descent-override: 27.8625%;
font-family: "Open Sans Fallback: Arial";
font-stretch: 100%;
font-style: normal;
font-weight: 400;
line-gap-override: 0%;
size-adjust: 105.1479%;
src: local("Arial"), local("ArialMT");
}
when I use the font family Open Sans and set the last fallback to sans-serif.
To get metrics for system fonts, you can hard-code, copy, or fetch only the metrics files for the system fonts @capsizecss/metrics:
-
@capsizecss/metrics/entireMetricsCollection/arial/regular/index.mjs -
@capsizecss/metrics/entireMetricsCollection/timesNewRoman/regular/index.mjs -
@capsizecss/metrics/entireMetricsCollection/courierNew/regular/index.mjs
Rather than installing the entire package with the metrics for all fonts.
Optimized font family overrides for system fonts would have the most impact since they are local and are already installed and are rendered immediately.
Hello hello!
Just trying out the feature, and I noticed that for remote fonts, for specifying a range of weights you use the weights key and it accepts an array with a string like ["100 900"]
But for local fonts the key is called weight and it just accepts the string "100 900" with no array
Unless there is some super complex reason for this, I feel like they should be consistent
references https://docs.astro.build/en/reference/experimental-flags/fonts/#weights https://docs.astro.build/en/reference/experimental-flags/fonts/#weight
Also having some issues getting local fonts to work at all
Minimal reproduction https://github.com/JusticeMatthew/astro-font-test
Unless there is some super complex reason for this, I feel like they should be consistent
This is on purpose. It used to be the same but it was more confusing because they actually work differently:
- When using a remote provider, providing an array is fine because it's up to the provider to request the font files
- When using the local provider, 1 variant = 1 font face declaration. And a font face declaration can only have 1 weight associated
I'll look into your issue today
Optimized font family overrides for system fonts would have the most impact since they are local and are already installed and are rendered immediately.
@jlarmstrongiv I don't think I understand. If a system font is being passed manually (eg. name Arial and fallbacks ['sans-serif']) I don't think we should generate optimized fallbacks because those fonts are already available on the system. However, I need to check if we handle this kind of case currently
EDIT: I think I see what you mean now
- When using the local provider, 1 variant = 1 font face declaration. And a font face declaration can only have 1 weight associated
But the weight can be a string of multiple weights? Or is that being handled under the hood by adding the font face declarations for each one?
The weight can be a weight (400 or "400") or a weight range ("100 900") but never several weights
@florian-lefebvre I’m more referring to how these packages work:
- https://nextjs.org/docs/pages/api-reference/components/font#adjustfontfallback
- https://github.com/unjs/fontaine?tab=readme-ov-file#usage
Often times, they infer a local system fallback font family (Arial, Times New Roman, or Courier New) based on your final css generic fallback family (sans-serif, serif, mono) and generate metrics for loading that local system fallback font family (renamed Open Sans Fallback: Arial with css metrics overrides).
That way, you may only specify Open Sans, sans-serif and the plugins will take care of the rest—generating a local system fallback font family with metric overrides.
Next.js focuses on fonts that are guaranteed to be installed on all machines. Specifying Inter as a local fallback and generating overrides is certainly possible, but it’s only effective on computers that already have Inter installed, which is the minority. Instead, optimizing local system fallback font families should be default because they are already installed, display instantly, and are supported by the majority of devices.
It can be confusing! Let me know if that makes sense.
I think I understand correctly and that's what we currently have too! There are improvements in https://github.com/withastro/astro/pull/13635, can you try the preview release?
Feature Request - Ability to get font hash in js/ts file.
Use Case:
import type { APIRoute } from "astro";
import { URL as SITE_URL } from "@/site.config";
import raw from "./_theme.css?raw";
const css = raw.replace(
/__DEPARTURE_MONO_SRC__/g,
new URL("giscus/fonts/departure-mono-regular.woff2", SITE_URL).href, // this needs to be in sync with astro font api
);
export const GET: APIRoute = () =>
new Response(css, {
headers: { "Content-Type": "text/css" },
});
Or a more generalized solution is to provide a way to customize font output location instead of default _astro/fonts/{HASH}.{FORMAT}
You're showing code there but what is your actual usecase?
Or a more generalized solution is to provide a way to customize font output location instead of default
You can customize build.assets
You're showing code there but what is your actual usecase?
Code is the use case, there is not way to make this work without extra penalty of duplicating font in public dir
- I am sure CDN's don't like this as well (Cloudflare in my case, internally figures out that the file has same name, same size and other attributes it send 304: NOT MODIFIED). Now it will have to serve new asset every build, due to name change.
For some application renaming files could be useful and called as
Cache Busting
You can customize build.assets
My idea was to output the font to a deterministic location not changing the entire build output dir.
fonts: [
{
provider: "local",
id: "xyz", // or similar deterministic value
name: "XYZ",
cssVariable: "--font-xyz",
variants: [
{
weight: 400,
style: "normal",
src: [`xyz.otf`],
},
],
},
]
Should output to ${ASTRO_BUILD_DIR}/fonts/${font.id}.${font.extension} This allows for:
A) Manual JS Import
import { URL as SITE_URL } from "@/site.config";
const fontUrl = new URL("_astro/fonts/xyz.otf", SITE_URL).href
B) Manual CSS Import
@font-face {
font-family: "NotXYZ";
src: url("/_astro/fonts/xyz.otf") format("opentype");
font-display: swap;
font-weight: 400;
font-style: normal;
}
Another valid query- What if i put my font in public dir, should astro duplicate it inside _astro/font ??
- If not, then this also solves the problem of deterministic naming
- If yes, then what's the benefit of duplication ?
Code is the use case
"Code" is not a use case at all, like what problem are you trying to solve? From what you say it seems your concern is about caching right?
The filename is hashed so:
- It should not change if you didn't change your font settings
- It should be cached between builds (because it's stored in node modules)
- It should be cached on the client for users, as long as the cloudflare adapters is setting proper headers for any assets under the cache dir
Oh and I just saw the end of your message about duplicating fonts in public and in _astro/fonts. Because of the processing we do, the fonts file you reference should not be in public because they will indeed be duplicated. I'll work on a code + docs warning
@florian-lefebvre, In the case you want to listen to entire use case .. here it is:
- I am using
giscusto support comments and reaction in my astro site - To customize it you need to provide it with a custom css file lets say
theme.css - Why a separate css file, giscus renders inside an iframe so it couldn't communicate with the css from my astro site
- To make the giscus theme and site theme look similar i want to use the same font
- To use the same font, we need a fixed url to use inside of the font-face definition
and here we arrive again at the same code ... which is not the use case itself :(
pages/giscus/theme.css.ts
import type { APIRoute } from "astro";
import { URL as SITE_URL } from "@/site.config";
import raw from "./_theme.css?raw";
const font = "ASTRO_HASH" // 💀 PROBLEM HERE
const css = raw.replace(
/__DEPARTURE_MONO_SRC__/g,
new URL(`_astro/fonts/${font}.woff2`, SITE_URL).href,
);
export const GET: APIRoute = () =>
new Response(css, {
headers: { "Content-Type": "text/css" },
});
pages/giscus/_theme.css
/* other stuff */
@font-face {
font-family: "Departure Mono";
src: url("__DEPARTURE_MONO_SRC__") format("woff2");
font-weight: normal;
font-style: normal;
font-display: swap;
}
/* other stuff */
EDIT: Using this dirty trick for now (mimics internal hashing)
import { readFileSync } from "fs";
import { extname, resolve } from "path";
import xxhash from "xxhash-wasm";
const { h64ToString } = await xxhash();
const ASSETS_DIR = "_astro";
export function getHashedFontPath(file: string): string {
const path = resolve(file);
const content = readFileSync(path);
return ASSETS_DIR + "/fonts/" + h64ToString(path + content) + extname(file);
}
Thanks that helps a lot to have the details! I'm okay with adding an API to get the font url. I'll need to think about its shape because for a given family you can have several font files etc
I would really like to be able to pick only specific variants of my local font to preload. At the moment, my app preloads ALL font files, even those of legacy formats like woff, or fonts that are rarely used in my app.
Maybe, when specifying the src array, allow setting preload in the object, and get rid of the preload property on the <Font> component (or rename the property to disablePreload):
import { defineConfig } from "astro/config";
export default defineConfig({
experimental: {
fonts: [{
provider: "local",
name: "Custom",
cssVariable: "--font-custom",
variants: [
{
weight: 300,
style: "italic",
src: [
// Rarely used, so don't preload at all
{ url: "./src/assets/fonts/MyFont-light-italic.woff2" },
{ url: "./src/assets/fonts/MyFont-light-italic.woff" }
]
},
{
weight: 400,
style: "normal",
src: [
{ url: "./src/assets/fonts/MyFont-regular.woff2", preload: true },
{ url: "./src/assets/fonts/MyFont-regular.woff" }
]
},
{
weight: 700,
style: "normal",
src: [
{ url: "./src/assets/fonts/MyFont-bold.woff2", preload: true },
{ url: "./src/assets/fonts/MyFont-bold.woff" }
]
}
// ...
]
}]
}
});
---
import { Font } from 'astro:assets';
---
<Font cssVariable="--font-custom" />
Which would only render the following:
<!-- Ignoring the generated file hashes here... -->
<link rel="preload" href="/_astro/fonts/MyFont-regular.woff2" as="font" type="font/woff2" crossorigin="">
<link rel="preload" href="/_astro/fonts/MyFont-bold.woff2" as="font" type="font/woff2" crossorigin="">
Instead of:
<!-- Ignoring the generated file hashes here... -->
<link rel="preload" href="/_astro/fonts/MyFont-light-italic.woff2" as="font" type="font/woff2" crossorigin="">
<link rel="preload" href="/_astro/fonts/MyFont-light-italic.woff" as="font" type="font/woff" crossorigin="">
<link rel="preload" href="/_astro/fonts/MyFont-regular.woff2" as="font" type="font/woff2" crossorigin="">
<link rel="preload" href="/_astro/fonts/MyFont-regular.woff" as="font" type="font/woff" crossorigin="">
<link rel="preload" href="/_astro/fonts/MyFont-bold.woff2" as="font" type="font/woff2" crossorigin="">
<link rel="preload" href="/_astro/fonts/MyFont-bold.woff" as="font" type="font/woff" crossorigin="">
This is only my personal suggestion - the exact implementation and API is obviously open for discussion.
Thanks for the feedback!
my app preloads ALL font files, even those of legacy formats like woff
This should not be the case. It was fixed recently, did you try with the latest version of Astro?
or fonts that are rarely used in my app.
That's a good point. I quite like what you're suggesting but that would only work for the local provider. I wonder if there's a way to support it for remote providers, or to move it somewhere else? eg.
---
import { Font } from 'astro:assets';
---
<Font cssVariable="--font-custom" preload={[400,500]} />
It's less centralized but it works for all kinds of providers
--- import { Font } from 'astro:assets'; --- <Font cssVariable="--font-custom" preload={[400,500]} />
Big fan of this
@florian-lefebvre wrote: This should not be the case. It was fixed recently, did you try with the latest version of Astro?
Yeah, I am on [email protected]. I went through the source code to look for the fix you mentioned and why it's not working for me - looks like the fix was only implemented for the non-local providers.
As you can see this code block is in the family.provider !== LOCAL_PROVIDER_NAME branch:
https://github.com/withastro/astro/blob/6ed83606f9bdfdf0a35aef1318cf0e2cb4b4236e/packages/astro/src/assets/fonts/load.ts#L151-L163
But there is no equivalent logic for the family.provider === LOCAL_PROVIDER_NAME branch - the collectPreload argument for collect() is always true:
https://github.com/withastro/astro/blob/6ed83606f9bdfdf0a35aef1318cf0e2cb4b4236e/packages/astro/src/assets/fonts/load.ts#L107
@florian-lefebvre wrote:
--- import { Font } from 'astro:assets'; --- <Font cssVariable="--font-custom" preload={[400,500]} />
This looks like a pretty good solution! The only question is if it should be differentiated between different font-style like normal, italic and oblique.
Maybe allow either a number | string (just the weight) or an object like { weight: number | string, style: 'normal' | 'italic' | 'oblique' } with the weight and style in the preload property array. 🤔
So, together, the type for the preload property could be:
type FontProps = {
// ...
preload: (number | string | { weight: number | string, style: 'normal' | 'italic' | 'oblique' })[]
// ...
};
Which, once implemented, would make it pretty flexible:
---
import { Font } from 'astro:assets';
---
<Font cssVariable="--font-custom" preload={[ 400, { weight: 500, style: 'normal' } ]} />
Just my suggestion!
looks like the fix was only implemented for the non-local providers.
You're right! I have a fix on another branch so that should land soon-ish
Hello, Just took the new fonts feature for a spin, and it's looking fantastic overall!
I did run into an issue when trying to use fonts from Fontshare. There's an issue with the URL for the Fontshare CDN (it looks malformed in the error message), as it's consistently throwing an error when trying to load a font. You can see the error in the screenshot below:
Minimal reproduction: https://congenial-couscous-4jpj9qg4gp42jv6v.github.dev/