How to build a blog with Hugo

Site Generator
How to build a blog with Hugo
Photo by 200 Degrees on Pixabay

This blog hadn’t a redesign for a decade. It was driven by self-made CMS, which naturally has become, say the least of it, outdated. So some time ago I ripened for a remastering. Instantly I started by looking for a CMS or a headless CMS. Then I realized, the blog is actually static – I don’t have here any forms or services, it’s all about representation of content. Why not to go with a site generator? With no server functionality it’s way more secure and reliable. It’s obviously faster. And how it turned out it gives you more freedom and flexibility.

I examined Jekyll, Gatsby, 11ty and Hugo. Despite my strong React background I took Go-based Hugo. Man, it is fast beyond any belief! What’s more:

What especially impressed me, Hugo allows to create custom Markdown tags. For example, I use custom img, which resizes provided resource image for every RWD break-point and generates HTML img with respectively populated srcset.

As for downsides, I had to learn some basics of Go template package to achieve what I wanted. It appeared to be quite elegant as a language though. The documentation is quite good in details, but a newcomer may run into difficulties. So here below I share my own experience of re-building my blog with Hugo.

Objectives

So what are we going to achieve? We are creating a plain blog, which has a home page with posts:

Home page

On the sidebar there is a list of available categories. Any of them leads to the filter listing:

Taxonomy filter page

Post detail page includes navigation links previous/next:

Post details page

Just as an example we have a site page “About”:

About page

Installing

The installation flow is pretty much explained in the documentation. The tool is available on macOS, Windows, Linux, OpenBSD and FreeBSD platforms. On Ubuntu I personally used snap approach:

snap install hugo --channel=extended

Note the extended, this way we obtain a version extended with Sass/SCSS support.

Getting started

Let’s create a new project named blog:

hugo new site blog

As we enter the created directory

cd blog

we can see the following directory structure:

.
├── archetypes
│   └── default.md
├── config.toml
├── content
├── data
├── layouts
├── resources
├── static
└── themes

Whereas:

  • archetypes - configuration templates for new content created with CLI tool
  • assets - any files you want to use in Hugo Pipe
  • config - in addition to using a single site config file we can make a complex environment-specific configuration structure.
  • content - Markdown files to make site content split onto section directories
  • data - besides content, we can use also pure data (YAML, JSON, or TOML) to generate site pages
  • layouts - layout templates, based on Go HTML templates
  • static - all the static content like images, CSS or JavaScript that we want access directly on site pages (without transformation)

You can also find config.toml - the root site config. Hugo supports YAML, JSON, or TOML styles. I find YAML as a cleaner data representation format. So we can replaced config.toml with config.yaml with the content:

baseURL: "http://localhost:1313/"
languageCode: en-us
title: Blog

I took localhost:1313 as default one for LiveReload For building you can set it dynamically like hugo -b http://myblog.dev

Content

Content blank can be created by using CLI. That way Hugo creates a file like content/posts/lorem-ipsum.md with prefilled meta data according to a matching archetype.

I would recommend though, manually creating content/posts/lorem-ipsum/index.md where lorem-ipsum is a slug, meaning the page will be accessably by URL /posts/lorem-ipsum. This way the page has own folder with page resources placed there.

As an example, the first content page content/posts/lorem-ipsum/index.md may look like:

---
title: "Lorem ipsum dolor sit amet"
date: 2020-03-04T13:40:25+01:00
draft: false
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam nisl mauris, dictum vel molestie nec, efficitur non arcu. "
coverImage: main.png
categories: [ "bar" ]
images: [ "/posts/lorem-ipsum/main.png" ]
---

Lorem ipsum dolor sit amet, ...

The leading block with meta data is called front matter. The block is bordered with ---, implying YAML format. Here the description of the fields:

  • title - the title of the page
  • date - creating date (using for sorting)
  • draft - can be used to toggle page as a draft
  • description - page description used as summary in the listing and SEO
  • coverImage - a custom field to specify cover image
  • categories - list of categories the page belong to
  • images - list of images for Open Graph

Similarly, we create the second page content/posts/fusce-eu-congue-dolor/index.md and about section page content/about/_index.md.

content/about/_index.md

---
title: "About"
date: 2020-03-04T13:40:25+01:00
draft: false
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam nisl mauris, dictum vel molestie nec, efficitur non arcu. "
categories: [  ]
---

Lorem ipsum dolor sit amet, consectetur adipiscing elit...

Layouts

We have decided to go with the following page types: home page, post details, about page, category filter page. Besides, we need a 404 Page Not Found one. The top level layouts can be the following:

layouts
├── 404.html
├── about
│   └── section.html
├── _default
│   └── category.html
├── index.html
└── posts
    ├── section.html
    └── single.html

Whereas:

  • layouts/index.html - home page layout
  • layouts/404.html - 404 Page Not Found layout
  • layouts/about/section.html - about page layout (index layout in about section by URL /about)
  • layouts/posts/section.html - index layout in posts section by URL /posts (here a copy of home page layout)
  • layouts/posts/single.html - post details layout by URL /posts/<slug>
  • layouts/_default/category.html - category layout by URL categories/<category>

