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.
And here is the same post with custom fonts delivered with FontFaceObserver.
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 useswap
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.