Giter Club home page Giter Club logo

fontaine's Introduction

fontaine

npm version npm downloads Github Actions Codecov

Automatic font fallback based on font metrics

Features

  • 💪 Reduces CLS by using local font fallbacks with crafted font metrics.
  • ✨ Generates font metrics and overrides automatically.
  • ⚡️ Pure CSS, zero runtime overhead.

On the playground project, enabling/disabling fontaine makes the following difference rendering /, with no customisation required:

Before After
CLS 0.24 0.054
Performance 92 100

Installation

With pnpm

pnpm add -D fontaine

Or, with npm

npm install -D fontaine

Or, with yarn

yarn add -D fontaine

Usage

import { FontaineTransform } from 'fontaine'

// Astro config - astro.config.mjs
import { defineConfig } from 'astro/config'

const options = {
  fallbacks: ['BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Arial', 'Noto Sans'],
  // You may need to resolve assets like `/fonts/Roboto.woff2` to a particular directory
  resolvePath: id => `file:///path/to/public/dir${id}`,
  // overrideName: (originalName) => `${name} override`
  // sourcemap: false
  // skipFontFaceGeneration: (fallbackName) => fallbackName === 'Roboto override'
}

// Vite
export default {
  plugins: [FontaineTransform.vite(options)]
}

// Next.js
export default {
  webpack(config) {
    config.plugins = config.plugins || []
    config.plugins.push(FontaineTransform.webpack(options))
    return config
  },
}

// Docusaurus plugin - to be provided to the plugins option of docusaurus.config.js
// n.b. you'll likely need to require fontaine rather than importing it
const fontaine = require('fontaine')

function fontainePlugin(_context, _options) {
  return {
    name: 'fontaine-plugin',
    configureWebpack(_config, _isServer) {
      return {
        plugins: [
          fontaine.FontaineTransform.webpack(options),
        ],
      }
    },
  }
}

// Gatsby config - gatsby-node.js
const { FontaineTransform } = require('fontaine')

exports.onCreateWebpackConfig = ({ stage, actions, getConfig }) => {
  const config = getConfig()
  config.plugins.push(FontaineTransform.webpack(options))
  actions.replaceWebpackConfig(config)
}

export default defineConfig({
  integrations: [],
  vite: {
    plugins: [
      FontaineTransform.vite({
        fallbacks: ['Arial'],
        resolvePath: id => new URL(`./public${id}`, import.meta.url), // id is the font src value in the CSS
      }),
    ],
  },
})

Note If you are using Nuxt, check out nuxt-font-metrics which uses fontaine under the hood.

If your custom font is used through the mechanism of CSS variables, you'll need to make a tweak to your CSS variables to give fontaine a helping hand. Docusaurus is an example of this, it uses the --ifm-font-family-base variable to reference a custom font. In order that fontaine can connect the variable with the font, we need to add a {Name of Font} override suffix to that variable. What does this look like? Well imagine we were using the custom font Poppins which is referenced from the --ifm-font-family-base variable, we'd make the following adjustment:

