Creating a Hugo Theme

But why ?

Why would I create a Hugo theme ?
Well, I wanted to blog using Hugo, I found the iris theme and I liked it. But I didn’t understand how to customize it to my needs. Therefore, I decided to create my own theme to learn how Hugo works and make (or at least try) to make a nice blog.

Let’s start

I started by creating a new Hugo site with the Hugo CLI. Then I created a new theme with the Hugo CLI. The hardest thing is to pick a name for the theme, I chose Deutalion because it sounds cool.

zsh
1
2
3
hugo new site themeCreation
cd themeCreation
hugo new theme deutalion

How does a theme work ?

You have the following files and folders in a theme:

text
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
.
├── LICENSE
├── archetypes
│   └── default.md
├── layouts
│   ├── 404.html
│   ├── _default
│   │   ├── baseof.html
│   │   ├── list.html
│   │   └── single.html
│   ├── index.html
│   └── partials
│       ├── footer.html
│       ├── head.html
│       └── header.html
├── static
│   ├── css
│   └── js
└── theme.toml

The types of page

There are three main types of page:

  • The homepage
  • The list of all your posts
  • A single post

For each of these you’ll have 1 .md file with the content and one template file where you’ll put the HTML code to display the content. Here are the path of the .md files and the corresponding template files:

text
1
2
3
4
content/_index.md             -> layouts/index.html
content/posts/_index.md       -> layouts/_default/list.html
content/posts/hugo-theme.md   -> layouts/_default/single.html
content/posts/another-post.md -> layouts/_default/single.html

The template files used to display these pages are stored in the layout/_defaults folder of the theme. You can also see in the layout folder the partials folder: this is where you put the HTML code that you want to reuse in your theme. For example, you can put the head and navbar in partials and then include them in your baseof.html file.

The baseof.html file is the base file of your theme. You can think of it as the skeleton for every page.

One key concept in Hugo is a block, you can define blocks in your baseof.html file and then override them in your template files. For example, you can define a block named main in your baseof.html file and then override it in your listof.

Here is for example my baseof.html file:

html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE html>
<html>
    {{- partial "head.html" . -}}
    <body>
            {{- partial "navbar.html" . -}}   
                <main >
                    {{- block "main" . }}{{- end }}
                </main>
            {{- partial "footer.html" . -}}
    </body>
    
</html>

Here I’m defining an HTML document, then I copy-paste the head in the document with {{-partial "head.html" . -}} in order to have the same head in all the pages. I do the same with the footer and the navbar. Then I define a block named main. Because every template uses the baseof.html file, you can define a main block in baseof and then override it in the list or single template.

Here is the single.html template

html
1
2
3
4
5
6
7
8
9
{{ define "main" }}
    <div class="text-center pt-5 md:pt-12 lg:pt-20 text-2xl justify-center container">
        {{ .Title }}
    </div>
    <br><br>
    <div class="container justify-center">
        {{ .Content }}
    </div>
{{ end }} 

Here I’m overriding the main block, and I’m adding the title and the content of the page. The {{ .Variable }} is Hugo syntax (in fact it’s go template syntax) that allows you to access the variable of the page. For example, the title of the page is stored in the .Title variable. You can define your own variables in the .md file of the page using front-matter. You also have access to a page context where all these variables are defined.

Customizing the rendering

You can add a custom way to render code block. In your content file you can add a code block like this:

md
1
2
3
'''go
// my go code
'''

And you can add a custom way to render this code block using the render-hook in the themes/deutalion/_default/_markup/ file. Here is mine for all the codes included in my content.

html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<figure class="py-5">
    <figcaption style="background-color:#323232;" class="rounded-t-2xl grid grid-cols-2  ">
        <div class="flex justify-start my-auto ">
            <div class=" text-white pl-3 ">{{ .Type }}</div>
        </div>
        <div class="flex justify-end my-auto rounded-t-2xl" style="background-color:#323232;">
            <button id="copy-button" data-tooltip-placement="bottom" type="button" style="background-color:#323232;" class="rounded-tr-2xl flex items-center px-3 text-xs font-medium text-white border-l border-gray-200   dark:bg-gray-800  hover:text-blue-400 ">
                <svg class="mr-2 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z">
                    </path>
                </svg> 
                <span class="copy-text rounded-t">
                    Copy
                </span>
            </button>
        </div>
    </figcaption>
    <div>
        {{ highlight .Inner .Type }}
    </div>
</figure>

This use a figure, with a figurecaption (the header displaying the language and the copy button) and the code highlighted by Hugo using {{ highlight .Inner .Type }}.

I added a bit of JavaScript to make the copy button work.

js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
document.querySelectorAll('#copy-button').forEach(function(button) {
    button.addEventListener('click', () => {
        copyTxt = button.querySelector('.copy-text')
        copyTxt.textContent = "Copied!";
        setTimeout(()=>{copyTxt.textContent = 'Copy';}, 3000);
        //0 is the lines number, 1 is the code
        var codeBlock = button.closest('figure').querySelectorAll('code')[1];
        var txt = ""
        codeBlock.childNodes.forEach(function DFS(node){
            if (node.hasChildNodes()) {
                for (var i = 0; i < node.childNodes.length; i++) {
                    DFS(node.childNodes[i]);
                }
            } else {
                txt += node.textContent;
            }
        })
        navigator.clipboard.writeText(txt)
    });
})

Emphasizing inline code

You might like to have a different style for inline code. You can do that by adding a bit of CSS in theme/yourtheme/static/css/tailwind.css:

css
1
2
3
4
5
6
7
@layer components{
   ...
   /* css for inline code*/
    p > code {
        @apply font-mono dark:text-gray-300 dark:bg-gray-700 text-gray-700  bg-gray-100 p-1 rounded ; 
    }
}

Conclusion

A theme is made using reusable component (partials). For each page on your website, you have 1 content file, and it’s corresponding template file used to generate the final HTML. All the template use the baseof.html file as a skeleton you can override the blocks defined in it. You can add custom rendering for links, code blocks, etc. using the render-hook.