Getting Started With Hugo Partials

Last Updated:

Tags: hugo, webpage

3 in a series of 5 articles in the Build a Website series.

Partials: the Bread and Butter

By now you should have most of the tools you need to work on your website, the configuration you’ll want for your site, and the bare basics of SCSS styling. We’re not far away from plugging along making posts left and right. After this article you should be able to basically get going with making content for your website.

Getting that info out on your website uses a technology called “partials,” which makes sense because it’s a partial HTML page. Before the advent of static site generators and SPA frameworks and PHP (the dark times), you may expect to specify each page as a full and complete HTML page. Well literally no one does that for any site of significant size. If you’d like to do this, well… I can’t stop you but I’d suggest you do something different with your time. Partials alleviate the tedium of writing each page by hand, or copy-pasting components repeatedly. We’ll dive into what I’ve found to be the most important partials.

Let’s Get Started

If you’re lazy (and you should be), then check out this empty canvas repo. If you’ve already created a new website using Hugo and you want to borrow this canvas template, clone the repo to a new location and then move it over into your current site directory. If you haven’t made a site yet, this should be O.K. to use as your starting template. You’ll want to set a different git remote later on (check out the mini-article I have here).

Go ahead and check out all of those files, see if you can grok them on your own, but I’ll be walking through it, worry not.

What Content?

Hugo keeps all of its content in the aptly named “content” folder. When you want to have any kind of markdown text to display you need to have some sort of partial to render that content. I’ll cover markdown more in-depth in a later article. We won’t need much to get started here, because we’re going to work on the much more technical Hugo partial syntax.

Hugo makes it easy to make new content too once you get it all set up, just run hugo new posts/new-stuff.md in your command line of choice and Hugo drops a new entry into your content folder. If you haven’t already, try it out. You’ll get a mostly markdown file with some “Lorem Ipsum” text, unless you have messed with your archetypes/default.md. The front matter (that’s the stuff at the top of your markdown files) will include the title you specified when you ran the hugo new command and the date will be roughly when you ran it. Additionally you’ll see something like “draft: true”. With this parameter set, Hugo won’t display this page unless you run the -D option either when serving or rendering your site: e.g. hugo serve -D (which is how I run while developing) or hugo -D (which I don’t run!).

So to get going, you’ll need some content. The canvas starter includes some lorem ipsums but if you’ve got that new markdown doc, drop some simple text there. If you’re inclined to test out the Markdown rendering, RTD.

So, Where is it?

If you made a content piece under “post,” then you can see that under a similar URL. First, make sure you’re running locally using this command

hugo serve -D

This sets up a lightweight server and binds to your localhost, usually on port 1313. Try and navigate to that https://localhost:1313, and if you’re using the startup app you should see a few pre-made “articles” and “posts” hanging around your “new-stuff” post. For my example, it’ll be here: https://localhost:1313/posts/new-stuff.md.

We’ve just fast-forwarded past partials, and we’re already looking at our starter project’s take on rendering that page. So what’s happening?

How Hugo Figures Out What to Draw

This is one of the hairiest parts of Hugo’s inner workings. If you didn’t build the system it’s a bit of an eldritch horror. If you dare, RTD. For my purposes, I will only look at the templates that we need to get the posts and landing page working.

Hugo builds your pages using HTML with some Go flavored goodness to keep us sane. To start, you have a baseof.html, template that acts like a picture frame for every other page in your website. The Canvas project opts not to use one, so we’ll be building one for our own sake. A lot of Hugo Themes supply this, but if you’re like me you don’t have a theme! That’s ok, because a lot of Hugo bloggers like to tell you how to set it up, like this wonderful site. Honestly, if you’re not keen on waiting for this series to be complete, you should probably check this website out. In that series, you’re looking at making your own Theme which I forego, but it’s got a lot of good info on making a Hugo Site.

All of our partials live in the layouts folder. By default that’s empty, but we’ve come supplied with some from our template repo. To get started we need an “index” layout, and most times you need to display different types of content in different ways. To organize your layouts, mirror the directory structure you see in the content folder. For this “post” we’ll also modify the layout in this location layouts/post/single.html. You can probably see how Hugo figures this out, but let’s spell it out.

