Please note: this blog has been migrated to a new location at https://jakesgordon.com. All new writing will be published over there, existing content has been left here for reference, but will no longer be updated (as of Nov 2023)

Building a Website with Hugo and Netlify

Fri, Mar 27, 2020

This website has been dormant for a couple of years while I’ve been busy elsewhere, but I’m hoping to be more active in 2020. I’m resurrecting this site and slowly dipping my toes back into the tech community. I thought it might be valuable to start by writing up a little on how the site itself is made…

Table of Contents


Static Websites

Using a full-blown web application framework like Rails, Django, or Express is usually overkill for websites like this one. Far better to keep it simple and use plain HTML hosted via a basic web server. However, that doesn’t mean you have to write all of your articles in raw HTML.

There are many open source static site generators available, in fact you can find a huge list of them maintained at staticgen.com. This website happily uses Hugo.


Markdown Content

For static websites content is typically written in markdown format:

# This is a title

This is a paragraph of text filled with insightful commentary about
a really interesting subject that we hope other folks might find useful.

  * this
  * is a list
  * of bullet points

We can highlight **bold text**, as well as _italics_. We can also
include [hyperlinks](http://codeincomplete.com), as well as
embed an ![](image.png)

A summary can be extracted (e.g. for the front page) by splitting around a <!--more--> comment:

This is an introductory paragraph about the topic with a high level overview.

<!--more-->

... further details about the topic go "below the fold".

Code blocks with syntax highlighting can be included easily:

```javascript
  function hello() {
    console.log('hello world')
  }
```

The frontmatter section declares relevant attributes about this content:

---
date:  2011-01-23              # the publish date of this article
title: Hello World             # the title for this article
slug:  welcome                 # the permalink slug for this article
---

Welcome to my website...

Read more about markdown:


Extending Markdown with Hugo Shortcodes

Hugo allows you to go beyond standard markdown in your content by adding shortcodes. These are small snippets of HTML that live in the layouts/shortcodes directory and can be inserted into your content. For example, to include a custom horizontal rule you can add a hr.html shortcode:

<hr class='separator'>

Shortcodes can be used to wrap content by using the go template syntax. For example, to float content to the right you can define a float-right.html shortcode:

<div style='float:right;'>{{ .Inner | markdownify }}</div>

You can even provide a raw.html shortcode to allow raw HTML directly:

{{ .Inner }}

Shortcodes are inserted into markdown content using the {{< shortcode >}} syntax:

This is a paragraph of text, followed by a horizontal rule

{{< hr >}}

{{< float-right >}}
## This title will float to the right
{{< /float-right >}}

Finally, we can include some raw HTML...

{{< raw >}}
<table>
  <tr>
    <td>foo</td>
    <td>bar</td>
    <td>baz</td>
  </tr>
</table>
{{< /raw >}}

Read more about Hugo shortcodes


Page Layout and Partials

The easiest way to present markdown content is with a predefined Hugo Theme where you can choose from a variety of styles that suit your need. However, for this website I chose to build my own simple theme with a custom single page layout and re-usable partial HTML snippets:

layouts/
  _default/
    single.html      # the default single page layout
  partials/
    head.html        # a partial <head> snippet
    header.html      # a partial header snippet
    footer.html      # a partial footer snippet

The default single page layout is a Hugo Template that defines the overall page structure:

{{- partial "head.html" . -}}
<body>
  <div id="frame">
    {{- partial "header.html" . -}}
    <div id="content" class="{{ .Type }} {{ .Slug }}">
      {{- .Content -}}
    </div>
    {{- partial "footer.html" . -}}
  </div>
</body>
</html>

The head.html partial defines the <head> section of every page in the site, including:

{{- $favicon := resources.Get "images/favicon.ico" | fingerprint                -}}
{{- $style   := resources.Get "css/content.scss" | toCSS | minify | fingerprint -}}

<!DOCTYPE html>
<html lang="en-us">
<head>
  <meta charset="utf-8">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="env" content="{{ hugo.Environment }}">
  <meta name="deploy-date" content="{{ now.Format "2006-01-02" }}">
  <meta name="publish-date" content="{{ .Date.Format "2006-01-02" }}">
  <meta name="last-modified-date" content="{{ .Lastmod.Format "2006-01-02" }}">
  <meta name="author" content="{{ site.Author.name }}">
  <title> {{ .Title }} | {{ .Site.Title }} </title>
  <link rel="stylesheet" href="{{ $style.RelPermalink }}">
  <link rel="shortcut icon" href="{{ $favicon.RelPermalink }}">
</head>

The header.html partial defines the logo and header navigation section, including:

<div id='header'>

  {{- $brand := resources.Get "images/brand.png" | fingerprint -}}

  <div>
    <a href='/'><img class='brand' src='{{ $brand.RelPermalink }}' alt='go to home page'></img></a>
  </div>

  <div class='byline'>
    <div class='links'>
      <a href='/'>home</a> |
      <a href='/articles/'>articles</a> |
      <a href='/games/'>games</a> |
      <a href='/projects/'>projects</a> |
      <a href='/about/'>about</a>
      <span class='show-sm-up'>| <a href='/about/resume.pdf'>resume</a></span>
      <span class='contact'>
        <a href='https://www.linkedin.com/in/jakesgordon' class='link linkedin' title='Jake Gordon on LinkeIn'><i class='fab fa-linkedin'></i></a>
        <a href='https://twitter.com/jakesgordon'         class='link twitter'  title='@jakesgordon on Twitter'><i class='fab fa-twitter'></i></a>
        <a href='https://github.com/jakesgordon'          class='link github'   title='@jakesgordon on GitHub'><i class='fab fa-github'></i></a>
        <a href='mailto:jake@codeincomplete.com'          class='link email'    title='email jake@codeincomplete.com'><i class='fal fa-envelope'></i></a>
      </span>
    </div>
  </div>

</div>

Finally, the footer.html partial defines the footer shown at the bottom of every page:

<div id='footer'>

  <div class='links'>
    <a href='/'>home</a> |
    <a href='/articles/'>articles</a> |
    <a href='/games/'>games</a> |
    <a href='/projects/'>projects</a> |
    <a href='/about/'>about</a>
  </div>

  <div class='copyright'>
    &copy; 2011-{{ now.Format "2006" }} Jake Gordon
  </div>

</div>

Read more about Hugo templates


Styling with SASS

This website is fairly basic and doesn’t need a lot of styling resources, but it’s still much easier to use the SASS precompiler than write raw css directly. Luckily by installing the extended version of Hugo we get SASS precompiler support built-in with no additional work necessary!

In fact you’ve already seen where this occurs in the head.html partial:

{{- $style := resources.Get "css/content.scss" | toCSS | minify | fingerprint -}}

<!DOCTYPE html>
<html lang="en-us">
<head>
  <link rel="stylesheet" href="{{ $style.RelPermalink }}">
</head>

The $style variable is loaded from css/content.scss and passed through a 3 step Hugo Pipeline:

The {{ $style.RelPermalink }} attribute is used as the stylesheet href.

With a SASS-enabled pipeline in place we can use scss syntax in our styles, for example:

// we can use SASS variables...
#frame {
  min-width: $screen-xs;
  max-width: $screen-md;
  margin: 0 auto;
  padding: 0 2rem;
}

