---
title: "Advanced"
description: "Find out how Nuxt Fonts works behind the scenes to optimize fonts in your project."
canonical_url: "https://nuxt.com/modules/fonts/advanced"
---
# Advanced

> Find out how Nuxt Fonts works behind the scenes to optimize fonts in your project.

## How it works

Nuxt Fonts processes all your CSS and does the following things automatically when it encounters a `font-family` declaration.

1. **Resolves fonts used in CSS**. It starts by looking in your `public/` directory for font files that match the name, like `Roboto.woff2`, `RobotoBold.ttf`, etc. Then it moves on to web font providers like `google`, `bunny` and `fontshare`, which we query through [unifont](https://unifont.dev). Once a provider is found (in this case, probably [Google Fonts](https://fonts.google.com)), we move on to the next step.
2. **Generates and injects `@font-face` rules for you.** We'll generate rules to point your browser to the right source files. They'll be injected into the same CSS where you use the `font-family`.
   ```css
   /* If you write something like this: */
   :root {
     font-family: Poppins;
   }
   ```
   ```css
   /* Then Nuxt fonts will add declarations that look like this at the beginning of the CSS file: */
   @font-face {
     font-family: 'Poppins';
     src: local("Poppins"), url("/_nuxt/fonts/<hash>.woff2") format(woff2);
     font-display: swap;
     unicode-range: U+0000-00FF,U+0131, /* ... */;
     font-weight: 400;
     font-style: normal;
   }
   /* ... plus more font-face declarations for other unicode ranges/weights */
   ```
3. **Proxies and caches font requests.** Rather than using the original source URLs (to remote servers), we generate rewrites under our own asset directory. When accessed by your browser, we download the font from the remote server and cache it locally.
4. **Creates font fallback metrics.** If we have access to the font metrics (ascent, descent, line gap, character width, etc.) then we can generate a fallback `@font-face` declaration as well, using [fontaine](https://github.com/unjs/fontaine). The idea is that we 'morph' a local system font (like Arial or Times New Roman) to be as close as possible to the size of the web font, to decrease layout shift ([read more about CLS](https://web.dev/articles/cls)).
   ```css
   :root {
     /* This will generate fallbacks for local versions of Helvetica and Arial, adjusted to match Roboto's metrics. */
     font-family: Roboto, Helvetica, Arial;
     /* If you provide a generic family (like serif or sans-serif), we will use a system font from that family. */
     font-family: Merriweather, serif;
   }
   ```
5. **Include fonts in build.** When you build your project, we'll copy across all the fonts used in your project so you don't need to make any external requests when loading your site. (Any that haven't already been cached in development are downloaded at build time.) Font file names are hashed and Nuxt will serve them with long-lived cache headers.

## What Nuxt Fonts is built on

Most of the pipeline above lives in framework-agnostic packages that you can use without Nuxt:

- [unifont](https://unifont.dev) resolves font metadata from a provider. It ships the `google`, `googleicons`, `adobe`, `bunny`, `fontshare`, `fontsource` and `npm` providers, and you can write your own against the same interface. Source at [unjs/unifont](https://github.com/unjs/unifont).
- [fontaine](https://github.com/unjs/fontaine) generates the adjusted fallback `@font-face` rules that cut layout shift while a web font loads.
- [fontless](https://github.com/unjs/fontaine) is the same resolve-and-inject pipeline as a Vite plugin, for projects that aren't built on Nuxt.

## Builder support

Nuxt Fonts works with the Vite, webpack and rspack builders. In each case we inject `@font-face` rules into your stylesheets and download the fonts at build time; where they are then served from differs, and is covered in [Where fonts are served from](#where-fonts-are-served-from).

With webpack and rspack, our CSS transform runs immediately before `css-loader`, so it sees your styles after any preprocessor (such as Sass) and PostCSS have run. A family named through a preprocessor variable is still detected, but a `font-family` value produced by a later loader in the chain is not.

## Where fonts are served from

The location of a font file is an implementation detail rather than a stable URL contract. Don't hard-code these paths; link to fonts through the CSS we generate, or read them from the [`fonts:resolved`](#fontsresolved) hook.

| Build                 | Path                                                            | `app.baseURL` | `app.cdnURL`   | Relative `app.baseURL` |
| --------------------- | --------------------------------------------------------------- | ------------- | -------------- | ---------------------- |
| Vite production build | `<buildAssetsDir>/fonts/<hash>.woff2`, that is `/_nuxt/fonts/…` | Applied       | Applied        | Applied                |
| Development           | `/_fonts/<hash>.woff2`                                          | Applied       | Not applicable | Not applicable         |
| Webpack, rspack       | `/_fonts/<hash>.woff2`                                          | Applied       | Not applied    | Not applied            |

In a Vite production build the fonts are handed to Vite's asset pipeline, so Vite rewrites their URLs exactly as it does for any other asset it emits: `app.baseURL`, `app.cdnURL`, a relative base and `experimental.renderBuiltUrl` all apply. Bundled stylesheets get URLs relative to the stylesheet (`url(./fonts/…)`), because that is what `@nuxt/vite-builder` asks Vite to do for non-JS assets; server-inlined `<style>` blocks get an absolute URL resolved at runtime.

Webpack and rspack have no equivalent asset pipeline for the CSS we generate, so fonts stay on a public path with long-lived cache headers, and only `app.baseURL` is applied to them.

## Caching fonts between builds

Font metadata and downloaded font files are cached on disk in `node_modules/.cache/nuxt/fonts/meta`, relative to your project root. If you restore that directory in CI, no fonts are downloaded and no provider requests are made at all.

```yaml [.github/workflows/build.yml]
- uses: actions/cache@v4
  with:
    path: node_modules/.cache/nuxt/fonts
    key: nuxt-fonts-${{ hashFiles('nuxt.config.ts') }}
    restore-keys: nuxt-fonts-
```

A few things to check if the cache doesn't seem to take effect:

- The cache step must run **after** you install dependencies. Some package managers remove unknown directories inside `node_modules`, which will delete a cache restored before install.
- `actions/setup-node` with `cache: 'pnpm'` (or `npm`/`yarn`) caches the package manager store, not `node_modules`, so it does not cover fonts.
- With an exact `key` and no `restore-keys`, a config change means a cache miss and a full re-download.
- In Docker builds, `node_modules/.cache` lives inside the build container. You need a cache mount or a persistent volume for it to survive:
  ```dockerfile [Dockerfile]
  RUN --mount=type=cache,target=/app/node_modules/.cache npm run build
  ```

If a path inside `node_modules` is awkward to cache, move it with the [`cache`](https://nuxt.com/modules/fonts/configuration#cache) option.

## Module Hooks

Nuxt Fonts exposes hooks for module authors to extend functionality.

### `fonts:providers`

Add or modify font providers after all modules have loaded.

```ts
nuxt.hook('fonts:providers', (providers) => {
  providers['my-provider'] = defineFontProvider(/* ... */)
})
```

### `fonts:public-asset-context`

::warning
Deprecated. Use [`fonts:resolved`](#fontsresolved) instead.
::

Access the font resolution context.

```ts
nuxt.hook('fonts:public-asset-context', (context) => {
// context.renderedFontURLs: Map<filename, sourceUrl>
// Populated during vite build
})
```

### `fonts:resolved`

Called when a font family is resolved, with the `@font-face` data generated for it. This covers families found in your CSS and families declared with `global: true` in `fonts.families`, so modules that render fonts outside the browser (e.g., OG image generation) can reuse them instead of resolving them again.

```ts
const files = new Map<string, () => Promise<Buffer>>()

nuxt.hook('fonts:resolved', (font) => {
  // font.fontFamily: 'Poppins'
  // font.fonts: FontFaceData[], with each `src` URL as served (with `app.baseURL`)
  for (const file of font.files) {
    // file.url: '/_nuxt/fonts/<hash>.woff2', matching a `src` URL in `font.fonts`
    files.set(file.url, file.readFont)
  }
})

nuxt.hook('nitro:build:before', async () => {
  for (const readFont of files.values()) {
    const contents = await readFont() // the file as served, subsetted if you set `glyphs`
  }
})
```

Listeners run while your stylesheets are transformed, so keep them fast: collect what you need and call `readFont()` later, as above.

In a production build, every family is resolved before Nitro builds. In development, families found in your CSS are resolved as the stylesheets that use them are requested.


## Sitemap

See the full [sitemap](https://nuxt.com/sitemap.md) for all pages.
