Building a Website with Hugo and Netlify
Fri, Mar 27, 2020This 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
- Markdown Content
- Extending Markdown with Hugo Shortcodes
- Page Layout and Partials
- Styling with SASS
- Site Map
- Hugo Configuration
- Hugo Directory Structure
- Hugo Page Bundles
- Development Tasks with Make
- Hosting with Netlify
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.
- Write article content in markdown format
- Build layout(s) with a template engine
- Use a static site generator to combine them into the final static output
- Deploy static files to a simple web server
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.
- Blazing fast
- Markdown format customizable with shortcodes
- Clear separation between content and layouts
- Flexible website navigation scheme
- Built in SASS style support
- Single executable dependency
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:
- Daring Fireball: Markdown, John Gruber (Creator of Markdown)
- Markdown Cheatsheet, Adam Pritchard
- Markdown Tutorial (Interactive), Garen Torikian
- The Markdown Guide, Matt Cone
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:
<meta>
tags forenv
,author
,deploy-date
,publish-date
andlast-modified-date
- a
<title>
tag provided by the.Title
attribute from the article’s frontmatter - a
<link>
tag for the css resource that is minified and fingerprinted by the hugo asset pipeline - a
<link>
tag for the favicon resource that is fingerprinted by the hugo asset pipeline
{{- $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:
- a brand logo fingerprinted by the hugo asset pipeline
- primary navigation links
<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'>
© 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:
toCSS
- runs through the SASS precompilerminify
- minifies the resulting css sourcefingerprint
- adds a cache-busting fingerprint to the output file location
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…
- it can be inferred from
git
ifenableGitInfo: true
is set in your site configuration - it can be specified explicitly with
lastmod: <date>
in the article frontmatter - it can fallback to a
date
orpublishDate
if found in the article frontmatter
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"]
- an explicit
lastmod: <date>
in frontmatter always takes priority - otherwise use git to infer the last time the content file was modified
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:
- a Leaf Bundle - which contains an
index.md
file - a Branch Bundle - which contains an
_index.md
file
The layout template used to transform the page content will differ:
- Leaf Bundle - expected to have no children and transformed using a single page template
- Branch Bundle - expected to have child bundles and transformed using a list template
The transformation of other content (e.g. markdown files) found in the same directory will differ:
- Leaf Bundle - no further content transformation will occur in that directory
- Branch Bundle - additional content will continue to be transformed
The handling of assets (e.g. images) found in the same directory will differ:
- Leaf Bundle - all assets in that directory, or any sub directory, will be copied
- Branch Bundle - only assets found in that directory will be copied
The handling of permalinks will differ:
- Leaf Bundle - will respect any
permalinks
configuration - Branch Bundle - do not respect
permalinks
config, so may require a customurl
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:
- run - use
hugo server --watch
to rebuild and reload when content changes - build - use
hugo
to build final production output
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
- Designed for hosting static sites
- Native Hugo support
- HTTP/2 support
- Global CDN
- Free tier
- Easy to get started
- Automatic ETag cache invalidation
- Automatic Gzip support
- Support for far-future Cache control headers
- Easy preview or staging deploys from git branches
- Continuous delivery via github commits
- Analytics available (in paid tier)
The basic setup process was very easy…
- Register for a new account
- Create a new site from the github repository
- Create a new deploy
- Set the build command to
hugo --environment prod --config config/config.yaml
- Set the publish directory to
public/prod
- Set a
HUGO_VERSION=0.67.1
environment variable
- Set the build command to
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
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!