Building DUALUSE

Adventures in sunk-cost

Travis dropped

Publishing has always been a bit tricky for me; as a strategy to avoid generating content I hold some synthetic requirements near and dear. I look for, in any generator, control over the output - and minimalism. WordPress, 3rd party venues (Medium, etc) were definitely out. Previously this meant hand-rolling HTML and CSS from scratch for every page without the help of a generator. Thankfully, a colleague (thanks Parsia!) clued me in to Hugo - and I set about building a Hugo site the hard way.

Hugo’s quick start guide encourages you to select an existing template, and begin throwing down your content. Someone else’s CSS and HTML? Totally unacceptable.

Most of you are familiar with the virtues of a programmer. There are three, of course: laziness, impatience, and hubris.

In This Guide

Read this post top-to-bottom to learn;

I assume familiarity with programming concepts; variables, control flow, and markup/styling; Markdown, HTML, and CSS. I skip right over setting up a Hugo working directory and site, for that begin with the official quick start guide - and return when you’re ready to create your own templates.

Dev Goals

I needed clean and semantically correct HTML51, along with a tight stylesheet (from scratch; with help from MDN2, citizens of StackOverflow, ALA, and others), that rendered well across a variety of screens and in print. I wanted to spend my time fussing over fonts, and waste as much of my visitors bandwidth as possible loading them. In line with doing everything from scratch I wanted the freedom to amuse myself with bizarre details. Finally - no JavaScript3 - trackers - dynamic content - anything. Nope. HTML5 and CSS3 would be enough.

Building The Hugo Templates

Equipped with the essentials; caffeine, google, and the Hugo documentation I set about adopting a brand new theme called, creatively enough, sparse - easy;

# hugo new theme sparse

This command generates a bare-bones theme in your Hugo working directory:

# tree ./themes/sparse
./themes/sparse
├── archetypes
│   └── default.md
├── layouts
│   ├── 404.html
│   ├── _default
│   │   ├── baseof.html
│   │   ├── list.html
│   │   └── single.html
│   ├── index.html
│   └── partials
│       ├── footer.html
│       ├── header.html
│       └── head.html
├── LICENSE
├── static
│   ├── css
│   └── js
└── theme.toml

Bare bones theme file and directory listing.

Structure

First up, I started prototyping a static page with dummy content that roughly matched the type of structure I was looking for:

<html>
	<head>
		<title>...</title>
		<link  ... rel="stylesheet"/>
	</head>
	<body>
		<header><!--Some type of identity.--></header>
		<main>
			<article>
				<header>...</header>
				<!--What you actually care about.-->
			</article>
		</main>
		<footer><!--Trash--></footer>
	</body>
</html>

Like every other HTML5 structure you’ve ever seen.

Thanks to CSS3, and my personal hangups, I was able to keep to this structure, winding up with:

<html>
	<head>
		<title>...</title>
		<meta  ... />
		<link  ... rel="stylesheet"/>
		<link  ... rel="icon" />
	</head>
	<body>
		<header><a><pre>...</pre></a></header>
		<main>
			<div><!--No good flexbox hack.--></div>
			<article>
				<header>...</header>
				<!--What you actually care about.-->
			</article>
			<aside><!--Table of Contents--></aside>
		</main>
		<footer><!--Copyright Notice--></footer>
	</body>
</html>

Thanks, attention to irrelevant details.

Let’s take a walk through sparse and see how everything fits together.

Common Template Sections

Blackfriday generates pristine markup for the main article content, and all you have to do is get Hugo to render it to the right spot. In a Hugo template, you have a few essential components for content layout, the most fundamental being baseof.html located in a theme’s ./_default/ directory.

Base Template

The base template renders the common frame for all content on your Hugo site - here’s mine;

<!DOCTYPE html>
<html>
{{- partial "head.html" . -}}
<body>
	<header canUrl="{{ .Permalink }}">
		{{ partial "header.html" . }}
	</header>
	<main>
		{{ block "main" . }}{{- end }}
	</main>
	<footer>
	{{- partial "footer.html" . -}}
	</footer>
</body>
</html>

sparse’s baseof.html.

You can spot 6 go-template functions, doing 5 discrete things, in the above sample;

