OVERHEAD

A range of choices

Deciphering Hugo

Some tweaks

Before moving onto the major topics, let’s improve Basic a bit more…

Better header partial

HTML for layouts/partial/header.html.

<!DOCTYPE html>
<html lang="en">
<head>
	<title>{{ if not .IsHome }}{{ .Title | markdownify | plainify }} | {{ end }}{{ .Site.Title }}</title>
	<meta charset="UTF-8">
	<meta name="description" content="{{ if .Description }}{{ .Description | markdownify | plainify | safeHTML }}{{ else }}{{ with .Site.Params.description }}{{ . | markdownify | plainify }}{{ end }}{{ end }}" 
	<meta name="keywords" content="{{ if .Params.tags }}{{ delimit .Params.tags ", " | markdownify | plainify | safeHTML }}{{ else }}{{ with .Site.Params.keywords }}{{ delimit . ", " | markdownify | plainify | safeHTML }}{{ end }}{{ end }}">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<link rel="stylesheet" blocking="render" type="text/css" href="/style.css">
</head>
<body>

<header aria-label="Hero banner">
	<h1><a href="/">{{ .Site.Title }}</a></h1>
</header>

This changes a few things:

  • Anytime you’re handling page variables such as .Title or .Description, it’s best to pass them through the markdownify and plainify functions, which first converts any markdown into HTML and then strips the HTML tags. Only site variables like .Site.Title are skipped since it’s reasonable to assume they won’t use markdown or HTML in their value (but if you do, give them the same treatment).
    • E.g. *example* (markdownify)-> <em>example</em> (plainify)-> example. In places where it would be appropriate to have HTML tags, you can just use the markdownify function.
    • The safeHTML function is used to declare HTML unicode like &hellip; as safe to insert, you can tell when the function is needed if you see &amp; in place of the & symbol in unicodes (e.g. &amp;hellip).
  • <title> will be prefixed with the page .Title if the page isn’t the homepage (e.g. First blog post | Basic).
  • Two <meta> tags are included to define the page’s description and keywords. If the page doesn’t have these variables set, it uses the .Site.Params from the config as a fallback.
  • The header is a link for easier navigation, and is given aria-label="Hero banner" to give a name for screen readers to announce (and so it can be uniquely selected in CSS if needed).

This “better” header partial isn’t perfect either, and uses way too many if functions for the sake of simplicity, but, I’ll cover how to simplify logic like this in the future.

CSS styling

Here’s a basic stylesheet to make your website nicer to look at:

CSS for static/style.css.

html {
	box-sizing: border-box;
	font: calc(1rem + 0.2vw)/1.5 sans-serif;
	scroll-behavior: smooth;
}

header {
	border-bottom: 0.2rem solid grey;
	text-align: center;
}

nav, article, footer {
	background: lightgrey;
	max-width: 48rem;
	margin: 4rem auto 0;
	padding: 0.5rem;
}

nav {
	display: flex;
	flex-flow: column wrap;
	gap: 0.5rem;
}

nav > a {
	background: grey;
	padding: 0.5rem;
}

This stylesheet is solely for making differentiating neighbouring elements easier, and to keep things legible enough for demonstration purposes, feel free to come up with your own style.

More on layouts, and the range function

The range function

To begin, create the _default/section.html layout that was previously left out:

HTML for layouts/_default/section.html.

{{ partial "header.html" . }}

<header><h2>{{ .Title | markdownify }}</h2></header>

<nav>
{{ range .Pages }}
	<a href="{{ .RelPermalink }}" title="{{ .Title | markdownify | plainify }}">{{ .Title | markdownify }}</a>
{{ end }}
</nav>

{{ partial "footer.html" . }}

Here’s what’s happening:

  • range .Pages lists all pages under blog/, and it also rebinds the context to each page it finds.
  • .RelPermalink is the relative URL of the page, use .Permalink if you want absolute URLs.
  • .Title is the title of the page, and is being used as the link’s text as well as its tool-tip.

This list could become pretty long over time, so you can use conditional functions to only list the 10 most recent pages:

HTML for layouts/_default/section.html.

{{ partial "header.html" . }}

<header><h2>{{ .Title | markdownify }}</h2></header>

<nav>
{{ range first 10 .Pages }}
	<a href="{{ .RelPermalink }}" title="{{ .Title | markdownify | plainify }}">{{ .Title | markdownify }}</a>
{{ end }}
</nav>

{{ partial "footer.html" . }}

See the where function’s documentation for how to nest conditionals.

Smarter use of layouts

Now that you know what the range function is, you can use it in the homepage to list the website’s sections and taxonomies:

