Hugo Theme From Scratch

M2 Hugo Logo.

A Hugo Theme From Scratch

As I am learning to use Hugo, “The world’s fastest framework for building websites,” I wanted to build a theme from scratch. I found a helpful site at * https://retrolog.io/blog/creating-a-hugo-theme-from-scratch * that is used to form this content.

First, let’s create a new Hugo site and then change into the directory:"

$ hugo new site test-hugo
$cd test-hugo 

Next, create a new theme:

$hugo new theme m2theme

Set the new theme in the config.toml config file:

baseURL ="http://example.org/"
languageCode = "en-us"
title = "My New HugoSite"
theme = "m2theme"

Start the server to check for errors in the web browser dev console

$hugo server -D

Check your we browser’s developer console as it should have no errors after browsing to http://localhost:1313/. For example, in Google Chrome, press Command+Option+C (Mac) or Control+Shift+C (Windows, Linux, Chrome OS).

Open /themes/m2theme/layouts/partials/head.html in a text editor and paste the following:

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" href="/css/bootstrap.min.css">
    <link rel="stylesheet" type="text/css" href="/css/style.css">
    {{ $title := print .Site.Title " | " .Title }}
    {{ if .IsHome }}{{ $title = .Site.Title }}{{ end }}
    <title>{{ $title }}</title>
</head>

Go to /themes/m2theme/static/css and create a file called style.css, we will place our custom CSS here later on.

Go to https://getbootstrap.com/ and download the latest version of Bootstrap (v5.0.2 at the time of writing). Extract the ZIP file and copy bootstrap.min.css to /themes/m2theme/static/css.

In addition to Bootstrap, we are going to use Feather icons to display icons on our site Go to https://github.com/feathericons/feather#client-side-javascript and download feather.min.js into /themes/m2theme/static/js

Create a file /themes/m2theme/layouts/partials/script.html and paste the following:

<script src="/js/feather.min.js"></script>
<script>
  feather.replace()
</script>

Now, let’s add a nav to the header so that we can navigate between the different sections of our site Open /themes/m2theme/layouts/partials/header.html and paste the following:

<div id="nav-border" class="container">
    <nav id="nav" class="nav justify-content-center">
        {{ range .Site.Menus.main }}
        <a class="nav-link" href="{{ .URL }}">
        {{ if .Pre }}
        {{ $icon := printf "<i data-feather=\"%s\"></i> " .Pre | safeHTML }}
        {{ $icon }}
        {{ end }}
        {{ $text := print .Name | safeHTML }}
        {{ $text }}
        </a>
        {{ end }}
    </nav>
</div>

For the footer let’s just add a basic copyright disclaimer

Let’s create one more partial to display metadata about each post e.g. date and tags Create a new file /themes/m2theme/layouts/partials/metadata.html:

{{ $dateTime := .PublishDate.Format "2006-01-02" }}
{{ $dateFormat := .Site.Params.dateFormat | default "Jan 2, 2006" }}
<i data-feather="calendar"></i>
<time datetime="{{ $dateTime }}">{{ .PublishDate.Format $dateFormat }}</time>
{{ with .Params.tags }}
  <i data-feather="tag"></i>
  {{ range . }}
    {{ $href := print (absURL "tags/") (urlize .) }}
    <a class="btn btn-sm btn-outline-dark tag-btn" href="{{ $href }}">{{ . }}</a>
  {{ end }}
{{ end }}

We are using Feather icons to spice things up a little, but in fact we are just rendering the publication date and the tags. Tags will be passed in as parameters from the post itself in the front matter, more information here:

---
author: "John Doe"
title: "My First Post"
date: "2006-02-01"
tags: ["foo", "bar"]
---

Open /themes/m2theme/layouts/_default/baseof.html, it should look like this:

<!DOCTYPE html>
<html>
    {{- partial "head.html" . -}}
    <body>
        {{- partial "header.html" . -}}
        <div id="content">
        {{- block "main" . }}{{- end }}
        </div>
        {{- partial "footer.html" . -}}
    </body>
</html>

Open /themes/m2theme/layouts/_default/list.html. We will define the main section of the baseof layout like this:

{{ define "main" }}
  <h1>{{ .Title }}</h1>
  {{ range .Pages.ByPublishDate.Reverse }}
  <p>
      <h3><a class="title" href="{{ .RelPermalink }}">{{ .Title }}</a></h3>
      {{ partial "metadata.html" . }}
      <a class="summary" href="{{ .RelPermalink }}">
          <p>{{ .Summary }}</p>
      </a>
  </p>
  {{ end }}
{{ end }}

The next layout artifact will be used to display a single post. Open /themes/m2theme/layouts/_default/single.html. Similarly to what we did above, let’s define the main section:

{{ define "main" }}
  <h1>{{ .Title }}</h1>
  {{ partial "metadata.html" . }}
  <br><br>
  {{ .Content }}
{{ end }}

Open /themes/m2theme/layouts/index.html in a text editor. With Bootstrap in place, we can now use the Jumbotron component to render a hero section. Paste the following:

