As much as a Hugo theme’s aesthetics meets our requirement, sometimes the default fonts do not fit our need. For example, the Lato font that comes with the Gokarna theme doesn’t support Japanese. As a result, the browser falls back to the system serif font for any Japanese character. This makes the viewing experience very jarring and unfriendly.

Being able to use our own fonts gives us the freedom to express and fulfill our design aspiration. In this post, I’m going to show how you can use your own fonts in Hugo, both via embedding a CDN link or hosting it as a static local font.

Use web fonts via CDN

There are many places that serve free fonts via CDN. For example:

In this post, let’s use Google Fonts for simplicity.

  1. Go to Google Fonts. For example, I search for “Japanese” in the Search fonts box to find a font that supports Japanese.
  2. In the font page , scroll down to Styles section. Type your typical text to preview aesthetics and whether the font supports your desired language. Note the desired styles/weights (e.g., Regular 400, Bold 700).
  3. If everything looks good, click Get font button on the upper right.
  4. Click Get embed code.
  5. Copy the code from Embed code in the of your html. For example:
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100..900&display=swap" rel="stylesheet">
    

    The preconnect tags help speed up the connection and are highly recommended. The display=swap parameter is a best practice for performance.

  1. Locate the base HTML file for your Hugo site. This is typically found in:

    • layouts/partials/head.html
    • layouts/_default/baseof.html

    In Gokarna theme, baseof.html loads the head.html partial, so I will use head.html.

  2. Paste the Google Fonts link tags to head.html. The Google Fonts link must come before the link to your main.css file. For example:

    <head>
     <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100..900&display=swap" rel="stylesheet">
    
     <link rel="stylesheet" href="{{ 'css/main.css' | relURL }}"> 
    </head>
    

    If your main.css is loaded before the Google Fonts link, your browser won’t know what “Noto Sans JP” is yet, and it will fall back to the generic serif font.

    When you include the link in your HTML, Google Fonts automatically loads a stylesheet that defines the “Noto Sans JP” font-family for the browser. Specifically, it generates and serves the appropriate CSS with all the necessary @font-face declarations, including:

    • Proper font-family name
    • All requested weights/styles
    • Multiple font formats (woff2, woff, etc.)
    • font-display: swap
    • Cross-browser compatibility

    You can verify this by visiting the Google Fonts URL directly in your browser. You’ll see the generated CSS with @font-face rules.

Use font in CSS

Finally, you need to instruct your blog’s CSS to use the font.

  1. Determine where your primary CSS files are located (e.g., in your theme’s static/css/ or assets/css/ directory). To override it safely, copy the file from the theme’s directory to your site’s root assets/css/ directory.

  2. Edit the relevant CSS file to apply the new font to your desired elements (e.g., body, h1, p, etc.) using the font-family property. The font name must match what Google Fonts provided.

    On Google Fonts Embed code page, refer to the syntax in Noto Sans JP: CSS class for a variable style box. Use it as indicated in your css file. For example

    .intro_text {
         font-family: "Noto Sans JP", sans-serif;
         font-weight: 700;  
         font-style: normal;
     } 
    

    When the browser serves this element, it will

    1. Download the Google Fonts CSS (which contains @font-face)
    2. Load the actual font files (indicated by @font-face) from Google’s servers
    3. Apply your specified font-family, font-weight, and font-style
  3. Save your changes and run hugo server to view the updated fonts. If it doesn’t work, see troubleshooting section .

As you can see, this is a very slick process. You just need to tweak the <link> tag in your HTML and the font-family in your CSS to load a font served via CDN.

Use self-hosted fonts

Unlike using fonts via CDN, you need to manually create the correct @font-face declaration in main.css or font.css when you

  • Self-host fonts (you downloaded font files to your server)
  • Use custom fonts not available on Google Fonts or other CDN providers
  • Need offline support or want to avoid external dependencies
  • Have specific performance requirements (preloading, subsetting, etc.)