When Hugo wants to render a content item, first it figures out the content “kind”, and “type” (and a lot of other attributes that may be useful depending on the shape and size of your blog). “Kind” resolves to a few different types of pages that every pages fits into. For your post/new-stuff.md it’s a regular page, so it’ll be filed as regular and it’ll try to use a single.html layout (because that’s how it works). The home page is it’s own “kind” so it’ll try to find other layouts, starting on the “index.html” and progressing through a list of other options (the arcane order for home pages is listed here in index form). For your regular pages, you can also specify different single.html pages depending on the different types of content you have: so if you have “posts” and “articles” and that distinction is relevant on your page then you can have a layouts/post/single.html, and a then you can have a layouts/article/single.html where you specify the layouts for the contents differently according to their “type.”

Sorry if that’s inscrutable, but that’s the way Hugo runs. The partials system goes deeper, but let’s stay up here where things kinda make sense.

Let’s Write Some Layouts

There are 2 main paradigms to making your layouts:

  1. have a base that builds up everything else,
  2. write your partials with partials that fill in the details.

I’m a fan of the first option, because it keeps things encapsulated. I don’t have to think about my post template missing the footer elements because I dinged it up. This can pose a problem in getting specific header elements, like JavaScript and CSS, to specific components without including them with the whole site, but it keeps things organized otherwise. If you need more control over the base for specific components, the Hugo system’s got you covered: RTD. This starter project leans towards the later, which means you can have some hanging tags if you aren’t exceedingly careful. We’ll fix that!

Let’s do some cleaning: for me, articles and posts are the same thing, and posts is a shorter word, so I delete the articles content section and the articles layouts to give me a localhost:1313/posts/xyz look to my URLs.

Let’s get started making that layout. First, look at the layouts/index.html page. This has a bunch of parts that (IMO) should be in your ‘baseof’. Before we get too deep into this: if you want to avoid dead links on the articles we just deleted then delete lines 43-62, and lines 59-61 after that. Create this file: layouts/_default/baseof.html and set it aside for now, and we’ll start hacking!

Your index.html will start like this:

{{ partial "head.html" . }}

<main>
<section>
<!-- code continues... -->

Let’s step through this. Right off the bat, we’ve got something that’s isn’t HTML. Double curly braces {{}} means we’re working with Hugo, and you’ll be getting something programmatically. Hugo processes the partials you give it, and this is one of those: That first “Action” {{ partial "head.html"}} finds that partial in the layouts/partials folder and renders it. So let’s open it up, layouts/partials/head.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Canvas</title>
  <link rel="stylesheet" href="/css/canvas.css" type="text/css">
</head>
<body>

This is all simple HTML document info, but we can upgrade a few things to make it easier on ourselves to maintain this project. Let’s actually use the config for filling in the details of our site. First, change the <title> tag to this:

<title>
  {{ block "title" . }}
  {{ .Site.Title }}
  {{ end }}
</title>

This drops a title tag and it’ll look for a “title” block, which you can define in other partials. It then pulls the Site’s title parameter with that {{ .Site.Title }} and renders it in the title tag, which is what shows up on your browser tab, or bookmarks (usually). The interesting part about the block function, is that it opens a new context that needs to be closed with an {{ end }} command. This content grabs content in the target page’s “title” block and then renders the stuff you put in before that {{ end }} call. We’ll cover what that block is in the next section. If the page you’re working with doesn’t have a title block, then it just whatever you put in that block (which is the .Site.Title, remember?). That {{ end }} block is more common using conditionals or the range function but if you forget it Hugo will complain!

Next, go ahead and add this before the first <link> tag:

<link rel="cannonical" href="{{ .Permalink }}">

This statement pulls the “Permalink” attribute from the current page. Every page has this attribute, so if you use the current context, “.” like we did here, you’re basically guaranteed to get the correct attribute. So both the .Site.Title and .Permalink both seem like they’re coming out of nowhere right? Hugo’s got this notion of context, which can get pretty squirely; but the Hugo documentation tries to clear it up here. In this case you might think of it like this: every page has access to the .Site attribute and every page better have a .Permalink. For this base template, this is pretty safe. In other cases, when you’re trying to access different attributes from pages that don’t have specific parameters, you’ll get a compilation error when you try to build your site using hugo. That’s one of the reasons you’ll need to make separate list pages, because they’re dealing with several pages, not just one!

