Customizing fonts for Hugo and Hermit

The Hermit theme uses a pretty big list of system-delivered fonts for body text and headlines (the same font is used for both).

Font selection

I like the visual differentiation of separate headline and copy fonts. Many sites go with the trend of a sans-serif font everywhere. While it is visually consistent, it’s also visually boring (to me).

Call me a traditionalist, but I’m used for body text to be in a serif font for easier letter recognition and readability. To maintain a similar visual style, the headline and code fonts are in the same family.

Finally, I wanted fonts that could be hosted with the rest of the site. Most foundries allow self-hosting fonts, but have a pay-per-impression model (Monotype) and require safeguards to prevent people from downloading the font from the site. To avoid those pitfalls, I turned to open-source fonts.

We’re running Fira Sans for headlines, Source Serif Pro for body copy, and Fira Code which is an extended version of Fira Mono that adds ligatures.

  • Fira Sans
  • Source Serif Pro
  • Fira Code

Customization in Hermit

Each font comes with multiple weights (light, regular, black, bold, italic, and so on) and different formats (eot, otf, ttf, woff, and woff2). For example, Fira Code comes in a Regular weight in five formats:

  • FiraCode-Regular.eot
  • FiraCode-Regular.otf
  • FiraCode-Regular.ttf
  • FiraCode-Regular.woff
  • FiraCode-Regular.woff2

I added all of these into my Hugo static folder static/fonts/. It doesn’t matter that we add all of these fonts to the folder. Later, we will download only the ones necessary for the web page.

I added a file layouts/partials/fonts.css to define how the fonts should look to CSS. An example looks like this:

@font-face{
    font-family: 'Source Serif Pro';
    font-weight: 200;
    font-style: normal;
    font-stretch: normal;
    font-display: swap;
    src: url('{{ .Site.BaseURL }}fonts/SourceSerifPro-ExtraLight.ttf.woff2') format('woff2'),
         url('{{ .Site.BaseURL }}fonts/SourceSerifPro-ExtraLight.otf.woff') format('woff'),
         url('{{ .Site.BaseURL }}fonts/SourceSerifPro-ExtraLight.otf') format('opentype'),
         url('{{ .Site.BaseURL }}fonts/SourceSerifPro-ExtraLight.ttf') format('truetype');
}

The two most important things to notice are the font-display: swap; descriptor and the order the font files are listed.

When font-display: swap; is used, this allows the browser infinite time to swap its default font to our requested one. This is needed depending on the client’s network speed. It will be used later with FontFaceObserver.

The browser will try to load fonts in the order shown in the src descriptor. The preferred usage depending on operating system and browser requirements are: WOFF2, WOFF, OpenType, then TrueType. WOFF2 is essentially the same as WOFF but with a better compression algorithm (Brotli versus zlib).

I added a file layouts/partials/extra-head.html to add the previous fonts.css partial:

<style type="text/css">{{ partial "fonts.css" . | safeCSS }}</style>

Hermit uses SCSS and compiles it during site build into the browser-served CSS. I copied the theme’s _predefined.css from assets/scss into the site’s top-level assets folder. Then added the highlighted lines to the fonts section:

// Fonts
//
$fonts: "Trebuchet MS", Verdana, "Verdana Ref", "Segoe UI", Candara, "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Tahoma, sans-serif;
$code-fonts: Consolas, "Andale Mono WT", "Andale Mono", Menlo, Monaco, "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", "Courier New", Courier, "YaHei Consolas Hybrid", monospace, "Segoe UI Emoji", "PingFang SC", "Microsoft YaHei";
$loaded-fonts: "Source Serif Pro"; // Hg
$loaded-code-fonts: "Fira Code"; // Hg
$loaded-heading-fonts: "Fira Sans"; // Hg

Finally, I customized assets/scss/style.css to override the fonts with my custom ones when FontFaceObserver activates. The two main sections are shown here. The .fonts-loaded class is set by FontFaceObserver when the custom fonts are loaded.

