Creating a minimal working template

I am attempting to create a tutorial that walks through the steps to create a new
template. This combines the different examples from the hugo web site. I’m avoiding
styling because that is actually the easy part.

Create a New Theme

Hugo doesn’t ship with a default theme. There are a few available (I counted a dozen
when I first installed Hugo) and Hugo comes with a command to create new templates.
The goal of this tutorial is to show you how to fill out the files to pull in your content.
TODO mention that focus is on generating content rather than presentation.

Introducing Zafta

This tutorial creates the “zafta” theme. It contains no styling so that it can focus
on the minimum effort needed to make a theme useable. It has a few opinions on the
layout, eventually those will be documented. For example, it uses “post” over “blog”.
That will be called out when we add a separate template for the two types of content.

Use the hugo “new” command to create the skeleton of a theme.
This creates the directory structure and places empty files for you to fill out.

$ cd _site_root_ 
$ hugo new theme zafta
$ vi themes/zafta/theme.toml
author = "michael d henderson"
description = "a minimal working template"
license = "MIT"
name = "zafta"
source_repo = ""
tags = ["tags", "categories"]
:wq

Update the Default Configuration File

It’s a good idea to add the theme name to the config.toml file.
If you don’t, you’ll need to remember to add “-t zafta” to all
your hugo commands or you won’t use the template that you’re
expecting to. While we’re in here, we’ll add some more information
that TODO need to document by example later.

$ vi config.toml
theme = "zafta" 
baseurl = "" 
contentdir = "content" 
layoutdir = "layouts" 
publishdir = "public" 
canonifyurls = true 
languageCode = "en-us" 
title = "your title here" 
MetaDataFormat = "toml" 
[indexes] 
    category = "categories" 
    tag = "tags" 
[author] 
    name = "your name here" 
:wq

Generate the Web Site

Note: run these same commands every time you need to generate the web site.

The remove is included because it is nice to clean out the results of prior
runs to keep things looking clean for this tutorial.
You wouldn’t do this very often on your working site.