Tidy up

Before this, I said we’d want to move some of the stuff in the layouts/index.html page out into that “baseof” page right? Well let’s do that now. If you’ve still got that layouts/_default/baseof.html page open, go ahead and move these lines from the top and bottom of your layouts/index.html page into the baseof.html file. You’ll now have this very odd file:

{{ partial "head.html" . }}

{{ partial "foot.html" .}}

The Canvas repo makes the stylistic decision not to close tags in the same context it opens them in, which I’m not a fan of: this leaves you open to making a dumb mistake where you forget a partial which wrecks your layout. I don’t like that being possible because I’m prone to making dumb mistakes, so let’s nip that in the bud. Change your layouts/partials/foot.html, layouts/partials/head.html and layouts/_default/baseof.html to look like this:

<<!-- layouts/partials/foot.html-->>
<footer>
  <ul>
      <li><a href="https://github.com/mdhender/canvas">Github</a></li>
  </ul>
</footer>

<<!-- layouts/partials/head.html-->>
<head>
  <meta charset="utf-8" />
  <title>
    {{ block "title" .}}
    {{ .Site.Title }}
    {{ end }}
  </title>
  <link rel="cannonical" href="{{ .Permalink }}">
  <link rel="stylesheet" href="/css/canvas.css" type="text/css">
</head>
{{ partial "head.html" . }}

{{ partial "foot.html" .}}

<<!-- layouts/_default/baseof.html -->>
<!DOCTYPE html>
<html>
{{ partial "head.html" . }}
<body>
</body>
{{- block "main" . }}{{- end }}
{{ partial "foot.html" .}}
</html>

After this change, we’ve remembered to close out all of our html tags by default and we’re good to go. With this, you’ll have a scaffold that your other templates can rely on to render the Title and Main portions consistently. Let’s keep going and make a post partial to take advantage of that.

The Post Partial

Next, we’re going to work with the template that handles rendering posts. This will live in layouts/post/single.html, so let’s just dive in. The default template that the canvas project we copied was built thinking we’d not want to use templates, so let’s get rid of the {{ partial }} tags before we do any real work.

Next, add this to the top of your template:

{{ define "title" }}
{{ .Title }}
{{ end }}

Pretty simple, right? This all follows the same principles as before, except this is rendering out of the baseof.html partial according to the corresponding title block. {{ define "title" }} creates a “title” block for Hugo to pick up and drop in the base template. I’d recommend messing around with both blocks to see what you can achieve.

Let’s wrap up by wrapping the “main” content in a similar tag, put a {{ define main }} block (with an {{ end }} tag at the bottom of the page) and wrap the rest of your content inside of a <main> tag.

The {{ .Content }} call is where the Hugo magic happens. If you didn’t do anything too adventurous with the markdown in your post, you’ll see something pretty boring, like a <p> tag, or maybe a header. We will dive into this in a later article, but if you’re particularly curious, keep reading those docs!

At the very bottom you should see this aside tag:

<aside>
  <ul>
    <li><a href="{{ .Site.BaseURL }}">Home</a></li>
    <li><a href="{{ .Section | absURL }}">Posts</a></li>
    <li><a href="{{ "articles/" | absURL }}">Articles</a></li>
  </ul>
</aside>

This is fine, but if you want that kind of navigation on all of your pages then it should go in the baseof, template right? Also, you probably want to get rid of that second anchor that leads to the “articles” that no longer exist. Where this navigation cluster goes is a matter of personal preference. I’ll opt to leave it here (and delete that articles line).

Getting Around

Finally, we’ve got a partial for displaying our posts. The canvas repository lets us navigate to our content from the home page and the localhost:1313/posts/ list page. Let’s look at that page to figure out how it works. First, open up the layouts/posts/list.html. Again, we’re going to get rid of the {{ partial }} calls at the top and bottom of this layout, and add our {{ define "main" }} and {{ end }} tags in their place. This should look basically the same as before but now you can’t mess up by deleting a partial.