/* hg */
.fonts-loaded body,
.fonts-loaded button,
.fonts-loaded input,
.fonts-loaded select,
.fonts-loaded textarea {
  font-family: $loaded-fonts;
  -moz-font-feature-settings:    "kern" 1;
  -ms-font-feature-settings:     "kern" 1;
  -o-font-feature-settings:      "kern" 1;
  -webkit-font-feature-settings: "kern" 1;
  font-feature-settings:         "kern" 1;
  font-kerning: normal;
}
/* /hg */
/* hg */
.fonts-loaded pre,
.fonts-loaded code,
.fonts-loaded pre tt {
  font-family: $loaded-code-fonts;
}
h1,
h2,
h3,
h4,
h5,
h6 {
  font-family: $fonts;
}
.fonts-loaded h1,
.fonts-loaded h2,
.fonts-loaded h3,
.fonts-loaded h4,
.fonts-loaded h5,
.fonts-loaded h6,
.fonts-loaded #site-footer,
.fonts-loaded .post-meta,
.fonts-loaded .post-nav,
.fonts-loaded .post-info,
.fonts-loaded #site-header,
.fonts-loaded #mobile-menu,
.fonts-loaded #spotlight,
.fonts-loaded .posts-group {
  font-family: $loaded-heading-fonts;
}
/* /hg */

Font delivery

Now, we can turn our attention to getting our custom fonts on the page. Several ways exist to do this, but the best way I’ve seen is FontFaceObserver (FFO).

The quick and dirty method, once the above is done, is to add fontfaceobserver.js to your web page, then JavaScript similar to the below. This is the code that loads the above fonts for this site.

The fonts-loaded class matches what we used in the SCSS above. When fonts-loaded is missing, the normal Hermit theme fonts are used. As the FFO code loads the font and applies the fonts-loaded style, the browser replaces the Hermit font with the custom one. You’ll notice the font “shifts” from the Hermit one to the custom one, depending on your Internet and browser rendering speed.

var fontData = {
  'Source Serif Pro': { weight: 400, style: 'normal' },
  'Source Serif Pro': { weight: 400, style: 'italic' },
  'Source Serif Pro': { weight: 700, style: 'normal' },
  'Source Serif Pro': { weight: 700, style: 'italic' },
  'Fira Sans': { weight: 400 },
  'Fira Sans': { weight: 700 },
  'Fira Code': { weight: 400, style: 'normal'  }
};

var observers = [];
Object.keys(fontData).forEach(function(family) {
  var data = fontData[family];
  var obs = new FontFaceObserver(family, data);
  observers.push(obs.load());
});

Promise.all(observers)
  .then(function(fonts) {
    fonts.forEach(function(font) {
      console.log(font.family + ' ' + font.weight + ' ' + 'loaded');
      document.documentElement.className += " fonts-loaded";
    });
  })
  .catch(function(err) {
    console.warn('Some critical font are not available:', err);
  });

Here is the post with Hermit’s default fonts.

Post with Hermit’s default fonts

And here is the same post with custom fonts delivered with FontFaceObserver.

Post with custom fonts

Video

Here is a video of this page showing the font download and swap-in using FontFaceObserver. Recorded on macOS Catalina 10.15.3 and Safari 13.0.5. It’s worth noting that Microsoft Edge version 80.0.361.48 did not exhibit any visible swap-in period. The page loaded immediately with the custom fonts after the initial download. Update 11 Feb 2020: On my laptop, Edge did not exhibit the font shift. On deploying to the web, the font shift is visible in both Safari and Edge.

Resources

Here are the sites I used while learning about this customization.

  • font-display at MDN: Mozilla Developer Network information on using the font-display CSS property. We use swap to allow our custom fonts to be swapped in after downloading.
  • FontFaceObserver (GitHub link): This is the JavaScript that actually loads the fonts from the server to the browser, and adds the fonts-loaded class when the font is loaded.
  • How We Load Web Fonts Progressively: This is the article that first introducted me to the challenges of loading custom fonts without disrupting the user experience. It goes into the issue of “FOIT” (flash of invisible text), various remedies, and introduced me to FontFaceObserver.
  • Serving Web Fonts at Professional Web Typography: Professional Web Typography is an excellent web book going into selecting fonts for web use and how to use them in your web site.
  • Using @font-face: How @font-face works, browser support, and descriptions of the different font formats.