Home page layout

Let’s start with home page:

layouts/index.html

{{ partial "header.html" . }}
<body>
  <div class="container">
    {{ partial "navbar.html" . }}
    <main class="main">
      {{ partial "posts.html" . }}
      {{ partial "sidebar.html" . }}
    </main>
    {{ partial "footer.html" . }}
  </div>
</body>
</html>

As you see here we use partial function to include sub-templates. First goes the header:

layouts/partials/header.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" >
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <meta name="description" content="{{ .Description }}">
    <link rel="canonical" href="{{ .Permalink }}">
    <title>{{ .Title }}</title>

    {{ template "_internal/opengraph.html" . }}
    {{ template "_internal/twitter_cards.html" . }}

    {{ $styles := resources.Get "css/index.css" | minify | fingerprint }}
    <link href="{{ $styles.Permalink }}" rel="stylesheet">

</head>

Here variables .Title and .Description contain the value provided by us in the front matter. Variable .Permalink contains the permanent link to the page.

Further we include internal templates Open Graph and Twitter Cards.

Next we take advantages of Hugo Pipes to minify and provide with Fingerprinting and SRI our CSS located in ./resources/css/index.css:

 {{ $styles := resources.Get "css/index.css" | minify | fingerprint }}
    <link href="{{ $styles.Permalink }}" rel="stylesheet">

You can also include to the pipe SASS preprocessing and PostCSS postprocessing.

layouts/partials/navbar.html

<header class="header">
    <nav class="main-nav">
      <a class="{{ if or (.Page.IsHome) (eq .Section "posts")  }}is-active{{ end }}" href="/" ><span>Blog</span></a>
      <a class="{{ if eq .Section "about" }}is-active{{ end }}"  href="/about" ><span>About me</span></a>
    </nav>
</header>

Here we just refer to section pages and use conditional logic to highlight the current section.

Alternatively you can use menu

layouts/partials/posts.html

<div class="posts">
{{ range .Paginator.Pages.ByDate.Reverse }}
  {{ .Render "article-card" }}
{{ end }}
{{ template "_internal/pagination.html" . }}
</div>

In order to render posts in the listing we iterate .Paginator.Pages collection. Then we sort the list by date and reverse it to have older posts on the top.

The post cards layout we keep in layouts/_default/article-card.html. That’s content view - a particular type of template designed for a repeating content.

At the end we include internal template to render the pagination.

layouts/_default/article-card.html

<article class="card__article">
  <h2 class="card__title"><a href="{{ .RelPermalink }}">{{ .Title }}</a></h2>
  <div class="card__meta">
    <time>{{ .Date.Format "January 2, 2006" }}</time>
  </div>
  <div class="summary">
    <figure>
      <img itemprop="image" src="{{ .RelPermalink }}/{{ .Params.coverImage }}" alt="{{ .Title }}" />
    </figure>
    <p>
    {{ .Description | html }}
    </p>
  </div>
</article>

In the post card layout we refer the front matter fields. While printing .Date we format it and .Description we render without sanitizing the HTML.

As for the cover image. We could simply place images in static/img and access as /img/image.jpg. But keeping them as page resources next to content file gives certain advantages. You will see it later in section “Responsive images”.

layouts/partials/sidebar.html

<aside class="sidebar">
  <article>
    <h2>Labels</h2>
    <nav class="category-list">
      {{- range $name, $taxonomy := $.Site.Taxonomies.categories }}
      {{- with $.Site.GetPage (printf "/categories/%s" $name) }}
      <a href="{{ .Permalink }}" class="category-list__li">
        {{ .Page.Title }} <span class="category-list__count">({{ $taxonomy.Count}})</span>
      </a>
      {{- end }}
      {{- end }}
    </nav>
  </article>
</aside>

Hugo supports flexible grouping of content (taxonomies). So you can categorize the content e.g. by categories, series, tags. All the content with categories list in the front matter gets indexed by Hugo. Thus we can get the full list of indexed categories from $.Site.Taxonomies.categories.

layouts/partials/footer.html

<footer class="footer">
  footer
</footer>

Post details layout

layouts/posts/single.html

{{ partial "header.html" . }}
<body>
  <div class="container">
  {{ partial "navbar.html" . }}

  <main class="main">
    <div>
      <article class="single">
        <h2 class="card__title">{{ .Title }}</h2>
        <div class="card__meta">
          <time>{{ .Date.Format "January 2, 2006" }}</time>
        </div>
        <figure>
          <img itemprop="image"
                src="{{ .RelPermalink }}/{{ .Params.coverImage }}"
                alt="{{ .Title }}" />
        </figure>
        {{ .Content }}
      </article>

      <nav class="page-nav">
       {{with .PrevInSection}}
       <a class="btn btn-default" href="{{.Permalink}}">
         Previous:
         <span>{{ .Title }}</span>
       </a>
       {{end}}
       {{with .NextInSection}}
       <a class="btn btn-default" href="{{.Permalink}}">
         Next: <span>{{ .Title }}</span>
       </a>
       {{end}}
     </nav>
    </div>
  </main>
{{ partial "footer.html" . }}
  </div>
