Hugo theme from scratch: the absolute minimum

In this post I build up an absolutely minimum working Hugo theme for my blog based on the files generated by hugo new theme. The result isn’t a blog I’d publish, but it’s a useful start in understanding how Hugo works.

I’m still wrestling with learning Hugo. Clearly the simple thing is to use someone else’s theme. But I need something custom and specific and also I want to understand what Hugo is doing. So I’m making my own. It’s slow going. There’s no great examples to crib from (all the “minimal blog” themes I could find are elaborate things that include lots of styling and extra features.)

One useful resources is the lookup order page buried deep in the Hugo docs. It explains the basics of how theme template files are used. The best part is where it says “The examples below look long and complex.” Yes, yes they do. But then it also then tells you that in fact there’s 4 templates that are most important: baseof.html,. list.html, single.html, and index.html. Most of my notes here are going to focus on those files.

One useful thing to know about is partial templates. These are HTML snippets intended to be included in other HTML templates. They’re kind of like subroutines, but for templates.

I’m going to follow this tutorial on creating a Hugo theme. It adds some extra stuff (some partial templates for inclusion, CSS, Javascript, a menu) but at its core are those 4 files listed above. I’m going to skip the parts I don’t want and document what happens along the way.

This whole process started with Hugo’s quickstart. I did that first and got my blog working with someone else’s theme. Then I removed the other themes I was using and created my own.


I’ve seeded my blog with a bunch of content (1771 posts) in directories like content/weblog/tech/good/ and content/weblog/politics/. I’ve tested this content works OK with other people’s themes. My blog has several top level directories: politics, tech, culture, etc. For simplicity I put all these directories not directly under content/ but in a single subdirectory (weblog/). This results in a /weblog/ in the URL I’d rather not have but it renders the whole blog in one giant list. With subdirs I was getting one list per top level category. I think this has to do with Hugo Sections, something I don’t understand yet.


Before you create a theme you need to configure a blog. I did this in the quickstart.

baseURL = ''
languageCode = 'en-us'
title = "Some Bits: Nelson's Weblog"
theme = "nelson"

“theme” is the name of a directory under themes/ we’ll create in a minute. Note the baseURL matters: it’s going to be hardcoded in a lot of output files. (Although Hugo seems pretty good about supporting relative URLs where possible.) I made a mistake at first and had it set to; but that causes duplication of /weblog/weblog/in the URLs given the section setup I did.

An empty skeleton theme

Step 1 is running hugo new theme nelson. This creates a skeleton theme. There’s 11 files that creates but they have very little in them. In particular index.html, list.html, and single.html are all empty. baseof.html does have some HTML boilerplate with a <html> tag and inclusion of some partials for page headers and footers; those partials are also generated as empty files. There are no template files for XML or RSS.

You can go ahead and build a Hugo blog with just this skeleton theme but not much happens. In particular no HTML is generated, despite Hugo saying 4 pages were created. But Hugo will create some XML. Two identical RSS 2.0 files under index.html and public/index.xml, both containing all 1771 blog posts entirely. My whole blog in one XML file! Also two empty RSS 2.0 files for “recent content in tags” and “recent content in categories”. Finally a sitemap.xml with 1775 URLs in it, presumably my individual posts and a few index pages. All in all it looks like Hugo has some default blog XML generation. That’s handy but I’m surprised those aren’t implemented with a generated template file. They can be overridden though.

You can also try to load URLs by running “hugo serve”. The suggested URL http://localhost:1313/ contains an empty <pre> block and nothing else. There is no .html file for this in the static blog, nor anything in the templates, so I guess this is something the Hugo server does. http://localhost:1313/index.xml does load and gives back the RSS contents that were generated. Useful URLs for individual posts like http://localhost:1313/weblog/tech/electrify/ from the sitemap give 404s; they weren’t generated, afterall.

Formatting single blog posts

So how to get posts rendering? The absolute simplest thing is to put any content at all in single.html. I put 3 bytes in that file; “Yo\n”. Rerunning hugo results in 1775 pages being created, 1776 files. The 4 XML files from before, the sitemap, and all 1771 blog posts. Nice! Every blog post is at the expected URL and all have the content “Yo”. Note that the HTML in baseof.html did not appear, that’s a little strange.

The next step is to make single.html a little more complex.
{{ define "main" }} Yo {{ end }}
Regenerating with this also creates 1776 files that all say “Yo”. But now the “Yo” is sandwiched inside the HTML template matter in baseof.html. So that’s a bit magic; apparently Hugo does something different depending on whether that “main” block is defined in the page template or not. (Note that the “main” section is used by baseof.html). Almost all the docs for templates always suggest defining a “main”, so that’s the ordinary thing, and it is shorthand for “slot this into baseof.html”. I wonder if this is explicitly coded behavior or if rendering baseof.html fails with no “main” defined.

Finally I replaced my single.html with a useful template that shows post content:

{{ define "main" }}
<h1>{{ .Title }}</h1>
{{ .Content }}
{{ end }}

Regenerating with that and I get something that’s starting to look like a real blog. Each post gets a title and the post content. Nice!

It’s worth noting at this point that Hugo’s preference for “pretty” URLs means that all your blog posts are in files named “index.html” in subdirectories named after the post filename. Ie, you get a tech/electrify/index.html, not tech/electrify.html. This behavior is configurable.

This was all more confusing than necessary; when I first generated my new theme I could not understand where my posts had gone. If it were me I’d make the new theme command generate something like my last single.html, not an empty file which then means don’t generate pages at all. But then maybe that would confuse people trying to use Hugo for something other than a blog.

Lists of blog posts

Having gotten single posts working it’s time to visit lists. Starting as before, I put just “Hi\n” in list.html and regenerate. Unsurprisingly, this creates a file with just “Hi\n” in three places: weblog/index.html, tags/index.html, and categories/index.html. Putting the “Hi” inside a {{ define "main" }} gets it to use the baseof.html template, just like single posts did.

Moving on to an actual useful list, I took a page out of the tutorial I’m following and put this in list.html

{{ define "main" }}
<h1>{{ .Title }}</h1>
{{ range .Pages.ByPublishDate.Reverse }}
<p> <a href="{{ .RelPermalink }}">{{ .Title }}</a> </p>
{{ end }}
{{ end }}

And voila! Now http://localhost:1313/weblog/ loads with a list of all 1771 posts. As the template suggests, each post is rendered inside its own <p> tag with a Title and a relative link (no hostname) to the Post. It’s like I have a real blog now! Well, the basics.

The magic is thanks to that {{ range }} in the template. The range Hugo function is incredibly important, it’s the hidden loop in templates. It stands for “iterate over this whole collection of things and insert stuff one by one”. Range. I read Hugo docs for hours before I found this key concept.


We’re not done yet but I’m going to declare victory with this ultra minimal blog. In summary, all that was necessary was the create a new theme template, then put some very basic stuff in single.html and list.html. There’s plenty more to do. The tags/index.html and categories/index.html files are still empty; I’d like categories to be populated by directory name. I’d also like to generate monthly archive pages. Could do with some pagination too, not to mention more formatting and breadcrumb navigation. But now at least I understand how to render a list of blog posts in Hugo. And I’ve got it rendering a useful top level list.

The main thing I learned in this exercise is what Hugo does by default with very little templating. It renders every content item through the single.html template (and usually, baseof.html). It also renders list.html for the basic list of all blog posts; it’s up to you to use the range function to build and render that list. As an aside Hugo is also doing some basic RSS and Sitemap creation for you. And that’s it!

Update: there’s some thoughtful discussion about this post on Lobsters.

3 thoughts on “Hugo theme from scratch: the absolute minimum

  1. great post! I tried doing this on my own recently and didn’t get too far, so I’m going to try again using this post as a guide.

    however, one line caught my eye:

    > I’ve seeded my blog with a bunch of content (1771 posts)

    I would love to hear more about how you did that. that’s a LOT of content to generate, so I’m curious how you went about it. care to give more info on that?

Comments are closed.