A Guide to Hugo Concepts

Hugo is a static site generator: it takes some plain-text content, marries it to a bunch of HTML templates, and produces a set of complete, static HTML pages that can be served by any generic, stand-alone web server. Simple.

Or maybe not. Hugo does a lot of things automatically, relying on conventions and implicit rules, rather than on explicit configuration.

For example, it tries to match each piece of content with the most “appropriate” template. Hugo will also generate certain pages entirely by itself (mostly content summaries and directory listings, or technical files like sitemap.xml).

All this convenience comes at a price, however: Hugo’s operations can appear very opaque. This would matter less if Hugo was configured to “just work” right out of the box. But Hugo by itself only provides the transformational engine: to actually produce output, it also needs a set of page templates (a “theme”). Not understanding the interplay between the website author’s source files, Hugo’s processing rules, and the theme templates, will result in hours of frustration.

Another difficulty for newcomers is that the “static site model” breaks with many assumptions we have come to make about the way the Web works. Because the generated website is completely static, all visible URLs must map to filesystem entities (files and directories) exactly. This is very different from the typical web app or REST API, where a URL is merely a logical identifier that can be interpreted essentially arbitrarily.

This write-up is an attempt to summarize my understanding of “how Hugo works”. It makes no attempt at being complete, and eschews most detailed howtos or reference documentation. Instead, it concentrates on the “Big Picture” ideas and architectural concepts that I could not find in the official Hugo documentation.


I am not a Hugo developer, not even a power-user. I have never attempted to write a theme. I merely want to use Hugo to build this website. But to even use Hugo with confidence, I found it necessary to research its behavior to a far greater level of detail than was easily accessible in the current documentation. This write-up is essentially a cleaned-up version of the notes I took while doing this research.

My own understanding of Hugo is rudimentary; if you find a mistake, please let me know so that I can fix it.

Part 1: A Roadmap to Hugo Concepts

Hugo takes a bunch of input files (typically in Markdown format), marries them to an appropriate set of templates, and produces a set of static HTML pages. These generated files are intended to be served “as is” by a web server, without any further processing; in particular, without any URL rewriting. The consequence is that for any publicly visible URL, there must be a filesystem entity (either a file, or a directory containing an index.html file).

Note 1:

The entire site generated by Hugo is completely static. Its filesystem paths correspond directly to public URLs. To have a public URL https://example.com/a/b/c requires a file or directory at location a/b/c under the web server’s docroot.

Basic as this concept is, it keeps tripping me up. I am so used application servers and web apps, which treat URLs as merely logical “endpoints”, that I need to keep reminding myself: with Hugo, if you expect to have some public URL, then you must have a filesystem entity, with exactly the same path!

Having established that, to Hugo, a website corresponds directly to a set of filesystem objects, the next important fact is that Hugo expects a strictly hierarchical layout of a website: a hierarchical, tree-like system of directories with files in them.

Note 2:

To Hugo, a website is a strictly hierarchical tree of directories with files (or directories) in them.

That makes good sense, but it is quite different from the way web applications usually organize and manage content. In the typical “shopping cart” application, pages are not so much organized hierarchically, as laterally: a series of workflow steps (checkout, shipment, payment, and so on) that have to be traversed in sequence. And for a site built using single-page architecture, the concept of a “page” as a distinct entity has become fluid anyway, so that the idea of a hierarchy of pages is almost meaningless.

Hugo is not built for this kind of flexibility. In Hugo’s world, a website consists of directories with files in them. If a user selects a file, the user receives that file. If the user navigates to a directory, the user will see a list of items (files) in that directory.

Hugo generates HTML pages from a set of flat files that contain the “content” to be shown on the website. Each individual file in the source directory becomes a single page in the website, and with the same path as the source file. And vice versa: each page of content in the generated website requires its own, separate input file, and at the same location in the filesystem.

Note 3:

Each content file in the source directory becomes a separate page in the website, and with the same path. Conversely, every page in the website requires a separate input file, and at the same location in the filesystem.

