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.