-
Notifications
You must be signed in to change notification settings - Fork 31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fonts #1039
base: main
Are you sure you want to change the base?
Fonts #1039
Changes from 16 commits
526ab7c
c53956c
4b67e1f
5006242
28b1c1c
987d13a
c589609
8250880
7233fbe
ff39e16
df5bbd5
c79c1fa
caa649b
64b9ba4
94b5805
15e938a
45bff8a
f238d52
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
florian-lefebvre marked this conversation as resolved.
Show resolved
Hide resolved
|
||
- 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: { | ||
florian-lefebvre marked this conversation as resolved.
Show resolved
Hide resolved
|
||
provider: "adobe", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how would you know the name of the provider here? Will it be... typed (👀) automatically based on what you put in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes! Thanks to |
||
weights: [200, 700], | ||
styles: ["italic"], | ||
subsets: [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would love an example with opentype fonts features There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess that would be one key per setting? Something like the following: {
fontKerning: "'kern' 1",
fontVariantAlternates: "1"
// etc...
} MDN docs: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_fonts/OpenType_fonts_guide |
||
"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. | ||
florian-lefebvre marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
#### Built-in providers | ||
|
||
|
||
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 | ||
ematipico marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm team just 400, Google Fonts also gives you only 400 when you unselect the "give all options" setting |
||
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's gonna be important to make sure those variables are accessible correctly to things like Tailwind. It's a bit unclear with this API because the Tailwind CSS would be imported in the frontmatter, whereas the fonts are in the document.
Perhaps Vite will re-order it to make it work, not sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well since it's just some css, I don't think it will cause any issues? In Tailwind 3, you'd have to reference like so: