Skip to content
Open

Fonts #1039

Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
526ab7c
feat: work on rfc
florian-lefebvre Oct 15, 2024
c53956c
feat: copy from stage 2
florian-lefebvre Oct 15, 2024
4b67e1f
chore: raw ideas
florian-lefebvre Oct 16, 2024
5006242
feat: examples
florian-lefebvre Oct 16, 2024
28b1c1c
fix: link
florian-lefebvre Oct 18, 2024
987d13a
chore: add todos
florian-lefebvre Oct 18, 2024
c589609
feat: work on providers
florian-lefebvre Oct 22, 2024
8250880
feat: defaults and families
florian-lefebvre Oct 22, 2024
7233fbe
feat: component and usage
florian-lefebvre Oct 22, 2024
ff39e16
fix: headings
florian-lefebvre Oct 22, 2024
df5bbd5
feat
florian-lefebvre Oct 22, 2024
c79c1fa
feat: drawbacks, alternatives, adoption
florian-lefebvre Oct 22, 2024
caa649b
feat: family
florian-lefebvre Oct 22, 2024
64b9ba4
feat: tweak
florian-lefebvre Oct 22, 2024
94b5805
Apply suggestions from code review
florian-lefebvre Oct 23, 2024
15e938a
Update 0052-fonts.md
florian-lefebvre Oct 24, 2024
45bff8a
feat: remove configurable defaults
florian-lefebvre Dec 18, 2024
f238d52
Update 0052-fonts.md
florian-lefebvre Dec 18, 2024
6454ba5
feat: update local provider shape
florian-lefebvre Feb 18, 2025
6ce971c
feat: caching
florian-lefebvre Feb 20, 2025
3c26829
feat: add fallbacks
florian-lefebvre Feb 27, 2025
a635c71
feat: explain fallbacks
florian-lefebvre Feb 28, 2025
8e4e172
feat: address reviews
florian-lefebvre Feb 28, 2025
6554d68
fix: typo
florian-lefebvre Feb 28, 2025
8ab8bc0
feat: update conditions order
florian-lefebvre Mar 3, 2025
bb51b9d
feat: remove cssVar prop
florian-lefebvre Mar 5, 2025
85dd3c2
feat: as prop
florian-lefebvre Mar 5, 2025
c4b13e6
feat: update fontaine to capsize
florian-lefebvre Mar 13, 2025
4c97a06
feat: update defaults
florian-lefebvre Mar 17, 2025
95c04c4
feat: as prop
florian-lefebvre Mar 17, 2025
f039f55
chore: remove todo
florian-lefebvre Mar 17, 2025
64e71d1
feat: disable automatic fallback generation
florian-lefebvre Mar 17, 2025
26a771d
feat: update rfc
florian-lefebvre Mar 30, 2025
28eac1e
Update 0052-fonts.md
florian-lefebvre Mar 31, 2025
360dc8f
feat: cssVariable
florian-lefebvre Apr 2, 2025
a057f65
feat: update local family shape
florian-lefebvre Apr 3, 2025
19872a1
feat: better local src
florian-lefebvre Apr 4, 2025
6ed0d41
feat: clarify entrypoints
florian-lefebvre Apr 4, 2025
06de5b8
feat: no default provider
florian-lefebvre Apr 7, 2025
265452b
feat: renames
florian-lefebvre Apr 10, 2025
a55738d
feat: weight/style inference for local provider
florian-lefebvre May 6, 2025
8665236
Merge branch 'main' into rfc/fonts
florian-lefebvre Aug 13, 2025
9eca2f9
chore: reorder
florian-lefebvre Aug 13, 2025
a2e5cd4
feat: mention csp
florian-lefebvre Aug 13, 2025
fb07e6f
Update 0055-fonts.md
florian-lefebvre Aug 27, 2025
75b5810
feat: getFontData()
florian-lefebvre Sep 19, 2025
72b6b46
feat: granular preloads
florian-lefebvre Oct 16, 2025
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
File renamed without changes.
369 changes: 369 additions & 0 deletions proposals/0052-fonts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,369 @@
**If you have feedback and the feature is released as experimental, please leave it on the Stage 3 PR. Otherwise, comment on the Stage 2 issue (links below).**