Here’s the complete workflow:

  1. Download font files (preferably WOFF2 + WOFF)
  2. Upload to blog’s fonts/ directory
  3. Use Transfonter to verify font metadata
  4. Create @font-face declarations matching the metadata
  5. Test in browser DevTools → Inspector/Elements -> Computed panel

Determine @font-face values

  1. Get font metadata from the downloaded files with Transfonter . Upload the file and it generates ready-to-use @font-face code.

  2. Identify Key Properties

PropertyWhere to Find ItExample
font-familyFont name in metadata"Noto Sans JP"
font-weightWeight value (100-900)700
font-styleUsually normal or italicnormal
font-stretchIf applicable (rare)normal
  1. Add @font-face to CSS

    Add this block for each font weight and style to font.css

    @font-face {
      font-family: 'Your Font Name';
      font-style: normal; /* or italic */
      font-weight: 400;   /* or 100, 200, 300, etc. */
      font-display: swap;
      src: url('./fonts/your-font-file.woff2') format('woff2'),
          url('./fonts/your-font-file.woff') format('woff');
    }
    

    For example, if you downloaded Fraunces Regular (weight 400):

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

    For Fraunces Bold (weight 700):

    @font-face {
      font-family: 'Fraunces';
      font-style: normal;
      font-weight: 700;
      font-display: swap;
      src: url('./fonts/Fraunces-Bold.woff2') format('woff2'),
          url('./fonts/Fraunces-Bold.woff') format('woff');
    }
    
  2. For Variable fonts

    A variable font is a single font file that contains multiple styles and weights of a typeface. This means that instead of having separate files for each weight or style, all variations can be accessed through a single file, allowing for a continuous range of design variations.

    Here’s an example of a variable font declaration:

    @font-face {
      font-family: 'Fraunces';
      font-style: normal;
      font-weight: 100 900; /* Range of supported weights */
      font-display: swap;
      src: url('./fonts/Fraunces-VariableFont_opsz,wght.woff2') format('woff2-variations');
    }
    

Best practices

Include multiple formats (for older browsers)

src: url('./fonts/font.woff2') format('woff2'),
     url('./fonts/font.woff') format('woff'),
     url('./fonts/font.ttf') format('truetype');

Use local() to avoid re-downloading

src: local('Fraunces Regular'),
     url('./fonts/Fraunces-Regular.woff2') format('woff2');

Organize by weight/style

Create separate @font-face rules for each weight/style combination you need.

Troubleshooting

Sometimes after we’ve setup CDN link or local font and specified an HTML element to use the font, the browser doesn’t render it as expected. This is usually a timing issue where your main.css is being processed before the Google Font’s stylesheet has fully loaded and defined the font on the page.

Here is the step-by-step troubleshooting guide:

  1. Verify CSS loading order

    In your Hugo header.html (or wherever your links are), the Google Fonts link must come before the link to your main.css file. If your main.css is loaded before the Google Fonts link, your browser won’t know what “Noto Sans JP” (or whatever the font is) is yet, and it will fall back to the generic serif.

  2. Check for theme overrides

    If you are using a theme, another stylesheet from the theme might be loaded after your main.css and is overriding your font-family declaration with a different one.

    To verify, use your browser’s Developer Tools (F12 or right-click → Inspect, then go to the Elements tab and click on the the HTML element to inspect).

    1. Inspect the HTML element.
    2. Look at the Styles panel and see which CSS rules are being applied to the font-family.
    3. If you see your CSS with the right font, but it has a line struck through it, it means a more specific or later-loaded rule is overriding it.

    Solution: Increase the specificity of your CSS rule, or ensure your CSS is the very last one loaded. You can try adding an ID to a parent element or using !important (e.g., font-family: "Noto Sans JP", serif !important;).

  3. Network request

    In your browser’s Developer Tools, check the Network tab. Refresh the page and look for the request to https://fonts.googleapis.com/css2?... and the subsequent font file downloads (.woff2). They should all return an HTTP status code of 200. If you do not see them, or instead see a 404 or 403 error, the font isn’t being loaded.

  4. Verify if there is any rule (e.g., if clauses) in your HTML that turns off loading the fonts.