{{- partial "head.html" . -}}
{{ and }} declare the start and end of control, respectively. - instructs rendering to collapse whitespace before or after, in this case both directions. partial4 is a function that looks up the parameter in the filesystem for inclusion. This one loads my header content from ./layouts/partials/head.html.
{{ .Permalink }}
A function is optional - if you provide a bare reference to a variable, it’ll be rendered to HTML and inserted. .Permalink is a Hugo Page Variable5 that produces a canonical URL to the page being rendered.
{{ partial "header.html" . }}
This grabs DUALUSE’s banner / home-link from ./layouts/partials/header.html.
{{ block "main" . }}{{- end }}
The block/end pair declare a section of this document - that a later step can grab a handle on to inject content. In this case the main block is populated with whatever Hugo thinks is a page’s core content. A content page fits the bill, but so do special list and index pages. Why does block have a inner part? If Hugo didn’t replace the main block, the page would be rendered with these contents as default.
{{- partial "footer.html" . -}}
FINALLY include ./layouts/partials/footer.html.

Zooming out from baseof, Hugo’s engine is iterating through various bits of content to render; the core layout, defining a page’s common elements, is handled by baseof, and depending on the type of content, main is rendered by a content layout type page; list, single, index, or 404. After Hugo has a layout it renders the content layout page into its block main.

Here it is in graph form. Don't worry, we'll keep expanding on this. Here it is in graph form. Don't worry, we'll keep expanding on this.
Here it is in graph form. Don’t worry, we’ll keep expanding on this.

In the figure above, to the left sits Hugo - and the relevant components to rendering - in the middle rests the templates the engine knows to look for and to the right, data sources - either dynamic (variables) or other components (partials) from the filesystem.

Let’s explore the other partials before getting into layout type main sections.

baseof imports this section to define the contents of html>head - we set meta tags, title, and stylesheets here.

<title>{{ .Site.Title }}</title>
<meta name="viewport" content="width=device-width" />
<meta name="theme-color" content="#00796b" />
<link href="/css/main.css" rel="stylesheet" />

{{- if (isset .Params "description") -}}
<meta name="description" content="{{ .Params.description }}" />
{{ else if (isset .Params "summary") }}
<meta name="description" content="{{ .Params.summary }}" />
{{ else if .Summary}}
<meta name="description" content="{{ .Summary }}" />
{{ else }}
<meta name="description" content="DUAL USE features DUAL USE TECHNOLOGY." />
{{ end }}
...junk...

sparse’s head.html.

We’ve already seen template variable resolution in baseof, but control flow elements and the .Site variables are new.

{{ .Summary }}
A page variable that includes a Hugo generated summary of the page content.
{{ .Site.Title }}
.Site contains Hugo Site Variables6 - populated, in part, from your Hugo site’s config.toml. .Title, as expected is defined by config.toml’s title setting.
.Params "description"
.Params contains a dictionary of arbitrary values set in page content. In this case, if content’s front-matter7 sets description="Tight Description", you can access it with .Params.description.
{{- if (isset .Params "description") -}}
if is a go template action8 that provides a conditional statement and isset9 is a Hugo function testing the .Params dictionary for description.
{{ if (STATEMENT) }} ... {{ end }}
Is the correct formatting for a conditional block, which can be further extended;
{{ else if (STATEMENT)}} & {{ else }}
Can appear between an {{ if ...}} and its corresponding {{ end }}.

The purpose of this block of logic around <meta name="description"> is to let us set its content as the contents of front-matter description or summary values, and fall back on the automatically generated Summary page variable - and provide a general description if all else fails.

The head component pulls from both Page and Site variables. The head component pulls from both Page and Site variables.
The head component pulls from both Page and Site variables.
<a class="noul" href="{{ .Site.BaseURL }}">
  <pre id="eye" role="img" alt="Who Watches?">...junk...</pre>
</a>

sparse’s header.html.

We know what’s going on now, we’re generating a link base to the BaseURL defined in config.toml’s baseURL setting.

The header component pulls from Site variables. The header component pulls from Site variables.
The header component pulls from Site variables.
{{ $related := .Site.RegularPages.Related . | first 5 }}
{{ with $related }}

<header> 
	<h1>Related Work</h1>
</header>

<section class="posts">

	{{ range . -}}
	<article class="post">
	  <header>
	    <h1><a href="{{ .Permalink }}">{{ .Title }}</a></h1>

	    <p class="demo">{{ if isset .Params "author" }} {{.Params.author}} dropped {{ end }}<time datetime={{ .Date.Format "2006-01-02T15:04:05Z0700" }} class="post-date">{{ .Date.Format "Mon, Jan 2, 2006" }}</time></p>
	  </header>
	  <p>{{ .Summary }}</p>
	  {{ if .Truncated }}
	  <div class="flexSpacer"></div>
	  <div class="read-more-link">
	    <a href="{{ .RelPermalink }}">Read More…</a>
	  </div>
	  {{ end }}
	</article>
	{{- end }}
{{ end }}