When I mentioned that you needed the {{ end }} tag or Hugo would complain, I mentioned you usually see them when working with ranges. Now, we’ve got a range call: check out the code around line 11.

<article>
  <ul>
    {{ range where .Pages.ByPublishDate.Reverse "Section" "posts" }}
      <li><a href="{{ .RelPermalink }}">{{ .Title }} - <time datetime="{{.Date}}">{{ .Date.Format "2006/01/02 15:04:05" }}</time></a></li>
    {{ end }}
  </ul>
</article>

Breaking it down, the range function iterates over a group that you provide. In this case, the group is “.Pages”, and the “dot” context is the site context. In this case the where clause is totally unnecessary, but let’s go over what it’s doing. The where keyword is telling Hugo that we don’t want it to iterate over all of the members of the collection we’re looking at. Instead, try to access a Property in each of the members (in this case “Section”) and make sure it’s the same as “posts”. This isn’t the only thing where can do, but it gives you a good idea of how it works: I’d encourage you to search for “where” in the hugo documentation for more info.

I haven’t mentioned the .ByPublishDate.Reverse part of the collection. Hugo provides a few default sorting orders, including weight, count, publish date, title, and date. If you’re curious about these other sorting methods, RTD.This page also has content list examples and goes into much deeper detail than I will.

We don’t need the where "Section" "posts" bit of our range call. That’s because:

  1. You probably don’t have any alternate types of pages
  2. This context already filters on “posts” because your on the post list page!

If we remove this it should render out the same, and you’ll have this code instead:

{{ range .Pages.ByPublishDate.Reverse }}
  <li><a href="{{ .RelPermalink }}">{{ .Title }} - <time datetime="{{.Date}}">{{ .Date.Format "2006/01/02 15:04:05" }}</time></a></li>
{{ end }}

Since we’re looking at this, let’s look at the list item. You’ll see 4 calls for Hugo to update. First is the href="{{ .RelPermalink }}" in the anchor tag: this is referencing the Page’s URL, and each entry we get from the containing range will have (or should have) it’s own unique address, and now when you click on the article your browser will take you there. Next, the {{ .Title }} call does what you might expect - this drops the title defined for the article in the front matter. Finally we get 2 different calls for .Date, the first only drops the date parameter from the front matter as plain text in the datetime attribute for this time tag. The second is more interesting. By invoking the .Format method, you are able to change how that data is displayed. This is a special function available to the .Date parameter for the page. The magic is explained here.

How Generic

If you foresee yourself making several different types of content which need to live in different areas, but won’t be using different lists then you can use the layouts\_default\ directory. When templates are specified in this directory but you haven’t defined a layout for some page (i.e. a taxonomy page, a regular page, or a list page), then Hugo will “default” back on this. This isn’t a bad idea, so let’s copy over our post’s single.html and list.html templates to layouts\_default\single.html and layouts\_default\list.html

In the list.html page, you’ll probably have the header “All Posts” and all the way at the bottom you’ll have an <aside> tag that references .Section. That won’t work if we have different types of content, so go ahead and replace the title block like this:

<!-- old -->
<header>
  <h1>All Posts</h1>
</header>

<!-- new -->
<header>
  <h1>All {{.Title}} </h1>
</header>

The aside tag probably has too much inf. Let’s fix that now, and actually let’s renege on my earlier decision: this navigation part has nothing to do with listing content! We should pull that out and put it in its own “sitenav” partial. Make a new file here: layouts/partials/sitenav.html then drop the aside tag in there.

<aside>
  <ul>
    <li><a href="{{ .Site.BaseURL }}">Home</a></li>
    <li><a href="{{ "posts/" | absURL }}">Posts</a></li>
  </ul>
</aside>

This works pretty well, and having all of your site navigation in one place, rather than 5 (as in the original) means you can mess with it to your heart’s content. To drop that partial in, make sure to put the partial tag in its place, like so:

{{ partial "sitenav" }}

One thing you may notice is that you could have new sections appear on your sitenav partial automatically by calling a range over the .Site.Sections property. That will work, but for this tutorial we don’t need to do that Do which ever style you’d like: if you want all of your content types to show up there all the time then it’ll work for you.

