How to include fonts in 2021? (without CDN)

A post from Jan 2016 asked the following simple question: ‘‘How does one include fonts in a project so that they still load when the internet is down?’’

TL;DR Serve relevant stylesheets and font files from /public. NOT copying stuff manually if possible.

I have had a similar issue with the Roboto fonts in a project since 2018. Initially I tried <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500"> but realized the user experience was quite bad when the font would take a long time to load (or not load at all indeed after waiting what seemed to be an eternity).

Then I tried to bundle the fonts included in the typeface-roboto npm module using runisland:static-assets, last updated 4 years ago (which is a fork of mys:fonts, last updated 4.5 years ago, that was recommended in the post linked above). One needs to meteor npm install --save typeface-roboto, then add a static-assets.json file at the project root with the following contents:

{
    "extensions": [
        "woff",
        "woff2"
    ],
    "map": {
        "node_modules/typeface-roboto/files/": "files/"
    }
}

However, I failed to make it work in development. Worked in production though. Probably because I was doing import 'typeface-roboto'; in /client/main.js instead of serving typeface-roboto/index.css from /public. Probably would have been fixed by properly setting <base href="/"> in <head>. Anyway…

Because I was tired of fonts not loading in development, I went back to a CDN solution using webfontloader (a solution reported here). This solution should give you more flexibility in terms of asynchronicity and error handling than the pure <link> solution. I was, however, too lazy to experiment with it. The solution is rather simple: meteor npm install --save webfontloader then add

import WebFont from 'webfontloader';

WebFont.load({
  google: {
    families: ['Roboto:300,400,500']
  }
});

to /client/main.js.

Finally, today I managed to get a fully working local serving solution both in development and production. I did what everybody recommends: copy files to the /public directory. Except I do it automatically on npm’s postinstall hook with the postinstall npm package, last updated 1.2 years ago. This relieves me from the frustration some people have expressed before regarding manually copying fonts from /node_modules to /public. The solution is similar to the one with runisland:static-assets but allows to also bundle typeface-roboto/index.css in /public. If I understand correctly this is not possible with runisland:static-assets since it registers itself as compiler for certain file extensions which should not overlap with the ones handled by meteor (js, css, etc.). Here is the relevant package.json configuration:

{
  "dependencies": {
    "typeface-roboto": "^1.1.13"
  },
  "devDependencies": {
    "postinstall": "^0.7.0"
  },
  "postinstall": {
    "typeface-roboto/index.css": "copy public/typeface-roboto/",
    "typeface-roboto/files/roboto-latin-*.woff": "copy public/typeface-roboto/files/",
    "typeface-roboto/files/roboto-latin-*.woff2": "copy public/typeface-roboto/files/"
  },
  "scripts": {
    "postinstall": "postinstall"
  }
}

The same solution can be used to bundle the worker JavaScript source file of pdfjs-dist for instance. Correctly referencing this file is a problem that frequently appears in their issue tracker.
PS: Note that typeface-roboto is deprecated in favor of @fontsource/roboto.

How do you serve fonts in your project?

I used to do it exactly the same way (postinstall copy to /public), but ended up copying them manually from time to time to reduce unwanted side-effects on updates.