</section>

    <nav>
      <ul class="sidebar-nav">
        {{ range .Site.Menus.main -}}
          <li><a href="{{.URL}}"> {{ .Name }} </a></li>
        {{- end }}
      </ul>
    </nav>

  <div class="flexSpacer"></div>


<p class="copyright">{{ with .Site.Params.copyright }}{{.}}{{ else }}&copy; {{ now.Format "2006"}}. All rights reserved. {{end}}</p>
<script type="text/javascript">
	console.log("%c"+document.getElementById("eye").innerHTML," color: #242424");
</script>
<!-- END SCENE -->

sparse’s footer.html. getElementById().innerHTML, that’s right.

The footer has some new constructs to review, go template pipelines10, go template variables11, and two new actions with and range. We’re using these to construct the bottom of the page - and list related articles.

.Site.RegularPages.Related
This site variable resolves other Hugo pages that are considered ‘related’. How Hugo fills related pages is configurable in your site config12. This variable is special, it’s a collection.
.Site.Menus.main
Another collection, this time containing references to URLs configured in the site config, or in page front-matter.
$related := .Site.RegularPages.Related
$ denotes the start of a name for a go template variable and := is the declare and assign operator.
{{ $related := .Site.RegularPages.Related . | first 5 }}
Using | is referred to as pipeline chaining, functions which consume and emit pipelines of compatible types can be chained together. Here, the call first 5 grabs the first 5 items from .Related. It may not be obvious, but everything to the right of an assignment, :=, including pipelines, gets evaluated first.
{{ with ... }} {{ end }}
with sets the scope for the enclosed block to the provided argument, if the provided argument is empty, the whole block is skipped. As we use it here, with, like if, supports an {{ else }} block.
{{ with $related }}{{ range .}}{{ end }}{{ end }}
range will iterate over the provided collection, and execute the enclosed block with the current item’s scope. In this case, a single related page. In the top example, {{ range . }} is iterating over the scope set with {{ with $related }}, $related.
Footer really isn't that complicated. Footer really isn't that complicated.
Footer really isn’t that complicated.

Awesome, we’ve built a functional layout, and had a taste of working with templates. Next, we explore the various Hugo layout types that provide rendering for the main block.

Content Sections

Content sections get loaded up depending on what type of context Hugo is trying to render. Let’s cover the three special layouts I’m using, Lists, Singles, and the Index.

List

When it’s not rendering individual content, like a post, Hugo spends most of its time generating lists of things. Lists of items sharing a category or tag, lists of content under a content section. Tons of lists.

{{ define "main" -}}
<section class="posts">
{{ range .Data.Pages -}}
	<article class="post">
	  <header>
	    <h1><a href="{{ .Permalink }}">{{ .Title }}</a></h1>

	    <p class="demo">{{ if isset .Params "author" }} {{.Params.author}} dropped {{ end }}<time datetime={{ .Date.Format "2006-01-02T15:04:05Z0700" }} class="post-date">{{ .Date.Format "Mon, Jan 2, 2006" }}</time></p>
	  </header>
	  <p>{{- if (isset .Params "description") -}}
		{{- .Params.description | markdownify -}}
		{{ else if (isset .Params "summary") }}
		{{- .Params.summary | markdownify -}}
		{{ else }}
		{{- .Summary -}}
		{{ end }}</p>
	  {{ if .Truncated }}
	  <div class="flexSpacer"></div>
	  <div class="read-more-link">
	    <a href="{{ .RelPermalink }}">Read More…</a>
	  </div>
	  {{ end }}
	</article>
{{- end }}
</section>
{{- end }}

./_default/list.html

You should be familiar with everything going on here. Hugo hands us a scope with .Data.Pages populated with the list of things it wants us to list, and we range through it to render individual items.

{{ define "main" -}}{{- end }}
Hugo will hunt for a block "main" to drop this rendered content into. Declaring it here means we’ll get inserted into baseof.
markdownify13
Hugo runs whatever you pipe to this function through Blackfriday for rendering, providing pretty-printing of stuff specified in front matter variables.

Single

Here’s the main show; the Single Page Template.

{{ define "main" -}}
<div id="mainPad">&nbsp;</div>