// we can use SASS nested styles...
#footer {
  margin: 10rem 0;
  text-align: center;
  .copyright { color: $gray-text; margin-top: 0.5rem; }
  .links { .selected { font-weight: bold; } }
}

// we can use SASS mixins...
#promo {
  @include h3;
}

Read more about Hugo’s SASS support


Site Map

Hugo will automatically generate a sitemap.xml file for your website. The site map will list all pages contained in the site along with their last modified date so that search engines will know to index them when their content changes. E.g:

  <url>
    <loc>/articles/building-a-website-with-hugo-and-netlify/</loc>
    <lastmod>2020-03-27T20:02:35-07:00</lastmod>
  </url>
  <url>
    <loc>/articles/javascript-state-machine-3-0-released/</loc>
    <lastmod>2017-06-10T17:02:56-07:00</lastmod>
  </url>
   ...

The <loc> of each article is {{ .RelPermalink }} - the root relative permalink for that article.

The <lastmod> date is a little more tricky…

You can customize the date priority order by configuring date rules. For this website I simplified the default behavior with the following custom site configuration:

frontmatter:
  lastmod: ["lastmod", ":git"]

Hugo Configuration

The primary config.yaml used for this site includes:

title: Code inComplete               # default site <title>
metaDataFormat: yaml                 # prefer yaml format for content frontmatter
enableGitInfo: true                  # infer lastmod dates from git
enableRobotsTXT: true                # use my custom robots.txt template