{{ define "main" }}
<div id="home-jumbotron" class="jumbotron text-center">
  <h1 class="title">{{ .Site.Title }}</h1>
</div>
{{ end }}

Open example/config.toml and we’ll also need a menu to navigate to the different sections, let’s add it:

[menu]
  [[menu.main]]
    name = "Home"
    pre = "home"
    url = "/"
    weight = 1
  [[menu.main]]
    name = "Posts"
    pre = "pen-tool"
    url = "/posts/"
    weight = 2
  [[menu.main]]
    name = "Tags"
    pre = "tag"
    url = "/tags/"
    weight = 3

Next, we will write your first post.

Open a terminal and use the following command from the root of your site to create a post

hugo new posts/my-first-post.md

Open the newly created example/content/posts/my-first-post.md Add some tags to the front matter

tags: ["hugo", "post"]

Here is the full front matter:

---
title: "My First Post"
date: 2021-07-15T12:24:05-04:00
draft: true
tags: ["hugo", "post"]
---

Add some content under the front matter:

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor 
incididunt ut labore et dolore magna aliqua. Pellentesque eu tincidunt tortor 
aliquam nulla facilisi cras fermentum odio. A erat nam at lectus urna duis. 
Sed velit dignissim sodales ut eu sem. Lectus urna duis convallis convallis 
tellus. Diam sit amet nisl suscipit adipiscing bibendum est. Sed felis eget 
velit aliquet sagittis id consectetur. Vulputate dignissim suspendisse in est 
ante in nibh mauris cursus. Morbi quis commodo odio aenean. Mollis nunc sed id 
semper risus in hendrerit gravida rutrum.

Ac ut consequat semper viverra nam. Hac habitasse platea dictumst vestibulum 
rhoncus. Amet porttitor eget dolor morbi non. Justo eget magna fermentum 
iaculis eu non. Id eu nisl nunc mi ipsum faucibus vitae aliquet nec. Aliquam 
id diam maecenas ultricies. Non sodales neque sodales ut etiam. Amet massa 
vitae tortor condimentum lacinia quis. Erat imperdiet sed euismod nisi porta. 
Nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Viverra 
suspendisse potenti nullam ac. Tincidunt id aliquet risus feugiat in. Varius 
quam quisque id diam vel. Egestas erat imperdiet sed euismod nisi. Scelerisque 
felis imperdiet proin fermentum leo vel orci porta non. Ut faucibus pulvinar 
elementum integer. Fermentum odio eu feugiat pretium nibh ipsum consequat nisl.

Hugo automatically takes the first 70 words of your content as its summary and stores it into the .Summary variable

The final result should look similar to this:

---
title: "My First Post"
date: 2021-07-15T12:24:05-04:00
draft: true
tags: ["hugo", "post"]
---

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor 
incididunt ut labore et dolore magna aliqua. Pellentesque eu tincidunt tortor 
aliquam nulla facilisi cras fermentum odio. A erat nam at lectus urna duis. 
Sed velit dignissim sodales ut eu sem. Lectus urna duis convallis convallis 
tellus. Diam sit amet nisl suscipit adipiscing bibendum est. Sed felis eget 
velit aliquet sagittis id consectetur. Vulputate dignissim suspendisse in est 
ante in nibh mauris cursus. Morbi quis commodo odio aenean. Mollis nunc sed id 
semper risus in hendrerit gravida rutrum.

Ac ut consequat semper viverra nam. Hac habitasse platea dictumst vestibulum 
rhoncus. Amet porttitor eget dolor morbi non. Justo eget magna fermentum 
iaculis eu non. Id eu nisl nunc mi ipsum faucibus vitae aliquet nec. Aliquam 
id diam maecenas ultricies. Non sodales neque sodales ut etiam. Amet massa 
vitae tortor condimentum lacinia quis. Erat imperdiet sed euismod nisi porta. 
Nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Viverra 
suspendisse potenti nullam ac. Tincidunt id aliquet risus feugiat in. Varius 
quam quisque id diam vel. Egestas erat imperdiet sed euismod nisi. Scelerisque 
felis imperdiet proin fermentum leo vel orci porta non. Ut faucibus pulvinar 
elementum integer. Fermentum odio eu feugiat pretium nibh ipsum consequat nisl.

After following the helpful article at https://retrolog.io/blog/creating-a-hugo-theme-from-scratch I noted some 404 errors in the browser console log deal with the CSS and JS minified files. To correct this I used the CDN URLS to replace the local references. Here are the updates:

In the /themes/m2theme/layouts/partials/head.html file, note that CDN URL has replaced the local URL:

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <link rel="stylesheet" type="text/css" href="/css/style.css">
    {{ $title := print .Site.Title " | " .Title }}
    {{ if .IsHome }}{{ $title = .Site.Title }}{{ end }}
    <title>{{ $title }}</title>
</head>

In the /themes/m2theme/layouts/partials/script.html file, note that CDN URL has replaced the local URL:

<script src="https://cdnjs.cloudflare.com/ajax/libs/feather-icons/4.28.0/feather.min.js"></script>
<script>
  feather.replace()
</script>

After the above steps, here is the final result: Custom Theme Result.