If you just found yourself replacing all those <aside> navigation sections and wondering “why don’t I just put this in the baseof template and not rewrite this,” then you’re already on track to organizing your Hugo site. Consider: you may want your <aside> to be a part of your main content, or you may not want the navigation to be everywhere. At this point, it’s a good idea to start to think about how your site will communicate the most important ideas and how a visitor to your site should expect it to work. This is where the tutorial ends on me saying “You gotta put this in your xyz.html template or it won’t be good,” and where it starts saying “You gotta actually decide how you want this to work now.” If you think there’s a best way to do it, then good! Make a site, or a post somewhere and tell the world, I’m not stopping you.

Let’s modify the index.html page, to suit this purpose. If you’re familiar with typical website design, you’ll know that the index.html page is a special page for defining the root of your website. It’s the same deal with Hugo, the file in layouts/index.html will be available at the root URL for this site

Home is Where the index.html File is

We’ve modified this this file already, but let’s take a look at the layouts/index.html page. As I’m looking at it after I’ve gone and abstracted my navigation I’ve got this:

{{ define "main" }}
<main>
  <section>
    <!-- this section is populated from content/_index.md -->
    <header>
      <h1>{{.Title}}</h1>
    </header>

    <article>
      {{.Content}}
    </article>

    <footer>
      <!-- Note that .Pages is the same as .Site.RegularPages on the homepage template. -->
      {{ range first 10 .Pages }}
          {{ .Render "Summary"}}
      {{ end }}
    </footer>
  </section>

  <section>
    <!-- this section is populated by pulling posts from the site -->
    {{ range first 1 (where .Pages.ByPublishDate.Reverse "Section" "posts") }}
        <article>
            <header>
              <h2>{{ .Title }}</h2>
              <p><time datetime="{{.Date}}">{{ .Date.Format "Mon, Jan 2, 2006" }}</time></p>
            </header>
            {{ .Summary }}
            <nav>
              <ul>
                <li><a href="{{ .RelPermalink }}">Read More &raquo;</a></li>
              </ul>
            </nav>
        </article>
    {{ end }}
    <footer>
      <a href="{{ "posts/" | absURL }}">All posts...</a>
    </footer>
  </section>
  {{ partial "sitenav" }}
</main>
{{ end }}

Straight up, most of this doesn’t work. For one, I’ve got 3 articles which all have some lorem ipsums in it, but they aren’t rendering any of it. The comment says that “.Pages is the same as .Site.RegularPages,” but I don’t find that to be the case anymore. So let’s fix it!

I’ve already streamlined my pages to get rid of posts named articles, so let’s just make that the main section and merge the top section.

<!-- ~ line 3 -->
  <section>
    <!-- this section is populated from content/_index.md -->
    <header>
      <h1>{{.Title}}</h1>
    </header>

    <article>
      {{.Content}}
    </article>

    <!-- this section is populated by pulling posts from the site -->
    {{ range first 1 (where .Pages.ByPublishDate.Reverse "Section" "posts") }}
        <article>
<!-- etc. etc. -->

I want my summaries to render, so let’s fix that Hugo code. The comment gives us a hint, and to fix it I just tell Hugo to reference the context’s Site attribute and give me the pages

{{ range first 5 (where .Site.Pages.ByPublishDate.Reverse "Section" "posts") }}

And why stop at 1 article, give me at least 5! If you’re in lockstep up to this point, I commend you but you should also see something odd. You probably also have a “Posts” page showing up in your list. List pages count as pages too, so if you want to exclude that page from you home page, then specify .RegularPages which Hugo says excludes list pages:

{{ range first 5 (where .Site.RegularPages.ByPublishDate.Reverse "Section" "posts") }}

Before we call the index page good, I’ll make a note of the “.Summary” property of regular pages. The {{ .Summary }} field is an interesting one, because it’s quite flexible (RTD). Without diving too deep into that rabbit hole, Hugo makes a summary magically based on the content of the page you’re looking at. By default it takes a short snippet from the front of the post and drops it in.

We’re Finally Here

So if you’re already a Markdown wizard, you can push content to a pretty simple blog, and you can navigate to that blog’s content. In the next article, I’ll be getting into upgrading the style sheet with Material. If you’re getting lost in all of this, I’ve made a handy dandy git repository to try to shore up the damage.