$ rm -rf public/* 
$ hugo server --watch --verbose

Verify the Results

$ find public -type f -name '*.html' | xargs ls -l
-rw-rw-r--  1 mdhender  staff     0 Sep 27 13:08 public/index.html 

Note that all the files are empty. This is because the default layouts are empty.

Update the Home Page Template

The home page is the page at the root of your static web site.
You don’t create it by editing a markdown file.
It is created from the template in themes/zafta/layouts/index.html.

Let’s make it show some static text. First, update the template.

$ vi themes/zafta/layouts/index.html
<!DOCTYPE html> 
<html> 
<body> 
  <p>hugo says hello!</p> 
</body> 
</html> 
:wq

Then generate the web site and verify the results.

$ find public -type f -name '*.html' | xargs ls -l 
-rw-rw-r--  1 mdhender  staff  246 Sep 27 13:32 public/index.html 
$ cat public/index.html 
<!DOCTYPE html> 
<html> 
<body> 
  <p>hugo says hello!</p> 
<script>document.write('<script src="http://' 
        + (location.host || 'localhost').split(':')[0] 
		+ ':1313/livereload.js?mindelay=10"></' 
        + 'script>')</script></body> 
</html>

The script is added by hugo to help you write your content.
Look for live reload in the documentation to see what it does and how to disable it.

Create Two Posts

Now that we have the home page generating static content, let’s add some content to the site so that we can
see how Hugo creates “lists” and “singles” through templates.

$ mkdir content/post
$ vi content/post/first.md 
+++
title = "first"
description = "first description"
date = "2014-09-13"
slug = "first post"
+++

# first post

so glad to say that this is the first post
:wq  

$ vi content/post/second.md 
+++
title = "second"
description = "second description"
date = "2014-09-27"
slug = "second post"
+++

# second post

second post, on a roll!
:wq

Generate the web site and verify the results.

$ find public -type f -name '*.html' | xargs ls -l 
-rw-rw-r--  1 mdhender  staff  246 Sep 27 13:38 public/index.html 
-rw-rw-r--  1 mdhender  staff    0 Sep 27 13:38 public/post/first-post/index.html 
-rw-rw-r--  1 mdhender  staff    0 Sep 27 13:38 public/post/index.html 
-rw-rw-r--  1 mdhender  staff    0 Sep 27 13:38 public/post/second-post/index.html 
-rw-rw-r--  1 mdhender  staff    0 Sep 27 13:38 public/post/theme/index.html 

Note that all the new files are empty. This is because the layouts for the content are empty.
The homepage doesn’t show the new content, either. We have to update the templates to add the posts.

Add a List to the Homepage

$ vi themes/zafta/layouts/index.html 
<!DOCTYPE html>
<html>
<body>
  {{ range first 10 .Data.Pages }}
    <h1>{{ .Title }}</h1>
  {{ end }}
</body>
</html> 
:wq

Generate the web site and verify the results.

$ find public -type f -name '*.html' | xargs ls -l 
-rw-rw-r--  1 mdhender  staff  302 Sep 27 13:44 public/index.html 
-rw-rw-r--  1 mdhender  staff    0 Sep 27 13:44 public/post/first-post/index.html 
-rw-rw-r--  1 mdhender  staff    0 Sep 27 13:44 public/post/index.html 
-rw-rw-r--  1 mdhender  staff    0 Sep 27 13:44 public/post/second-post/index.html

$ cat public/index.html 
<!DOCTYPE html> 
<html> 
<body> 
    <h1>second</h1> 
    <h1>first</h1> 
<script>document.write('<script src="http://' 
        + (location.host || 'localhost').split(':')[0] 
		+ ':1313/livereload.js?mindelay=10"></' 
        + 'script>')</script></body> 
</html>

You can verify that the home page does show the title of the two posts.
The posts themselves are still empty.

Make Singles Show Content

Hugo likes “list” and “single” templates. The single template is used to show a single piece of content.
The list template is used to show a group of single pieces on one page.

Let’s update the default single layout. It will be used by the template engine
when it can’t find a more specific template to use.

$ vi themes/zafta/layouts/_default/single.html 
<!DOCTYPE html> 
<html> 
<head> 
	<title>{{ .Title }}</title> 
</head> 
<body> 
  <h1>{{ .Title }}</h1> 
  {{ .Content }} 
</body> 
</html> 
:wq

Generate the web site and verify the results.

$ find public -type f -name '*.html' | xargs ls -l 
-rw-rw-r--  1 mdhender  staff   302 Sep 27 13:51 public/index.html 
-rw-rw-r--  1 mdhender  staff   307 Sep 27 13:51 public/post/first-post/index.html 
-rw-rw-r--  1 mdhender  staff     0 Sep 27 13:51 public/post/index.html 
-rw-rw-r--  1 mdhender  staff   310 Sep 27 13:51 public/post/second-post/index.html

Notice that the posts now have content. You can go to localhost:1313/post/first-post to verify.

Linking to Content

It’s inconvenient to have to manually type the URL for the content, so let’s add links to them to the home page.

$ vi themes/zafta/layouts/index.html
<!DOCTYPE html>
<html>
<body>
  {{ range first 10 .Data.Pages }}
    <h1><a href="{{ .Permalink }}">{{ .Title }}</a></h1>
  {{ end }}
</body>
</html>

Generate the web site and verify the results.

Ever Wonder Why Posts Appear in the “post/” Directory?

The default in Hugo is to use the directory structure of the content/ directory
to guide the location of the generated html in the public/ directory. Let’s verify
that by creating an “about” page at the top level.

$ vi content/about.md 
+++
title = "about"
description = "about this site"
date = "2014-09-27"
slug = "about time"
+++

# about us

i'm speechless
:wq

Generate the web site and verify the results.

$ find public -name '*.html' | xargs ls -l
-rw-rw-r--  1 mdhender  staff   334 Sep 27 15:08 public/about-time/index.html
-rw-rw-r--  1 mdhender  staff   527 Sep 27 15:08 public/index.html
-rw-rw-r--  1 mdhender  staff   358 Sep 27 15:08 public/post/first-post/index.html
-rw-rw-r--  1 mdhender  staff     0 Sep 27 15:08 public/post/index.html
-rw-rw-r--  1 mdhender  staff   342 Sep 27 15:08 public/post/second-post/index.html

Notice that the page wasn’t created at the top level. It was created in a sub-directory
named ‘about-time/’. That name came from our slug. Hugo will use the slug to name the
generated content. It’s a reasonable default, by the way, but we can learn a few things
by fighting it for this file.

One other thing. Take a look at the home page.

$ cat public/index.html
<!DOCTYPE html>
<html>
<body>
    <h1><a href="http://localhost:1313/post/theme/">creating a new theme</a></h1>
    <h1><a href="http://localhost:1313/about-time/">about</a></h1>
    <h1><a href="http://localhost:1313/post/second-post/">second</a></h1>
    <h1><a href="http://localhost:1313/post/first-post/">first</a></h1>
<script>document.write('<script src="http://'
        + (location.host || 'localhost').split(':')[0]
		+ ':1313/livereload.js?mindelay=10"></'
        + 'script>')</script></body>
</html>

Notice that the “about” link is listed with the posts?
That’s not desirable, so let’s change that first.

$ vi themes/zafta/layouts/index.html
<!DOCTYPE html>
<html>
<body>
  <h1>posts</h1>
  {{ range first 10 .Data.Pages }}
    {{ if eq .Type "post"}}
      <h2><a href="{{ .Permalink }}">{{ .Title }}</a></h2>
    {{ end }}
  {{ end }}

  <h1>pages</h1>
  {{ range .Data.Pages }}
    {{ if eq .Type "page" }}
      <h2><a href="{{ .Permalink }}">{{ .Title }}</a></h2>
    {{ end }}
  {{ end }}
</body>
</html>
:wq

Generate the web site and verify the results. The home page has two sections,
posts and pages, and each section has the right set of headings and links in it.

But, that about page still renders to about-time/index.html.

$ find public -name '*.html' | xargs ls -l
-rw-rw-r--  1 mdhender  staff    334 Sep 27 15:33 public/about-time/index.html
-rw-rw-r--  1 mdhender  staff    645 Sep 27 15:33 public/index.html
-rw-rw-r--  1 mdhender  staff    358 Sep 27 15:33 public/post/first-post/index.html
-rw-rw-r--  1 mdhender  staff      0 Sep 27 15:33 public/post/index.html
-rw-rw-r--  1 mdhender  staff    342 Sep 27 15:33 public/post/second-post/index.html

Knowing that hugo is using the slug to generate the file name, the simplest solution is
to change the slug. Let’s do it the hard way and change the permalink in the configuration file.

$ vi config.toml
[permalinks]
	page = "/:title/"
	about = "/:filename/"

Generate the web site and verify that this didn’t work.
Hugo lets “slug” or “URL” override the permalinks setting in the configuration file.
Go ahead and comment out the slug in content/about.md, then
generate the web site to get it to be created in the right place.

Creating Top Level Pages

Let’s add an “about” page and display it at the top level (as opposed to
a sub-level like we did with posts).

Sharing Templates

If you’ve been following along, you probably noticed that posts have titles in the
browser and the home page doesn’t. That’s because we didn’t put the title in the
home page’s template (layouts/index.html). That’s an easy thing to do, but let’s
look at a different option.

We can put the common bits into a shared template that’s stored in the themes/zafta/layouts/partials/
directory.

Create the Header and Footer Partials

In Hugo, a partial is a sugar-coated template. Normally a template reference has a
path specified. Partials are different. Hugo searches for them along a TODO defined
search path. This makes it easier for end-users to override the theme’s presentation.

$ vi themes/zafta/layouts/partials/header.html
<!DOCTYPE html>
<html>
<head>
	<title>{{ .Title }}</title>
</head>
<body>
:wq

$ vi themes/zafta/layouts/partials/footer.html
</body>
</html>
:wq

Update the Home Page Template to Use the Partials

The most noticable difference between a template call and a partials call is the lack of path:

{{ template "theme/partials/header.html" . }}

versus

{{ partial "header.html" . }}

Both pass in the context.

Let’s change the home page template to use these new partials.

$ vi themes/zafta/layouts/index.html
{{ partial "header.html" . }}

  <h1>posts</h1>
  {{ range first 10 .Data.Pages }}
    {{ if eq .Type "post"}}
      <h2><a href="{{ .Permalink }}">{{ .Title }}</a></h2>
    {{ end }}
  {{ end }}

  <h1>pages</h1>
  {{ range .Data.Pages }}
    {{ if or (eq .Type "page") (eq .Type "about") }}
      <h2><a href="{{ .Permalink }}">{{ .Type }} - {{ .Title }} - {{ .RelPermalink }}</a></h2>
    {{ end }}
  {{ end }}

{{ partial "footer.html" . }}
:wq

Generate the web site and verify the results. The title on the home page is now “your title here”,
which comes from the “title” variable in the config.toml file.

Update the Default Single Template to Use the Partials

$ vi themes/zafta/layouts/_default/single.html
{{ partial "header.html" . }}

  <h1>{{ .Title }}</h1>
  {{ .Content }}

{{ partial "footer.html" . }}
:wq

Generate the web site and verify the results. The title on the posts and the about page
should both reflect the value in the markdown file.

Add a Date Published to Posts

It’s common to have posts display the date that they were written or published, so let’s
add that. The front matter of our posts has a variable named “date.” It’s usually the date
the content was created, but let’s pretend that’s the value we want to display.

Add the Date Published to the Template

We’ll start by updating the template used to render the posts.
The template code will look like:

{{ .Date.Format "Mon, Jan 2, 2006" }}

Posts use the default single template, so we’ll change that file.

$ vi themes/zafta/layouts/_default/single.html
{{ partial "header.html" . }}

  <h1>{{ .Title }}</h1>
  <h2>{{ .Date.Format "Mon, Jan 2, 2006" }}</h2>
  {{ .Content }}

{{ partial "footer.html" . }}
:wq

Generate the web site and verify the results.
The posts now have the date displayed in them.
There’s a problem, though. The “about” page also has the date displayed.

As usual, there are a couple of ways to make the date display only on posts. We could
do an “if” statement like we did on the home page. Another way would be to create a
separate template for posts.

The “if” solution works for sites that have just a couple of content types.
It aligns with the principle of “code for today,” too.

Let’s assume, though, that we’ve made our site so complex that we feel we have to
create a new template type. In hugo-speak, we’re going to create a section template.

Let’s restore the default single template before we forget.

$ mkdir themes/zafta/layouts/post
$ vi themes/zafta/layouts/_default/single.html
{{ partial "header.html" . }}

  <h1>{{ .Title }}</h1>
  {{ .Content }}

{{ partial "footer.html" . }}
:wq

Now we’ll update the post’s version of the single template. If you remember
hugo’s rules, the template engine will use this version over the default.

$ vi themes/zafta/layouts/post/single.html
{{ partial "header.html" . }}

  <h1>{{ .Title }}</h1>
  <h2>{{ .Date.Format "Mon, Jan 2, 2006" }}</h2>
  {{ .Content }}

{{ partial "footer.html" . }}
:wq

Note that we removed the date logic from the default template and put it in the post template.
Generate the web site and verify the results. Posts have dates and the about page doesn’t.

Don’t Repeat Yourself

DRY is a good design goal and Hugo does a great job supporting it.
Part of the art of a good template is knowing when to add a new template and when to update an existing one.
While you’re figuring that out, Accept that you’ll be doing some refactoring.
Hugo makes that easy and fast, so it’s okay to delay splitting up a template.

27 Likes

Great ++ … many thanks !

Just wanted to add a comment regarding themes and templates, that threw me off a bit when I first started using Hugo… You don’t actually need to create a theme, it’s perfectly fine just using /layouts for your site. As far as I can understand, /themes is for creating re-usable layouts. In my case, for creating a unique customer specific site, that’s not needed.

3 Likes

@michael_henderson This is awesome. You really have a talent with writing. We’d certainly love any contribution you would like to make to the docs (including this tutorial).

1 Like

I’d be happy to add it after I finish. Where in the docs should this go? As a new markdown file under tutorials?

1 Like

I would either put it there, or in the themes directory. Location doesn’t matter as much, we should put a menu entry in both locations though.

Thank you! Without tutorials like this one I feel that learning Hugo is like learning how to write by looking up words in a dictionary (the official docs) or by looking at a finished novel (sample sites).

I need to see the process to understand and learn: start from scratch, write a working hello-world site (without any copy-pasting or templates), then add new features, change the behaviour, and explain how and why I should do things in a certain way.

I would be very happy to read or watch more such tutorials.

2 Likes

I put in a pull request, but, now that i’m looking at other stuff in docs/content, I’ve noticed that I have the formatting wrong. I’ll fix that up and resubmit.

Awesome indeed, thank you a lot for this! It’s been very helpful already, and I still have to complete it.

This is an awesome post! I have to admit that after reading the documentation pages or looking at existing themes this wasn’t that clear. Thanks!

This is a great jumping-off point but could you add some more examples showing how you might use ALL the variables you defined in your config.toml file in your templates. Every example I see tends to define a load of variables but only ever shows how to access “baseUrl” and “title” in the provided examples. Maybe it’s just me, but I find the whole process for knowing which syntax to use to embed variables from config.toml into templates completely baffling. For example:

  • When do I need to use .Site and when do I not?
  • Why are some variables defined in lower case but referenced in templates in upper case?
  • Why are some of the ‘default’ config values like “title”, “baseUrl”, etc perfectly straightforward to embed in templates, using [for example] .Title or .Site.Title, whilst other seemingly default ones seem impossible to 'get a handle on?

Example with theme = "mytheme" defined in conffig.toml and trying to ‘echo’ that value in the header.html template:

// using {{.Theme }} in template //
<.Theme>: Theme is not a field of struct type *hugolib.Node
//
// using {{.Site.theme }} in template //
.Site.theme>: theme is not a field of struct type *hugolib.SiteInfo
//
// using {{.Site.Theme }} in template //
<.Site.Theme>: Theme is not a field of struct type *hugolib.SiteInfo
//
// using {{.Site.Params.theme }} or {{.Site.Params.Theme }} in template 
// ...doesn't return an error, but value is empty //
1 Like

That’s a good suggestion. I’m working on another theme that pulls in a lot of the topics that have come up on these forums. I’ll post as soon as it’s reasonably complete.

I’m stuck after Linking to Content step. I update the theme’s layout to show the .Permalink and regenerate the site. But the new public/index.html file doesn’t have links. I’ve tried removing the content in the public dir and regenerating and still nothing. I have the zafta theme set at the top level config.

Any suggestions on what I’m doing wrong?

Something odd is going on with my setup. No mater what I do with themes/zafta/layouts/index.html I’m getting an old version of the homepage. It’s like hugo is ignoring my theme all together.

I double checked the top level config.toml and it shows zafta. Is there some way to get more verbose output from Hugo?

Thanks! I was able to create a theme for my site from scratch using your tutorial. Very helpful. It also made me understand better how Hugo works.

Thanks for the tutorial!

Hi,

Thank you, awesome post, I’m working through it step by step.

I’m right after the first two posts; having just rendered (empty) content the first time.

Where is this file coming from?

Files in /public are created by Hugo. That name implies that you created a post called theme.md. Check /content/post/ for it.

Thank you for the reply, but I found that file in your tutorial and not on my side. I have since figured out that I don’t have to worry about it too much though. :slight_smile:

@pieterbreed, I had a post called theme.md while I was working on this. Looks like I deleted it between steps, that’s why you only see it in the one listing. Good eyes :).

1 Like