- Start Date: 2024-10-15
- Reference Issues: <!-- related issues, otherwise leave empty -->
- Implementation PR: <!-- leave empty -->
- Stage 2 Issue: https://github.com/withastro/roadmap/issues/1037
- Stage 3 PR: https://github.com/withastro/roadmap/pull/1039

# Summary

Have first-party support for fonts in Astro.

# Example

```js
// astro config
export default defineConfig({
fonts: {
families: ["Roboto", "Lato"],
},
});
```

```astro
---
// 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>
```

# Background & Motivation

Fonts is one of those basic things when making a website, but also an annoying one to deal with. Should I just use a link to a remote font? Or download it locally? How should I handle preloads then?

The goal is to improve the DX around using fonts in Astro.

> Why not using fontsource?

Fontsource is great! But it's not intuitive to preload, and more importantly, doesn't have all fonts. The goal is to have a more generic API for fonts (eg. you want to use a paid provider like adobe).

# Goals

- Specify what font to use
- Cache fonts
- Specify what provider to use
- Load/preload font on a font basis
- Generate fallbacks automatically
- Performant defaults
- Runtime agnostic
- Configure font families (subset, unicode range, weights etc)

# Non-Goals

- Runtime API (SSR is supported tho)
- Automatic subsetting (eg. analyzing static content)
- Automatic font detection (ie. downloading fonts based on font families names used in the user's project)

# Detailed Design

## Astro config

### Overview

The goal is to have a config that starts really simple for basic usecases, but can also be complex for advanced usecases. Here's an example of basic config:

```js
import { defineConfig } from "astro/config";

export default defineConfig({
fonts: {
families: ["Roboto", "Lato"],
},
});
```

That would get fonts from [Google Fonts](https://fonts.google.com/) with sensible defaults.

Here's a more complex example:

```js
import { defineConfig, fontProviders } from "astro/config";
import { myCustomFontProvider } from "./provider";

export default defineConfig({
fonts: {
providers: [
fontProviders.adobe({ apiKey: process.env.ADOBE_FONTS_API_KEY }),
myCustomFontProvider(),
],
defaults: {
provider: "adobe",
weights: [200, 700],
styles: ["italic"],
subsets: [
"cyrillic-ext",
"cyrillic",
"greek-ext",
"greek",
"vietnamese",
"latin-ext",
"latin",
],
},
families: [
"Roboto",
{
name: "Lato",
provider: "google",
weights: [100, 200, 300],
},
{
name: "Custom",
provider: "local",
src: ["./assets/fonts/Custom.woff2"],
},
],
},
});
```

### Providers

#### Definition

A provider allows to retrieve font faces data from a font family name from a given CDN or abstraction. It's a [unifont](https://github.com/unjs/unifont) provider.

#### Built-in providers

##### Google

This is the default, and it's not configurable. Given the amount of fonts it supports by, it sounds like a logic choice. Note that the default can be customized for more advanced usecases.

```js
export default defineConfig({
fonts: {
families: ["Roboto"],
},
});
```

```js
export default defineConfig({
fonts: {
defaults: {
provider: "local",
},
families: [
{
name: "Roboto",
provider: "google",
},
],
},
});
```

##### Local

This provider, unlike all the others, requires paths to fonts relatively to the root.

```js
import { defineConfig, fontProviders } from "astro/config";

export default defineConfig({
fonts: {
families: [
{
name: "Custom",
provider: "local",
src: ["./assets/fonts/Custom.woff2"],
},
],
},
});
```

#### Opt-in providers

Other unifont providers are exported from `astro/config`.

```js
import { defineConfig, fontProviders } from "astro/config";

export default defineConfig({
fonts: {
providers: [
fontProviders.adobe({ apiKey: process.env.ADOBE_FONTS_API_KEY }),
],
// ...
},
});
```

#### Why this API?

1. **Coherent API**: a few things in Astro are using this pattern, namely integrations and vite plugins. It's simple to author as a library author, easy to use as a user
2. **Keep opt-in providers**: allows to only use 2 providers by default, and keeps the API open to anyone
3. **Types!**: now that `defineConfig` [supports generics](https://github.com/withastro/astro/pull/12243), we can do powerful things! Associated with type generation, we can generate types for `families` `name`, infer the provider type from `defaults.provider` and more.

### Defaults

Astro must provide sensible defaults when it comes to font weights, subsets and more. But when dealing with more custom advanced setups, it makes sense to be able to customize those defaults. They can be set in `fonts.defaults` and will be merged with Astro defaults.

We need to decide what default to provide. I can see 2 paths:

| Path | Example (weight) | Advantage | Downside |
| --------- | ------------------- | --------------------- | --------------------------------------------------------------------------------- |
| Minimal | Only include `400` | Lightweight | People will probably struggle by expecting all weights to be available by default |
| Extensive | Include all weights | Predictable for users | Heavier by default |

### Families

A family is made of a least a `name`:

```js
export default defineConfig({
fonts: {
families: [
{
name: "Roboto",
},
"Roboto", // Shorthand
],
},
});
```

It can specify options such as `weights`, `subsets` that default to the value of `fonts.defaults`:

```js
export default defineConfig({
fonts: {
families: [
{
name: "Roboto",
weights: [400, 600],
},
],
},
});
```

It can also specify a `provider` (and `src` if it's the `local` provider):

```js
export default defineConfig({
fonts: {
families: [
{
name: "Roboto",
provider: "local",
src: "./Roboto.woff2",
},
],
},
});
```

### Font component

Setting the config (see above) configures what fonts to download, but it doesn't include font automatically on pages. Instead, we provide a `<Font />` component that can be used to compose where and how to load fonts.

```astro
---
import { Font } from "astro:assets"
---
<head>
<Font family="Inter" preload cssVar="primary-font" />
<Font family="Lato" />
</head>
```

### Family

The family will be typed using type gen, based on the user's config.

### Preload

Defaults to `false`:

- **Enabled**: Outputs a preload link tag and a style tag, without fallbacks
- **Disabled**: Output a style tag with fallbacks (generated using [fontaine](https://github.com/unjs/fontaine))

### cssVar

Defaults to `astro-font-${computedFontName}`. Specifies what identifier to use for the generated css variable. This is useful for font families names that may contain special character or conflict with other fonts.

## Usage

Since fallbacks may be generated for a given family name, this name can't be used alone reliably:

```css
h1 {
font-family: "Inter"; /* Should actually be "Inter", "Inter Fallback" */
}
```

To solve this issue, a css variable is provided by the style tage generated by the `<Font />` component:

```css
h1 {
font-family: var(--astro-font-inter); /* "Inter", "Inter Fallback" */
}
```

## How it works under the hood

- Once the config is fully resolved, we get fonts face data using `unifont`
- We generate fallbacks using `fontaine` and pass all the data we need through a virtual import, used by the `<Font />` component
- We inject a vite middleware in development to download fonts as they are requested in development
- During build, we download all fonts and put them in `outDir`

Data is cached to `cacheDir` for builds and `.astro/fonts` in development.

# Testing Strategy

- Integration tests
- Experimental flag (`experimental.fonts`)

# Drawbacks

I have not identified any outstanding drawback:

- **Implementation cost, both in term of code size and complexity**: fine
- **Whether the proposed feature can be implemented in user space**: yes
- **Impact on teaching people Astro**: should make things easier, will need updating docs
- **Integration of this feature with other existing and planned features**: reuses `astro:assets` to export the component, otherwise isolated from other features
- **Is it a breaking change?** No

# Alternatives

## As an integration

This feature could be developed as an integration, eg. `@astrojs/fonts`. It will probably be an internal integration (like actions) but making it part of core allows to make it more discoverable, more used. It also allows to use the `astro:assets` module.

## Different API for simpler cases

The following API has been suggested for the simpler cases:

```js
export default defineConfig({
fonts: ["Roboto"],
});
```

I'd love to support such API where you can provide fonts top level, or inside `fonts.families` but we can't. We can't because of how the integration API `defineConfig()` works. What if a user provides fonts names as `fonts`, and an integration provides fonts names as `fonts.families`? Given how the merging works, the shape of `AstroUserConfig` and `AstroConfig` musn't be too different. It already caused issues with i18n in the past.

# Adoption strategy

- **If we implement this proposal, how will existing Astro developers adopt it?** Fonts setups can vary a lot but migrating to the core fonts api should not require too much work
- **Is this a breaking change? Can we write a codemod?** No
- **How will this affect other projects in the Astro ecosystem?** This should make [`astro-font`](https://github.com/rishi-raj-jain/astro-font) obsolete

# Unresolved Questions

- We need to see how merging `fonts.defaults` will work, especially for `updateConfig()`. Should we merge arrays in this case?
- We need to check if fallbacks should still be included for preloaded fonts