Getting Started With Hugo Partials
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:
- have a base that builds up everything else,
- 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:
- You probably don’t have any alternate types of pages
- 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 »</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.