author:
  name: Jake Gordon                  # author name
  email: jake@codeincomplete.com     # author email

permalinks:
  article: /articles/:slug           # permalink pattern for article content
  game:    /games/:slug              # permalink pattern for game content

frontmatter:
  lastmod: ["lastmod",":git","date"] # allow explicit lastmod to take priority over git inferred value

When building for production with ENV=prod an additional prod/config.yaml is included:

# specify production baseurl when absolute URL's are required (e.g in RSS feeds)
baseurl: "https://codeincomplete.com/"

params:
  assets:
    minify: true         # enable asset minification
    fingerprint: true    # enable asset fingerprints
  social:
    enabled: true        # enable 'tweet/like this' social buttons
  disqus:
    enabled: true        # enable disqus comments in production
  analytics:
    google:
      enabled: true      # enable google analytics in production

Read more about Hugo configuration


Hugo Directory Structure

Hugo projects share a similar directory structure, mine looks something like this:

assets/       # assets (css, images) go through the hugo asset pipeline (minification, fingerprinting, etc)
config/       # hugo configuration files
content/      # website content
  about/      #   the about page
  article/    #   the blog articles
  game/       #   the games page
  projects/   #   the projects page
  _index.md   #   the home page
layouts/      # layout template files
static/       # static files that are copied as-is

Read more about the Hugo directory structure


Hugo Page Bundles

One of the slightly confusing aspects of Hugo is it’s Page Bundle metaphor requiring you to understand the difference between:

The layout template used to transform the page content will differ:

The transformation of other content (e.g. markdown files) found in the same directory will differ:

The handling of assets (e.g. images) found in the same directory will differ:

The handling of permalinks will differ:

Read more about Hugo’s page bundles.

NOTE: Personally, I think the page bundle metaphor and the single vs list page template is an unnecessary complexity for Hugo that seems caused by it’s attempts to automagically generate category/taxonomy pages for you - a feature that seems to cause more problems than the value it provides. However, despite this complexity Hugo is still a fantastic tool!


Development Tasks with Make

We only need 2 commands for day-to-day development with Hugo:

We can use a simple Makefile to encapsulate those 2 tasks:

ENV  ?= dev
PORT ?= 3000

run:
  hugo server --watch --environment $(ENV) --config config/config.yaml --port=$(PORT)

build:
  hugo --environment $(ENV) --config config/config.yaml --cleanDestinationDir

Allowing us to easily make run the development server, or ENV=prod make build the final output.


Hosting with Netlify

Once the content is written, the layouts built, and the website is in working order we need to be able to host it somewhere. Previous options include Github Pages, an S3 bucket, or a cheap Linode or Digital Ocean virtual server.

Luckily there is a fantastic option available with Netlify

The basic setup process was very easy…

That was all that was needed to build and deploy the site and make it available within a few minutes at https://codeincomplete.netlify.com. Once the site was deployed, routing DNS to the new location was as simple as replacing my previous A record with a new ALIAS record within my DNS provider (currently I use NameCheap).

ALIAS   codeincomplete    codeincomplete.netlify.com

Once DNS was configured correctly I clicked the Netlify “Provision Let’s Encrypt SSL/TLS Certificate” button, waited for a few minutes for the provisioning to complete, and the newly deployed website was available and secure at https://codeincomplete.com.

Finally, to improve performance further, since I am using the Hugo pipeline to fingerprint my static css and image assets, I added a _headers file to tell Netlify to set far-future cache control headers on my css and images folders:

# far-future for fingerprinted css assets
/css/*
  cache-control: max-age=31536000

# far-future for fingerprinted image assets
/images/*
  cache-control: max-age=31536000

It was a surprisingly simple and smooth process.

Great job Netlify!


Conclusion

Building Websites with Hugo - Brian P. Hogan

This site has been dormant for a while, but the underlying choice to build it as a static website has made it relatively easy to maintain and update over the years. I’m fairly happy that the choice - made many years ago - to use Hugo has held up and continues to be maintained as an active and constantly improving tool.

If you’re interested in learning more about Hugo I highly recommend Brian Hogans new book Building Websites with Hugo

I’m also delighted to recently discover Netlify as a great hosting solution and to finally turn off my Linode virtual machine that has been quietly running without problems for over 5 years - you served us well and will be missed!