Skip to content

Conversation

@florian-lefebvre
Copy link
Member

@florian-lefebvre florian-lefebvre commented Oct 15, 2024

Summary

Have first-party support for fonts in Astro:

// astro.config.mjs
import { defineConfig, fontProviders } from "astro/config"

export default defineConfig({
	fonts: [
		{
			name: "Roboto",
			cssVariable: "--font-roboto",
			provider: fontProviders.google(),
		}
	]
})
---
// layouts/Layout.astro
import { Font } from "astro:fonts"
---
<head>
	<Font cssVariable="--font-roboto" />
	<style>
		h1 {
			font-family: var(--font-roboto);
		}
	</style>
</head>

Links

@florian-lefebvre florian-lefebvre changed the title [WIP] Fonts Fonts Oct 22, 2024
@florian-lefebvre florian-lefebvre marked this pull request as ready for review October 22, 2024 14:39
@florian-lefebvre florian-lefebvre mentioned this pull request Oct 22, 2024
@lilnasy
Copy link
Contributor

lilnasy commented Oct 22, 2024

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.

@florian-lefebvre
Copy link
Member Author

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.

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

@JusticeMatthew
Copy link

JusticeMatthew commented Jul 14, 2025

I think in builds we should keep them as is as it's better for caching but in dev, we could either do something like inter-500-italic-<hash>.<ext> or even inter-500-italic.<ext>

Perhaps after what we learned in #13826 it might also be good to indicate if the file is a subset as well? (if possible) 😅

@florian-lefebvre
Copy link
Member Author

@JusticeMatthew see withastro/astro#14279

@robinlahtinen
Copy link

The experimental Fonts API inserts elements at the beginning of the <head> section, which pushes important meta tags such as the charset declaration beyond the first 1024 bytes of the HTML. As a result, Lighthouse reports: "Charset declaration is missing or occurs too late in the HTML."

@delucis
Copy link
Member

delucis commented Sep 14, 2025

The experimental Fonts API inserts elements at the beginning of the <head> section, which pushes important meta tags such as the charset declaration beyond the first 1024 bytes of the HTML. As a result, Lighthouse reports: "Charset declaration is missing or occurs too late in the HTML."

IIUC the <Font> component renders where it is used, so adding it to your pages below important meta tags like that should fix this.

@florian-lefebvre
Copy link
Member Author

Yes as Chris said you control the position in the head

@florian-lefebvre
Copy link
Member Author

What do you think about cssVariable requiring the leading --? Do you think it's fine as is or would you prefer not having to specify it? Eg.

{
-	cssVariable: '--my-font'
+	cssVariable: 'my-font'
}

-<Font cssVariable="--my-font" />
+<Font cssVariable="my-font" />
:root {
	font-family: var(--my-font);
}

@JusticeMatthew
Copy link

What do you think about cssVariable requiring the leading --? Do you think it's fine as is or would you prefer not having to specify it? Eg.

{
-	cssVariable: '--my-font'
+	cssVariable: 'my-font'
}

-<Font cssVariable="--my-font" />
+<Font cssVariable="my-font" />
:root {
	font-family: var(--my-font);
}

Personally, I don't mind either way, but for consistency, I think it would be nice to require the --. This way, users are typing the same thing in each of the few places they need it vs "Oh wait I'm in the component prop, I need to not include the --" if that makes sense

@RomanHauksson
Copy link

Agreed on requiring the --. This made it quicker for me to understand what corresponds to what.

@plttn
Copy link

plttn commented Oct 30, 2025

Hey, following up on this: using getFontData() with something like Satori for a statically built site is still pretty rough (with a few documentation gaps it seems too). I have a font that the Font API only ends up putting the woff2 in the build. (Space Grotesk).

If I follow the snippet from the example shown in how to use it I run into a few problems:

  1. There's await inside a non-async function. -- Easy fix, no big deal.
  2. The Satori example object doesn't actually work either. -- again, no big deal
  3. Referencing the Roboto config example from above, there's a comma missing making it invalid.
  4. With the woff2 gap in Satori, I'm not entirely sure that this snippet would ever work, even in an SSR deployment. 🤔
20:49:12   └─ /og.png[ { url: '/_astro/fonts/0bcd64743584f51b.woff2', format: 'woff2' } ]

After finally wiring up everything to be type-happy, I end up getting a 404 for Roboto (and a woff2 conversion error for Space Grotesk) with the 404 ending up being my production URL, which doesn't have Roboto built/deployed, and the referenced URL for Space Grotesk ends up being the woff2 as the woff isn't included in the final build that I would deploy to my production site.

The 404 for Roboto appears to be because the intent of this API only seems to be server-side rendering, is that correct?

I'd just like to be able to simplify this and be able to build it locally:

const fetchFonts = async () => {
  // Regular Font
  const fontFileRegular = await fs.readFile(
    "node_modules/@fontsource/space-grotesk/files/space-grotesk-latin-400-normal.woff",
  );
  const fontRegular: ArrayBuffer = fontFileRegular.buffer as ArrayBuffer;

  // Bold Font
  const fontFileBold = await fs.readFile(
    "node_modules/@fontsource/space-grotesk/files/space-grotesk-latin-600-normal.woff",
  );
  const fontBold: ArrayBuffer = fontFileBold.buffer as ArrayBuffer;

  return { fontRegular, fontBold };
};