<article>
	<header>
		<h1>{{ .Title }}</h1>
		<p>{{ .Params.subtitle }}</p>
		<p class="demo">{{.Params.author}} dropped <time datetime={{ .Date.Format "2006-01-02T15:04:05Z0700" }} class="post-date">{{ .Date.Format "Mon, Jan 2, 2006" }}</time></p>
	</header>
  {{ .Content }}
</article>
<aside id="toc">
	{{ if and (gt .WordCount 400 ) (.Params.toc) }}

	<header>
		<h1>Contents</h1>
	</header>
	    {{.TableOfContents}}
	{{ end }}

</aside>

{{- end }}

./_default/single.html

Hugo has rendered a markdown file into .Content using Blackfriday. Let’s just review the conditional here;

.WordCount
A Hugo-populated page variable with the number of words found in .Content.
{{ if and (gt .WordCount 400 ) (.Params.toc) }}
Familiar infix notation for this is if (.WordCount > 400) AND (.Params.toc).and and gt aren’t actually operators, they’re functions. We’re actually looking at if ( and(gt(.WordCount,400), .Params.toc)), and this is a little clearer when you think of it that way. More than 400 words? Front matter sets toc=true? Render a Table of Contents.

Index

I won’t bother - my ./index.html is just a copy of the ./_default/list.html template. You can be more adventurous and specify a totally unique and different Homepage Template. Maybe with some bio info.

Shortcodes

That’s it - we’re done with Hugo templates. Markdown and the extensions provided by Blackfriday cover most things you’d want to generate as content - but there are still a few holes in functionality. That’s where Hugo Shortcodes come in.

Shortcodes are provided by a template - and reside under ./layouts/shortcodes as .html files.

Most of my page content is going to contain some code - you can defintely use Markdown code fences, indents, or what-have-you for a pleasant experience - but I wanted all my code-blocks wrapped up as figure elements with figcaptions.

<figure>{{.Inner -}}
<figcaption><p>{{.Get 0 | markdownify }}</p></figcaption>
</figure>

The code shortcode.

All you have to do to make this shortcode available is drop this in shortcodes/code.html.

{{<code "The `code` shortcode.">}}
{{<highlight go-html-template>}}
<figure>{{.Inner -}}
<figcaption><p>{{.Get 0 | markdownify }}</p></figcaption>
</figure>
{{</highlight>}}
{{</code>}}

Using the code shortcode.

Hopefully you can see what’s going on from the usage example;

{{<code "The `code` shortcode.">}}{{</code>}}
These open and close a shortcode - they kind of look like go’s template control blocks, but with XML style elements. Funky.
{{.Inner -}}
This is whatever content appeared in between the shortcode block start and end. You can tell from the example above that .Inner contents get automatically processed through Blackfriday, so you can nest shortcodes and Markdown formatting.
.Get 0
This command grabs the first argument supplied in the shortcode opening statement - in our case, The `code` shortcode.. We pass this to markdownify because, unlike .Inner, these arguments aren’t processed by Blackfriday.

bc provides blockquotes, like the one you saw at the top of the article. This shortcode takes a parameter specifying the quote credit, with the quote text in the body.

{{$cite:=.Get 0 | htmlEscape}}
<blockquote {{ printf "cite=%q" $cite  | safeHTMLAttr }}>{{.Inner | markdownify -}}
</blockquote>

The bc shortcode.

Which I use like;

{{<bc "Larry Wall">}}
Most of you are familiar with the virtues of a programmer.  There are three, of course: laziness, impatience, and hubris.
{{</bc>}}

Using the bc shortcode.

Remember that context-aware encoding that go templates perform? Well, it was doing absolutely the wrong thing for the cite attribute here - URL Encoding instead of HTML Entity Encoding.

{{$cite:=.Get 0 | htmlEscape}}
HTML Entity Encode anything that might break out of the attribute value context. (I wouldn’t rely on htmlEscape to prevent injection attacks, and neither should you.)
{{ printf "cite=%q" $cite | safeHTMLAttr }}
printf is format strings; put $cite into cite=%q according to %q. safeHTMLAttr tells the engine that no further encoding of this value is necessary for the HTML Attribute context.

Wrap

You should have a better idea of how all the separate Hugo template components work together to create a site - I’ll run through DUALUSE’s styling in a later post.

Everything Covered. Everything Covered.
Everything Covered.

If you see anything wrong in this post - or think something needs clarification, let me know and I’ll update it.