HTML for layouts/index.html.

{{ partial "header.html" . }}

<nav>
{{ range where .Site.Pages "Kind" "in" (slice "taxonomy" "section") }}
	<a href="{{ .RelPermalink }}" title="{{ .Title | markdownify | plainify }}">{{ .Title | markdownify }}</a>
{{ end }}
</nav>

{{ partial "footer.html" . }}

Doing where .Site.Pages "Kind" "in" (slice "taxonomy" "section") essentially asks for .Site.Pages which are either a kind of “taxonomy” or a kind of “section”.

And, so you can stop seeing “Page Not Found” everywhere, add the files for taxonomies and terms too:

HTML for layouts/_default/{taxonomy,term}.html.

{{ partial "header.html" . }}

<header><h2>{{ .Title | markdownify }}</h2></header>

<nav>
{{ range .Pages }}
	<a href="{{ .RelPermalink }}" title="{{ .Title | markdownify | plainify }}">{{ .Title | markdownify }}</a>
{{ end }}
</nav>

{{ partial "footer.html" . }}

Except, you don’t actually need these files. Sections, taxonomies, terms, and even the homepage are actually “list” pages, meaning instead of a separate layout file for each, you can instead create layouts/_default/list.html and reserve the other files for when you need to override the list layout.

So, since taxonomy.html and term.html have identical content, you can simplify this to:

HTML for layouts/_default/list.html.

{{ partial "header.html" . }}

<header><h2>{{ .Title | markdownify }}</h2></header>

<nav>
{{ range .Pages }}
	<a href="{{ .RelPermalink }}" title="{{ .Title | markdownify | plainify }}">{{ .Title | markdownify }}</a>
{{ end }}
</nav>

{{ partial "footer.html" . }}

And keep layouts/index.html & layouts/_default/section.html since they’re different.

Moreover, have you noticed the <a> element is getting duplicated? Perhaps you should create a partial for it? Actually no, there’s an even better way:

HTML for layouts/_default/li.html.

<a href="{{ .RelPermalink }}" title="{{ .Title }}">{{ .Title }}</a>

And then, replace every instance of <a> with .Render "li", for example:

HTML for layouts/_default/list.html.

{{ partial "header.html" . }}

<header><h2>{{ .Title | markdownify }}</h2></header>

<nav>
{{ range .Pages }}
	{{ .Render "li" }}
{{ end }}
</nav>

{{ partial "footer.html" . }}

The advantage is that you can define different list items for sections, taxonomies, or terms by creating layouts/name-of-list-page/li.html, and then specifying it with .Render (e.g. .Render "blog/li").

Or, you could just create layouts/arbitrary-name.html; .Render doesn’t care about the filename.

Context rebinding issues

So far, I’ve only demonstrated very simple context rebind scenarios, such as .Title inside of range .Pages, however, let me show you something more complicated to demonstrate how you can deal with trickier problems.

Let’s say you had something like this:

Example HTML of a .Site.Title variable inside a range function.

{{ range .Pages }}
	{{ .Site.Title }}
{{ end }}

In this case, .Site.Title would be blank, since the .Pages themselves don’t have a site title. However, you can fix this by rebinding the context of .Site.Title to the root of the page the variable is declared in using $:

Example HTML of a $.Site.Title variable inside a range function.

{{ range .Pages }}
	{{ $.Site.Title }}
{{ end }}

Now, this works as you’d expect. But, what if you moved $.Site.Title into a partial?

Example HTML of a partial.html partial used inside a range function.

{{ range .Pages }}
	{{ partial "example.html" . }}
{{ end }}

Example HTML of the partial.html partial.

{{ $.Site.Title }}

This won’t work because $ rebinds the context of .Site.Title to the root of the page the variable is declared in, however, the variable is declared in the partial, and the partial’s context is still inside range .Pages. You could solve this with {{ partial "example.html" $ }}, but, what if you wanted to access variables from inside range .Pages at the same time?

Example HTML of the partial.html partial, with both the .Description and $.Site.Title variables.

{{ .Description }}
{{ $.Site.Title }}

Thankfully, you can instead take advantage of the page variable:

Example HTML of the partial.html partial, with both the .Description and page.Site.Title variables.

{{ .Description }}
{{ page.Site.Title }}

The page variable is set to the root of the page you’re currently viewing, hence why it isn’t called .page since it can’t have a different context (it’s the same reason the now variable has no dot), and it can be used to reliably escape any nesting of functions and partials possible.

Also, if you find yourself using page.Site like above, you can instead use the site variable (e.g. site.Title).