The template that Hugo will use to render the contents of the input file is a “single page template”. (The other kind of template are “list templates”, which we will see in a moment.)

This concept is so central to Hugo that it is baked into its architecture: each page is based on a single input file, and its URL is given by the input file’s path in the filesystem. This means that it is not possible to combine several, independent bits of content on a single page. As opposed to other templating frameworks, Hugo does not support a “component model”, where a page is built up from widget-like components: every page is either a list, or corresponds to a single input file. (It is possible to “decorate” a page with things like menus, footers, sidebars. But it is in general not possible to collate a single web page from multiple bits of input.)

So far we talked about files in the input tree. Directories are treated differently. For each directory, Hugo creates a page that displays all the items (typically files) in that directory. The template used for this page is a “list template”.

Note 4:

For each directory, Hugo automatically generates a list of the items (typically files) contained in that directory.

While it is possible to modify or augment the information that is displayed on these “list” pages, it is important to realize that they are usually created automatically. This is in contrast to “single” pages, which are created specifically to display an item of content provided by the user.

To generate any output, Hugo needs at least one “single page” and one “list” template — otherwise, it doesn’t know how to produce output. (It is possible to have multiple templates, with different ones being used for different parts of the site.) A set of templates, together with required items such as stylesheets or JavaScript files, constitutes a “theme”.

Note 5:

Hugo is just a templating engine. It must be supplemented with a set of templates that define the look and feel of the generated website.

Unfortunately, Hugo ships without a default or fall-back theme. To do anything with Hugo, it is necessary to either adopt a theme (for instance from the repository at themes.gohugo.io), or to create at least a “single page” and a “list” template.

Templates are more than merely aesthetic “skin”. They determine what information is visible on a page and can be surprisingly complex. As Hugo parses the input content, it builds up internal data structures, then selects an appropriate template (“single page” for files, “list” for directories), and passes the content data to the template. The template, in turn, must navigate the data structure passed to it by the Hugo engine, and build up a complete HTML page from it.

Review and Outlook

When working with Hugo, it is important to keep its operating model and its limitations firmly in mind.

  • All content is static. URLs are fixed and must correspond to items in the filesystem.

  • Each piece of content is rendered as a single, separate page; it is not possible to combine independent blocks of content to form a page. For a directory, Hugo will automatically generate a list of all items contained in the directory.

In practice, the first is inherent in the “static website” architecture. It does take some getting used to, in particular when coming from a background of application servers or web apps, and it does pose problems as soon as any sort of user response is desired. (For example, it is not easy to support user comments on blog entries.)

More problematic, in my experience, is the decision to treat all content as either “single page” or “list of items”. Although valid on some level (even a site like Amazon consists primarily of list and single-detail pages), I think it is just too limiting and constrains the design of the resulting website too much. A comparison with Amazon is revealing: an Amazon single-detail page does not only contain information about one item, but also a list of other items (“Customers who bought also bought…"), as well as a list of reviews! This has nothing to do with the static website model (although they probably don’t, Amazon could pre-generate it’s single-detail pages, including recommendations and reviews, updating them once a day), but stems from Hugo’s expectation that the final website must map, directly, to a hierarchy of filesystem objects.

In fact, much of Hugo’s complexity comes from the fact that its underlying architectural metaphors are simply too restrictive, necessitating all kinds of ad-hoc workarounds (for menus, partials, URL management, and so on — we will cover some of them later) that make Hugo so opaque and difficult to understand!

Why Hugo?

The idea of a static-site generator has a lot of appeal; and being independent of both WordPress and the various blogging platforms is very attractive. Among the various static-site generators, Hugo and Jekyll seem to be the two most popular. As a non-Ruby developer, I ruled out Jekyll. I did not want to become dependent on the larger Node/npm ecosystem, so that ruled out the JavaScript contenders. And none of the Python entries seemed to match any of the other options in popularity. That left Hugo.