into something like this (I'll even deal with having to do the buffers here still, but not needing to do the buffer casting to make Satori happy with it would be exceptional):

const fetchFonts = async () => {
  // Regular Font
  const fontFileRegular = await getFontData("--font-space-grotesk", 400, "normal", "woff");
  );
  const fontRegular: ArrayBuffer = fontFileRegular.buffer as ArrayBuffer;

  // Bold Font
  const fontFileBold= await getFontData("--font-space-grotesk", 600, "normal", "woff");

  const fontBold: ArrayBuffer = fontFileBold.buffer as ArrayBuffer;

  return { fontRegular, fontBold };
};

Overall though I'm very happy with the Font feature and I can't wait to see it graduate.

@florian-lefebvre
Copy link
Member Author

Thanks for the feedback! Interesting API idea, I'll think about it. 2 questions:

  1. Can you create a minimal reproduction with the satori stuff so I can look into it?
  2. Do you want to send a PR to docs to update the example? File is located at https://github.com/withastro/docs/blob/main/src/content/docs/en/reference/experimental-flags/fonts.mdx

That would be super helpful

@plttn
Copy link

plttn commented Oct 30, 2025

Thanks for the feedback! Interesting API idea, I'll think about it. 2 questions:

  1. Can you create a minimal reproduction with the satori stuff so I can look into it?
  2. Do you want to send a PR to docs to update the example? File is located at https://github.com/withastro/docs/blob/main/src/content/docs/en/reference/experimental-flags/fonts.mdx

That would be super helpful

I can spin up a repro example definitely! I don't think it would make sense to edit the docs just yet though given that like I mentioned, even in the best case, that use case probably wouldn't work as it currently stands.

@plttn
Copy link

plttn commented Oct 30, 2025

@florian-lefebvre
Copy link
Member Author

florian-lefebvre commented Oct 30, 2025

Thanks for the repro. I think there are a few things to consider here, let me know what you think:

  1. getFontData() is only responsible for giving all the data we have. We cannot ensure you'll get data usable by satori, it depends on your provider
  2. However, it looks like some unifont providers as they're implemented currently do not always try to get non woff2 files. Eg. fontsource implementation: https://github.com/unjs/unifont/blob/main/src/providers/fontsource.ts#L43
  3. It seems some unifont providers return woff files (eg. google's) but they do not end up in getFontData(). I'll look into it tomorrow

So IMO the API is working as designed, but work should be done upstream (unjs/unifont#274) and there's a bug in Astro

@plttn
Copy link

plttn commented Oct 30, 2025

Thanks for the update! I can work with that longer term. In the meantime I'll continue using the node_modules path, and maybe explore an upstream fix for unifont so that it gets the woff/ttf variants in which case Satori is happy. Thanks for digging into this with me! :)

@plttn
Copy link

plttn commented Oct 30, 2025

cutting the Gordian knot here: bunny has both fonts I use as well and grabs the woff variants so I think we're golden! Thanks again!

@florian-lefebvre
Copy link
Member Author

Great! Do you think the docs example could be improved, eg. with more realistic filtering of the font data? If so please open an issue or a PR on https://github.com/withastro/docs, that would help a lot

@Nickersoft
Copy link

@florian-lefebvre Hey, great work on the Fonts API so far – it's shaping up really well. One thing I noticed in the docs is there doesn't seem to be a very good story around variable fonts, apart from the one line mentioning you can specify a weight range such as "100 900". Fontsource technically offers both static and variable versions of fonts and it's not always clear when you specify "Inter" for example, what is being downloaded. Inter also offers optical sizing (opsz) for its fonts, but there don't seem to be any way to enable it via the current API.

@RomanHauksson
Copy link

A couple weeks ago I discovered a bug, that the automatically generated fallback font faces weren’t being resized correctly. I was going to move my reproducible example into a Stackblitz container and write up an issue and all that, but never got around to it, so I’ll just comment the info here (better than not reporting it at all I suppose, sry!)

I noticed there was some CLS when the font for one of my projects gets swapped from Arial (the fallback) to Noto Sans. When I looked into the source code to see how the fonts API generates the size adjustment factor + font metric overrides for Arial, I saw that it used some code copied from Fontaine, which was copied from Capsize. I tried generating the overrides from Capsize directly, and that worked. So some logic might have gotten lost during the game of telephone?

Try previewing this example to see the difference.

@florian-lefebvre
Copy link
Member Author

florian-lefebvre commented Nov 4, 2025

Hi @Nickersoft, thanks!

doesn't seem to be a very good story around variable fonts

The reason is we don't handle them in any special way (or if any, very little)! Handling is the responsability of each provider, in unifont. So you may way want to have a look at the fontsource provider to see how it's handled and how it could be improved: https://github.com/unjs/unifont/blob/main/src/providers/fontsource.ts.

When the feature is stabilize, we could include a section dedicated to variable fonts in docs.

Inter also offers optical sizing (opsz) for its fonts, but there don't seem to be any way to enable it via the current API.

Currently only the google provider supports this through this option: https://github.com/unjs/unifont/blob/main/src/providers/google.ts#L16. Here is how it's used in practice: https://github.com/shipshapecode/starpod/blob/main/astro.config.mjs#L35-L46.

So if you want support for this when using the fontsource, please open an issue on the unifont repo: https://github.com/unjs/unifont/issues

@florian-lefebvre
Copy link
Member Author

@RomanHauksson thanks for the report, I'll have a look this week

@florian-lefebvre
Copy link
Member Author

@RomanHauksson thanks for the repro, it's very helpful! I made a preview PR in withastro/astro#14713 and tested locally, here's the diff:

-size-adjust: 122.9249%;
-ascent-override: 86.9637%;
-descent-override: 23.8357%;
+size-adjust: 106.3255%;
+ascent-override: 100.5403%;
+descent-override: 27.5569%;

Seems to now match capsize perfectly! Please give it a try as well and confirm if it looks good to you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.