</body>
</html>

Here we render content pretty much the same as in layouts/_default/article-card.html. What is new - usage of .PrevInSection and .NextInSection objects for sibling page navigation. If you have troubles by reading the corresponding template logic here the explanation. Imagine we have an object .Foo with property .Bar. If we want to render .Foo.Bar we do the following:

{{with .Foo }}
{{ .Bar }}
{{end}}

If .Foo exists the block renders taking in the context of the object.

About page layout

layouts/about/section.html

{{ partial "header.html" . }}
<body>
  <div class="container">
    {{ partial "navbar.html" . }}
      <main class="main">
        <div>
          {{ .Content }}
        </div>
      </main>
    {{ partial "footer.html" . }}
  </div>
</body>
</html>

Category filter layout

layouts/_default/category.html

{{ partial "header.html" . }}
<body>
  <div class="container">
    {{ partial "navbar.html" . }}
    <main class="main">
      <div class="posts">
        <h2 class="category">Items in category <b>{{ .Title | lower }}</b></h2>
        {{ range .Data.Pages }}
            {{ .Render "article-card" }}
        {{ end}}
      </div>
      {{ partial "sidebar.html" . }}
    </main>

    {{ partial "footer.html" . }}
  </div>
</body>
</html>

At the end our layout directory shall look like that:

layouts
├── 404.html
├── about
│   └── section.html
├── _default
│   ├── article-card.html
│   ├── category.html
│   └── terms.html
├── index.html
├── partials
│   ├── categories.html
│   ├── footer.html
│   ├── header.html
│   ├── navbar.html
│   ├── posts.html
│   └── sidebar.html
└── posts
    ├── section.html
    └── single.html

Extras

In order to increase the quantity and quality of traffic to our blog, let’s copy/paste RSS, sitemap and robots templates from RSS, sitemap, robots.txt.

layouts
├── ...
├── robots.txt
├── rss.xml
└── sitemap.xml

We also extend layouts/partials/header.html with following:

{{ with .OutputFormats.Get "rss" -}}
    {{ printf `<link rel="%s" type="%s" href="%s" title="%s" />` .Rel .MediaType.Type .Permalink $.Site.Title | safeHTML }}
{{ end -}}

It adds:

<link rel="alternate" type="application/rss+xml" href="https://example.com/index.xml" title="Site Title">

Building and running

Now we can start Hugo server with LiveReload:

hugo server --buildDrafts --disableFastRender

and navigate tot he generated blog http://localhost:1313/.

Home page

While the server is running we can edit contents/layouts/CSS and get the changes immediately visible on open pages.

If we want to build for deployment we go:

hugo -b https://example.com -D

Responsive images

We want to bring the best possible user experience, so we have to make our images responsive. Imagine we have content column of maximum width 680px, meaning that is maximal width of the images. However, we support Retina 2x aspect ratio. So our content source images shall be 1360px wide. We what the browser to load an image most fitting the device including 414px one on small devices.

In layouts we can use the following snippet:


{{.Scratch.Set "src" (.Resources.GetMatch (printf "*%s*" .Params.coverImage)) }}
{{ $src := .Scratch.Get "src" }}

{{ .Scratch.Set "small" ($src.Resize "414x") }}
{{ .Scratch.Set "medium" ($src.Resize "680x") }}

{{ $small := .Scratch.Get "small" }}
{{ $medium := .Scratch.Get "medium" }}

<img
  itemprop="image"
  sizes="(max-width: 414px) 414px, 680px"
  srcset="{{ $small.RelPermalink }} 414w, {{ $medium.RelPermalink }} 680w, {{ $src.RelPermalink }} 2x"
  alt="{{ .Title }}" />

Here we retrieve a resource from cover image. We take the original image (1360px) resource in $src variable. We resize image for 414px width and save the resource in $small variable. Similarly we get image version of 680px wide in $medium variable. We use links to the image versions (e.g. $small.RelPermalink) in srcset breakpoints.

That’s nice, but what about content images? Fortunately we can extend Markdown with shortcodes.

I would like to introduce a shortcode for responsive images that can be used like:

{{< img src="example.png" alt="Example" >}}

For that we create a template in shortcodes/img.html with content identical to the one we used for responsive images in layouts, except the lines:

{{.Scratch.Set "src" (.Resources.GetMatch (printf "*%s*" (.Get "src"))) }}
...
<img
  ...
  alt="{{ .Get "alt" }}" />

Instead of taking the source image URL and alt from front matter we read the value of the tag properties.

Recap

Hugo is lighting-fast. When running the server with LivEreload you cannot even notice when the re-build happening. It’s a mature static site generator. by now the developers, seems like, thought about everything. Shortcodes and Markdown customization give you unprecedented freedom. Hugo pipes allow to set up the entire development environment unprecedented Hugo. I really enjoy working with it.