:root {
  /* ... */
-  --ifm-font-family-base: 'Poppins';
+  --ifm-font-family-base: 'Poppins', 'Poppins override';

Behind the scenes, there is a 'Poppins override' @font-face rule that has been created by fontaine. By manually adding this override font family to our CSS variable, we make our site use the fallback @font-face rule with the correct font metrics that fontaine generates.

How it works

fontaine will scan your @font-face rules and generate fallback rules with the correct metrics. For example:

@font-face {
  font-family: 'Roboto';
  font-display: swap;
  src: url('/fonts/Roboto.woff2') format('woff2'), url('/fonts/Roboto.woff')
      format('woff');
  font-weight: 700;
}
/* This additional font-face declaration will be added to your CSS. */
@font-face {
  font-family: 'Roboto override';
  src: local('BlinkMacSystemFont'), local('Segoe UI'), local('Helvetica Neue'),
      local('Arial'), local('Noto Sans');
  ascent-override: 92.7734375%;
  descent-override: 24.4140625%;
  line-gap-override: 0%;
}

Then, whenever you use font-family: 'Roboto', fontaine will add the override to the font-family:

:root {
  font-family: 'Roboto';
  /* This becomes */
  font-family: 'Roboto', 'Roboto override';
}

💻 Development

  • Clone this repository
  • Enable Corepack using corepack enable (use npm i -g corepack for Node.js < 16.10)
  • Install dependencies using pnpm install
  • Run interactive tests using pnpm dev; launch a vite server using source code with pnpm demo:dev

Credits

This would not have been possible without:

License

Made with ❤️

Published under MIT License.

fontaine's People

Contributors

aleksandrhovhannisyan avatar atinux avatar danielroe avatar gitrowin avatar johnnyreilly avatar michaeltaranto avatar renovate[bot] avatar zanfee avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fontaine's Issues

The URL_RE doesn't work if the font URL contains a ")" close paren character

🐛 The bug

There are some packages, for example, the inter-ui NPM package (which is recommended by the official inter GH readme for NPM) which, very unfortunately, have parens in some of the folder names.

For example from the inter-ui package the fonts are under folders with parens in their names.

Example font file: inter-ui/Inter (web)/Inter-Regular.woff2

The URL_RE that fontaine is using is defined as

const URL_RE = createRegExp(
  exactly("url(").and(charNotIn(")").times.any().as("url")).and(")"),
  ["g"]
);

So no matter what it breaks with these URLs.

The problem is that source URL would get regex'd out as inter-ui/Inter (web and then the next check is to see if the source ends with one of the supported extensions (and since the regex mangled the URL, the extension was lost).

🛠️ To reproduce

https://stackblitz.com/edit/github-hdufeg?file=index.css

🌈 Expected behaviour

Parsing of the source url should support any valid URL characters

ℹ️ Additional context

No response

Gatsby instructions

📚 Is your documentation request related to a problem?

I would like to see guidance on how to use fontaine with Gatsby.

We're using a pretty standard set up of Gatsby and Sass. I've installed fontaine using the below code in gatsby-node.js. However the override font-face rules aren't created and the font-family isn't being changed.

exports.onCreateWebpackConfig = ({ stage, actions, getConfig }) => {
  const config = getConfig();
  config.plugins.push(
    FontaineTransform.webpack({
      fallbacks: ['BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Arial', 'Noto Sans']
    })
  );
  actions.replaceWebpackConfig(config);
};

🔍 Where should you find it?

Ideally README.md

ℹ️ Additional context

No response

add framework examples

It would be good to add some sandboxes or example configurations so it's easy for people to get up-and-running with (at least):

  • Angular
  • Laravel
  • Vite
  • Next.js

getMetricsForFamily returns null for font families containing multiple spaces (e.g., "IBM Plex Mono")

🐛 The bug

I'm trying to read the font metrics for IBM Plex Mono (this issue is actually reproducible with any typeface that has more than one space in its family name), but getMetricsForFamily is returning null. This is because of the following line of code:

https://github.com/danielroe/fontaine/blob/e7391ee7593f4e67dbdcb8fdfd045c5fcc3f9e62/src/metrics.ts#L15

Which should do a global replacement for spaces:

const name = camelCase(family).replace(/ /g, '')

🛠️ To reproduce

N/A (invoke getMetricsForFamily with IBM Plex Mono or any other font family with multiple spaces in its name)

🌈 Expected behaviour

Current behavior: IBM Plex Mono becomes iBMPlex Mono

Expected behavior: IBM Plex Mono becomes iBMPlexMono, allowing us to key in here correctly:

https://github.com/seek-oss/capsize/blob/42d6dc39d58247bc6b9e013a4b1c4463bf287dca/packages/metrics/src/entireMetricsCollection.json#L5842-L5852

ℹ️ Additional context

No response

enable `size-adjust` metric calculation

Currently we can use metrics to adjust vertical height accurately. There's still a horizontal mismatch though, even if slight, and we can likely solve this by applying the right metrics to size-adjust.

Parse font faces and consider font weight

🆒 Your use case

Sometimes we have this setup:

@font-face {
    font-family: 'Biennale';
    font-weight: 400;
    font-style: normal;
    font-display: swap;
    src: url('~/assets/fonts/Biennale-Book.woff2') format('woff2'),
    url('~/assets/fonts/Biennale-Book.woff') format('woff');
}

@font-face {
    font-family: 'Biennale';
    font-weight: 500;
    font-display: swap;
    src: url('~/assets/fonts/Biennale-Medium.woff2') format('woff2'),
    url('~/assets/fonts/Biennale-Medium.woff') format('woff');
}

When fontaine is parsing the font-face it creates the override/fallback font-face without accounting for the weight.

@font-face {
  font-family: "Biennale override";
  src: local("BlinkMacSystemFont"),local("Segoe UI"),local("Roboto"),local("Helvetica Neue"),local("Arial"),local("Noto Sans");
  ascent-override: 94.4%;
  descent-override: 24.5%;
  line-gap-override: 0%;
}
@font-face {
    font-family: 'Biennale';
    font-weight: 400;
    font-style: normal;
    font-display: swap;
    src: url('/_nuxt/assets/fonts/Biennale-Book.woff2') format('woff2'),
    url('/_nuxt/assets/fonts/Biennale-Book.woff') format('woff');
}
@font-face {
  font-family: "Biennale override";
  src: local("BlinkMacSystemFont"),local("Segoe UI"),local("Roboto"),local("Helvetica Neue"),local("Arial"),local("Noto Sans");
  ascent-override: 96.7%;
  descent-override: 26.5%;
  line-gap-override: 0%;
}
@font-face {
    font-family: 'Biennale';
    font-weight: 500;
    font-display: swap;
    src: url('/_nuxt/assets/fonts/Biennale-Medium.woff2') format('woff2'),
    url('/_nuxt/assets/fonts/Biennale-Medium.woff') format('woff');
}

🆕 The solution you'd like

We should probably add weight parsing, we should just probably parse it with regexp and add it on the created weight/override.

Expected result would be:

@font-face {
    font-family: "Biennale override";
    font-weight: 400;
    font-style: normal;
    src: local("BlinkMacSystemFont"),local("Segoe UI"),local("Roboto"),local("Helvetica Neue"),local("Arial"),local("Noto Sans");
    ascent-override: 94.4%;
    descent-override: 24.5%;
    line-gap-override: 0%;
}
@font-face {
    font-family: 'Biennale';
    font-weight: 400;
    font-style: normal;
    font-display: swap;
    src: url('/_nuxt/assets/fonts/Biennale-Book.woff2') format('woff2'),
    url('/_nuxt/assets/fonts/Biennale-Book.woff') format('woff');
}
@font-face {
    font-family: "Biennale override";
    font-weight: 500;
    font-display: swap;
    src: local("BlinkMacSystemFont"),local("Segoe UI"),local("Roboto"),local("Helvetica Neue"),local("Arial"),local("Noto Sans");
    ascent-override: 96.7%;
    descent-override: 26.5%;
    line-gap-override: 0%;
}
@font-face {
    font-family: 'Biennale';
    font-weight: 500;
    font-display: swap;
    src: url('/_nuxt/assets/fonts/Biennale-Medium.woff2') format('woff2'),
    url('/_nuxt/assets/fonts/Biennale-Medium.woff') format('woff');
}

🔍 Alternatives you've considered

No response

ℹ️ Additional info

No response

Doesn't seem to work with Webpack

🐛 The bug

Added as a regular webpack plugin in my config, when building (with or without the watch flag) the bundle, the font faces and the font-family are all untouched.

🛠️ To reproduce

https://stackblitz.com/edit/webpack-webpack-js-org-7bs3qn?file=webpack.config.js

🌈 Expected behaviour

It should apply the expected transformation per font.

ℹ️ Additional context

The config uses the following loaders for SCSS and CSS:

  • MiniCssExtractPlugin
  • css-loader
  • postcss-loader
  • sass-loader

It also uses stylelint to lint the files.

The postcss config is pretty straight forward as it consists of the default configurations of the following plugins:

  • autoprefixer
  • postcss-preset-env
  • cssnano

Fonts are processed using Webpack 5's asset/module system (instead of file-loader for instance). "Static" fonts that aren't meant to change are not touched by webpack, but referenced in URLs (pointing to the correct file from the main css file's perspective).

NB: I've also tried with file-loader instead, the result is the same

Fonts CLS seem to be worse?

📚 What are you trying to do?

I was loading this through nuxt-fontaine, and then through nuxt-fonts and it seems like CLS is actually worse using this package. We're using Plus Jakarta Sans from Bunny Fonts. The fallback font is loading, but the sizing seems to be off and it's actually causing more CLS than not using this.

When this package is not used, it falls back to Helvetica, which is fairly close and has a similar number of lines in most places. When this is in use, even if Helvetica is set as the fallback font (instead of Arial, which it defaults to) the sizing is just generally worse everywhere.

🔍 What have you tried?

I've manually set the fallback font to be Helvetica. I've also tried using both nuxt-fontaine and nuxt-fonts to make sure I wasn't configuring things in an odd way.

I also tried Roboto, which is used in some of the examples. I saw similar CLS issues, though not as severe.

ℹ️ Additional context

Is this particular font just hard to size or something? Generally, nuxt-fonts worked pretty well, but the sizing of the fallback font was definitely a problem.

Maige Usage

Thanks for trying Maige.

Running GPT-based services is pricey. At this point, we ask you to add payment info to continue using Maige.

Add payment info

Feel free to close this issue.

Need help configuring vite.config.mjs

📚 Is your documentation request related to a problem?

Hello, first of all, thank you for this project! I am getting started and was wondering if you could help me get off the ground.

I have a webpage that uses a custom font, GraphikCondensed, and it has significant CLS on Chrome for Android.

The fallback font is Roboto, but I am on a Mac, so Roboto has been installed on the Mac. The woff2 file for the custom font, GraphikCondensed, has been placed under the fonts directory in the playground.

In vite.config.mjs, I am not sure about what needs to be in the options.

This is what index.css looks like...

@font-face {
  font-family: 'GraphikCondensed';
  src: local('GraphikCondensed'), url(/fonts/GraphikXXCondensed-Bold-Web.992f2cf.woff2)
      format('woff2');
  font-display: swap;
  font-weight: 400;
  font-style: normal;
}

h1 {
  font-family: 'GraphikCondensed';
  font-size: 3rem;
  line-height: 1.1;
}

What should the configuration for vite.config.mjs be? This is what I currently have...

import { defineConfig } from 'vite'
import { FontaineTransform } from 'fontaine'

export default defineConfig({
  plugins: [
    FontaineTransform.vite({
      fallbacks: ['Roboto'],
      // resolve absolute URL -> file
      resolvePath: (id) => 'file:///Library/fonts/Roboto-Regular.ttf',
    }),
  ],
})

Also, these changes were made in the /playground directory, then pnpm demo:dev was run. However, when opening the css file, the file has not been changed, and the font metrics for the override are not present. I do not see any error messages, so am wondering if I should be running another command.

🔍 Where should you find it?

readme

ℹ️ Additional context

No response

getMetricsForFamily returns null for all fonts on 0.3.0

🐛 The bug

Only reproducible on [email protected]. May have regressed with the changes in c0f6483. To reproduce, clone the repo I created below, run npm install, and then node index.js.

Invoking getMetricsForFamily returns null for any legitimate font. Only reproducible in other packages that install fontaine, but not in fontaine itself in the test environment.

🛠️ To reproduce

https://github.com/AleksandrHovhannisyan/fontaine-131

🌈 Expected behaviour

The function should not return null. It's unclear why tests pass locally, but when I tried using [email protected] locally in my project, I was getting null no matter what I tried.

ℹ️ Additional context

No response

Usage with tailwind

🆒 Your use case

Update README to show usage with tailwind.

🆕 The solution you'd like

This is how I got it to work. The module adds the fallback fonts correctly but we need to tell tailwind the fallback font family name in order to actually use it.

// tailwind.config
module.exports = {
  theme: {
    extend: {
      fontFamily: {
		// For every font, provide another string in `${fontName} fallback` format
        plex: ["IBM Plex Sans Arabic", "IBM Plex Sans Arabic fallback"],
      },
    },
  },
};

🔍 Alternatives you've considered

No response

ℹ️ Additional info

I'd make a PR if the solution sounds good

Action Required: Fix Renovate Configuration

There is an error with this repository's Renovate configuration that needs to be fixed. As a precaution, Renovate will stop PRs until it is resolved.

Error type: Cannot find preset's package (:automergeEarlyMondays). Note: this is a nested preset so please contact the preset author if you are unable to fix it yourself.

Maige Usage

Thanks for trying Maige.

Running GPT-based services is pricey. At this point, we ask you to add payment info to continue using Maige.

Add payment info

Feel free to close this issue.

CLI/programmatic usage

Any way of using this in a CLI? Maybe analyzing a URL and returning the generated font overrides? I guess you'd also need to provide the local files.

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

offer 'minimal' installation

Currently we include metrics for lots of fonts, which adds to install size. This is only in development, of course, but for users who know their font metrics it would be nice to offer a minimal option.

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Awaiting Schedule

These updates are awaiting their schedule. Click on a checkbox to get an update now.

  • chore(deps): update dependency magic-string to v0.30.11
  • chore(deps): update devdependency @types/node to v20.14.14
  • chore(deps): update devdependency bumpp to v9.4.2
  • chore(deps): update test packages to v2.0.5 (@vitest/coverage-v8, vitest)
  • chore(deps): update all linters (@typescript-eslint/eslint-plugin, @typescript-eslint/parser, eslint)
  • chore(deps): update dependency unplugin to v1.12.1
  • chore(deps): update devdependency @antfu/eslint-config to v2.25.0
  • chore(deps): update pnpm to v9.7.0
  • chore(deps): lock file maintenance

Rate-Limited

These updates are currently rate-limited. Click on a checkbox below to force their creation now.

  • chore(deps): update devdependency lint-staged to v15.2.8
  • chore(deps): update dependency @capsizecss/unpack to v2.3.0
  • chore(deps): update dependency vite to v5.4.0
  • chore(deps): update all linters to v8 (major) (@typescript-eslint/eslint-plugin, @typescript-eslint/parser)
  • 🔐 Create all rate-limited PRs at once 🔐

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

github-actions
.github/workflows/ci.yml
  • actions/checkout v4
  • actions/setup-node v4
  • actions/checkout v4
  • actions/setup-node v4
  • codecov/codecov-action v4
.github/workflows/release.yml
  • actions/checkout v4
  • actions/setup-node v4
npm
package.json
  • @capsizecss/metrics ^2.1.1
  • @capsizecss/unpack ^2.0.1
  • magic-regexp ^0.8.0
  • magic-string ^0.30.10
  • pathe ^1.1.2
  • ufo ^1.5.3
  • unplugin ^1.11.0
  • @antfu/eslint-config 2.22.3
  • @types/node 20.14.10
  • @types/serve-handler 6.1.4
  • @typescript-eslint/eslint-plugin 7.16.1
  • @typescript-eslint/parser 7.16.1
  • @vitest/coverage-v8 2.0.2
  • bumpp 9.4.1
  • eslint 9.7.0
  • eslint-config-prettier 9.1.0
  • execa 9.3.0
  • get-port-please 3.1.2
  • lint-staged 15.2.7
  • serve-handler 6.1.5
  • simple-git-hooks 2.11.1
  • typescript 5.5.4
  • vite 5.3.5
  • vitest 2.0.2
  • pnpm 9.5.0
playground/package.json
  • vite ^5.1.5

  • Check this box to trigger a request for Renovate to run again on this repository

expose as postcss plugin

For platforms supporting postcss, it would be reasonable to benefit from postcss parsing rather than the current regexp based solution (which could remain fallback)

Customize the font overridden name

🆒 Your use case

Probably in some case, we won't have that override name in the font name.

🆕 The solution you'd like

I would like to suggest to customize the overridden name.

🔍 Alternatives you've considered

I probably not understand well, so let me know if I'm wrong.

ℹ️ Additional info

No response

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.