Poop Sheet

Hugo

Creating a basic skeleton

At time of writing, this is a bit of a schlep because the recommended command

hugo new site whatever_name

does not produce any basic layouts files to get a newbie started. So rather run

hugo new theme whatever_name

which will produce themes/whatever_name. Then do

mv themes/whatever_name .
rmdir themes

then

cd whatever_name
hugo server

will bring up a simple Hugo site on http://localhost:1313 you can start altering to your taste.

skeleton_home.png

Understanding where Hugo is fetching the title, menu, summaries etc from requires diving into the basic directory structure.

Directory structure

A cool recent feature of Hugo is it’s ability to render ASCII art as SVG using GoAT, and I’ve used that to prettify the output of tree.

Up to version v0.146.0 there was a layouts/_default folder. These files have been moved directly into layouts/.

layouts/partials is now layouts/_partials as has layouts/shortcodes to layouts/_shortcodes.

w h                                                           a                                                           t e a a c d h i l L R s t v r s   o a u 1 a                 I E t h e c s   n t g 8 y                 C A a e r h e t a o n o E D t m _ e d t c j e _ p . u b h p _ s t t N M i f e n t e s s s n i o t t a o a p     e a e S E c a . a y f s t n s o s s m g a     c x r E . v t m p a d t m e e e r t o m m i o e e u m m e s _ p p p l o . . t f h h h m t i n . d c m s l a a x i o o o f h h i o e e e e e o o h o l t i i . n s s s . t t a o a a a n r n m t n . n n m d t t t h m m l t d d d u m . y m . m . . d e - - - b i t l l s e c j e . . s h . l i d c j x 1 2 3 r n m r s s r h h . t h c s s . . . y d l . s . . t t h m t o s m m m c e h . h h m m t l m d d d e x t h t t l l m l - . m t m m l c m l m l l a d l n y o n . j p g

Union file system

If you think you need a symbolic link in your project directory, use Hugo’s union file system instead.

I discovered the above the hardway. I wanted to use common layout files across several sites, and thought I’d do this the traditional Unix way with symbolic links. While this worked for layouts, when I tried to do this with content I got weird bugs because some “softlinked” files were ignored by Hugo.

Exactly what I wanted to achieve is covered by Hugo’s modules which I initially found incomprehensible because it’s built on Go’s module system, requiring a go.mod file and complicated pathnames like github.com/my/repo which is weird if you’re not using github.

Anyways, it turns out none of that is necessary if you simply add a {"module": { "mounts": ...}} section to your site configuration file as described in Union file system.

An advantage over symbolic links is global files can be kept in the module’s directories and local files in the given site’s directories.

Types

                            s a s m o c r l a b a r i p j l a c s e a s i f b y e e e e c r t n l o l s l l t r t o o e e e s i e a l m m m n g t e e e e g e i a n n n r n n t t t g p o i n t

Object and method names are capitalized. Although not required, to avoid confusion we recommend beginning variable and map key names with a lowercase letter or underscore.

layouts/baseof.html

<!DOCTYPE html>
<html lang="{{ or site.Language.LanguageCode site.Language.Lang }}" dir="{{ or site.Language.LanguageDirection `ltr` }}">
<head>
  {{ partial "head.html" . }}
</head>
<body>
  <header>
    {{ partial "header.html" . }}
  </header>
  <main>
    {{ block "main" . }}{{ end }}
  </main>
  <footer>
    {{ partial "footer.html" . }}
  </footer>
</body>
</html>

layouts/_partials/head.html

<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>{{ if .IsHome }}{{ site.Title }}{{ else }}{{ printf "%s | %s" .Title site.Title }}{{ end }}</title>
{{ partialCached "head/css.html" . }}
{{ partialCached "head/js.html" . }}

.IsHome

partialCached

layouts/_partials/head/css.html
{{- with resources.Get "css/main.css" }}
  {{- if hugo.IsDevelopment }}
    <link rel="stylesheet" href="{{ .RelPermalink }}">
  {{- else }}
    {{- with . | minify | fingerprint }}
      <link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
    {{- end }}
  {{- end }}
{{- end }}

resources.Get

This function operates on global resources. A global resource is a file within the assets directory, or within any directory mounted to the assets directory.

For page resources, use the Resources.Get method on a Page object.

hugo.IsDevelopment

hugo.IsProduction

hugo.IsServer

It seems nolonger necessary to have a config/_default/hugo.json and config/production/hugo.json

layouts/_partials/header.html

<h1>{{ site.Title }}</h1>
{{ partial "menu.html" (dict "menuID" "main" "page" .) }}

The global site has the advantage that .Site only works in the current context, and needs to be addressed as making it akin to $.Site to avoid scoping problems.

To simplify your templates, use the global site function regardless of whether the Site object is in context.

layouts/_partials/menu.html

This has an inline partial

{{- /*
Renders a menu for the given menu ID.

@context {page} page The current page.
@context {string} menuID The menu ID.

@example: {{ partial "menu.html" (dict "menuID" "main" "page" .) }}
*/}}

{{- $page := .page }}
{{- $menuID := .menuID }}

{{- with index site.Menus $menuID }}
  <nav>
    <ul>
      {{- partial "inline/menu/walk.html" (dict "page" $page "menuEntries" .) }}
    </ul>
  </nav>
{{- end }}

{{- define "_partials/inline/menu/walk.html" }}
  {{- $page := .page }}
  {{- range .menuEntries }}
    {{- $attrs := dict "href" .URL }}
    {{- if $page.IsMenuCurrent .Menu . }}
      {{- $attrs = merge $attrs (dict "class" "active" "aria-current" "page") }}
    {{- else if $page.HasMenuCurrent .Menu .}}
      {{- $attrs = merge $attrs (dict "class" "ancestor" "aria-current" "true") }}
    {{- end }}
    {{- $name := .Name }}
    {{- with .Identifier }}
      {{- with T . }}
        {{- $name = . }}
      {{- end }}
    {{- end }}
    <li>
      <a
        {{- range $k, $v := $attrs }}
          {{- with $v }}
            {{- printf " %s=%q" $k $v | safeHTMLAttr }}
          {{- end }}
        {{- end -}}
      >{{ $name }}</a>
      {{- with .Children }}
        <ul>
          {{- partial "inline/menu/walk.html" (dict "page" $page "menuEntries" .) }}
        </ul>
      {{- end }}
    </li>
  {{- end }}
{{- end }}

list.html vs section.html etc

The lookup-order is sectionname.html > section.html > list.html

list.html can double as home.html, taxonomy.html, term.html… any page with an _index.md file.

layouts/home.html

index.html takes precedence, but I find that too confusing with index.md pages.

{{ define "main" }}
  {{ .Content }}
  {{ range site.RegularPages }}
    <h2><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></h2>
    {{ .Summary }}
  {{ end }}
{{ end }}

Summary

layouts/section.html

{{ define "main" }}
  <h1>{{ .Title }}</h1>
  {{ .Content }}
  {{ range .Pages }}
    <h2><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></h2>
    {{ .Summary }}
  {{ end }}
{{ end }}

layouts/_defaults/single.html

{{ define "main" }}
  <h1>{{ .Title }}</h1>

  {{ $dateMachine := .Date | time.Format "2006-01-02T15:04:05-07:00" }}
  {{ $dateHuman := .Date | time.Format ":date_long" }}
  <time datetime="{{ $dateMachine }}">{{ $dateHuman }}</time>

  {{ .Content }}
  {{ partial "terms.html" (dict "taxonomy" "tags" "page" .) }}
{{ end }}

layouts/partials/terms.html

{{- /*
For a given taxonomy, renders a list of terms assigned to the page.

@context {page} page The current page.
@context {string} taxonomy The taxonony.

@example: {{ partial "terms.html" (dict "taxonomy" "tags" "page" .) }}
*/}}

{{- $page := .page }}
{{- $taxonomy := .taxonomy }}

{{- with $page.GetTerms $taxonomy }}
  {{- $label := (index . 0).Parent.LinkTitle }}
  <div>
    <div>{{ $label }}:</div>
    <ul>
      {{- range . }}
        <li><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></li>
      {{- end }}
    </ul>
  </div>
{{- end }}

layouts/partials/menu.html

{{- /*
Renders a menu for the given menu ID.

@context {page} page The current page.
@context {string} menuID The menu ID.

@example: {{ partial "menu.html" (dict "menuID" "main" "page" .) }}
*/}}

{{- $page := .page }}
{{- $menuID := .menuID }}

{{- with index site.Menus $menuID }}
  <nav>
    <ul>
      {{- partial "inline/menu/walk.html" (dict "page" $page "menuEntries" .) }}
    </ul>
  </nav>
{{- end }}

{{- define "partials/inline/menu/walk.html" }}
  {{- $page := .page }}
  {{- range .menuEntries }}
    {{- $attrs := dict "href" .URL }}
    {{- if $page.IsMenuCurrent .Menu . }}
      {{- $attrs = merge $attrs (dict "class" "active" "aria-current" "page") }}
    {{- else if $page.HasMenuCurrent .Menu .}}
      {{- $attrs = merge $attrs (dict "class" "ancestor" "aria-current" "true") }}
    {{- end }}
    {{- $name := .Name }}
    {{- with .Identifier }}
      {{- with T . }}
        {{- $name = . }}
      {{- end }}
    {{- end }}
    <li>
      <a
        {{- range $k, $v := $attrs }}
          {{- with $v }}
            {{- printf " %s=%q" $k $v | safeHTMLAttr }}
          {{- end }}
        {{- end -}}
      >{{ $name }}</a>
      {{- with .Children }}
        <ul>
          {{- partial "inline/menu/walk.html" (dict "page" $page "menuEntries" .) }}
        </ul>
      {{- end }}
    </li>
  {{- end }}
{{- end }}

Front Matter

Draft, future, and expired content

Something I and probably many other newbies tripped over was that getting the values wrong in any of these 4 key-value pairs prevents the page from rendering.

Page Methods

Frontmatter Accessors

PAGE.Date 2024-01-15 13:34:29 +0200 +0200

PAGE.ExpiryDate 0001-01-01 00:00:00 +0000 UTC

PAGE.PublishDate 2024-01-15 13:34:29 +0200 +0200

PAGE.Lastmod 2025-08-05 18:38:50 +0200 +0200

PAGE.Description

PAGE.Draft false

PAGE.GetTerms "artists" Pages(0)

PAGE.Keywords []

PAGE.Layout

PAGE.Title Hugo

PAGE.LinkTitle hugo

PAGE.Params map[date:2024-01-15 13:34:29 +0200 +0200 draft:false iscjklanguage:false lastmod:2025-08-05 18:38:50 +0200 +0200 linktitle:hugo publishdate:2024-01-15 13:34:29 +0200 +0200 title:Hugo]

PAGE.Sitemap { -1 sitemap.xml false}

PAGE.Slug

PAGE.Summary

PAGE.Weight 0

PAGE.Aliases []

Links

PAGE.Permalink https://poopsheet.co.za/html-tools/hugo/

PAGE.RelPermalink /html-tools/hugo/

PAGE.GitInfo

.AuthorDate 2025-08-05

.CommitDate 2025-08-05

.AuthorEmail robert.joeblog@gmail.com

.AuthorName Robert Laing

.Subject started hugo/frontmatter section

PAGE.CodeOwners: []

PAGE.Kind: section

PAGE.Type: html-tools

PAGE.BundleType: branch

PAGE.AllTranslations: Pages(1)

PAGE.AlternativeOutputFormats: [{alternate {rss application/rss+xml index alternate false false true false false false false 0} /html-tools/hugo/index.xml https://poopsheet.co.za/html-tools/hugo/index.xml}]

PAGE.Content

PAGE.CurrentSection: /home/roblaing/webapps/frontiersoftware/content/html-tools/hugo/_index.md

PAGE.Data

PAGE.Eq

PAGE.File: _index

PAGE.FirstSection: /home/roblaing/webapps/frontiersoftware/content/html-tools/_index.md

PAGE.Fragments: {[0xc00278dc80] [creating-a-basic-skeleton directory-structure draft-future-and-expired-content front-matter frontmatter-accessors layouts_defaultssinglehtml layouts_partialsheadcsshtml layouts_partialsheaderhtml layouts_partialsheadhtml layouts_partialsmenuhtml layoutsbaseofhtml layoutshomehtml layoutspartialsmenuhtml layoutspartialstermshtml layoutssectionhtml listhtml-vs-sectionhtml-etc page-methods types union-file-system] map[creating-a-basic-skeleton:0xc00278d9c0 directory-structure:0xc00278dd00 draft-future-and-expired-content:0xc0002cd280 front-matter:0xc0002cc840 frontmatter-accessors:0xc0003177c0 layouts_defaultssinglehtml:0xc00027f500 layouts_partialsheadcsshtml:0xc0001b4780 layouts_partialsheaderhtml:0xc0001b5440 layouts_partialsheadhtml:0xc0001737c0 layouts_partialsmenuhtml:0xc00025af40 layoutsbaseofhtml:0xc000173680 layoutshomehtml:0xc00025bdc0 layoutspartialsmenuhtml:0xc00027f900 layoutspartialstermshtml:0xc00027f840 layoutssectionhtml:0xc00027f300 listhtml-vs-sectionhtml-etc:0xc00025bc00 page-methods:0xc0002e2c40 types:0xc00278df40 union-file-system:0xc00278de00]}

PAGE.FuzzyWordCount : 0

PAGE.GetPage PATH

PAGE.HeadingsFiltered: []

PAGE.OutputFormats: [{canonical {html text/html index canonical false true false false false false true 10} /html-tools/hugo/ https://poopsheet.co.za/html-tools/hugo/} {alternate {rss application/rss+xml index alternate false false true false false false false 0} /html-tools/hugo/index.xml https://poopsheet.co.za/html-tools/hugo/index.xml}]

PAGE.Resources: [skeleton_home.png]

Pages Methods

Sort Orders

Inserting strings is called interpolation. Extracting substrings could be called extrapolation.

Some common synonyms of interpolate are insert, insinuate, intercalate, interject, interpose, and introduce. While all these words mean “to put between or among others,” interpolate applies to the inserting of something extraneous or spurious. https://www.merriam-webster.com/thesaurus/interpolate

frontmatter

Each content file, ie an index.md for a leaf or _index.md for a branch, has what Hugo calls front matter, ie metadata that splits these files into a head and body section.

The default skeleton archetypes/default.md is

+++
date = '{{ .Date }}'
draft = true
title = '{{ replace .File.ContentBaseName "-" " " | title }}'
+++

While the skeleton uses toml, it can be yaml or json, which I find simplest.

{
  "date":  "{{ .Date }}",
  "draft": true,
  "title": "{{ replace .File.ContentBaseName "-" " " | title }}"
}

Setting draft to true by default tripped me up, and I’m sure most other novices since that means your page doesn’t get rendered by default.

data

Hugo allows data in a variety of formats to sourced from several different places.

Global data is typically stored in the /data subdirectory.

Local data uses .Resources.Get which reads files in the current bundle, not be confused with resources.Get which fetches global resources.

JSON unmarshal and remarshal

  {{ $schema := dict }}
  {{ with .Resources.Get "schema.json" }}
    {{ with . | transform.Unmarshal }}
      {{ $schema = . }}
    {{ end }}
  {{ end }}
  <pre>{{ transform.Remarshal "json" $schema }}</pre>

JSON types once marshalled

Object
map[string]interface {} Can be tested with reflect.IsMap
Array
[]interface {} Can be tested with reflect.IsSlice
String
string
Number
float64
Null
nil

assets

Global resources are kept in the assets directory.

I initially didn’t get the advantage of putting CSS and JS files in the assets directory rather than the static directory and found it a little confusing that the reason is explained in Hugo Pipes which are unrelated to the | operator in template functions which are called Go Pipes.

Before discovering the advantages of the assets folder over static, I had a problem that browsers wouldn’t load upload style and script files. Filenames in the assets folder get a unique fingerprint (aka hashes) when they are changed. It also allows translation of style and JS files from other languages if desired, and minification.

modules

Modules

A Hugo module is a go module, so requires the same steps:

mkdir mymod
cd mymod
hugo mod init mymod
go: creating new go.mod: module mymod

hugo mod

Hugo Modules: everything you need to know!

Go Modules

Working with Hugo Module Locally

Tutorial

hugo mod init github.com/gohugoio/myShortcodes

Configuration

{
   "module": {
      "noProxy": "none",
      "noVendor": "",
      "private": "*.*",
      "proxy": "direct",
      "replacements": "",
      "vendorClosest": false,
      "workspace": "off",
      "hugoVersion": {
         "extended": false,
         "max": "",
         "min": ""
      },
      "imports": [
         {
            "disable": false,
            "ignoreConfig": false,
            "ignoreImports": false,
            "path": "github.com/gohugoio/hugoTestModules1_linux/modh1_2_1v"
         },
         {
            "path": "my-shortcodes"
         }
      ],
      "mounts": [
         {
            "source": "content",
            "target": "content"
         },
         {
            "source": "static",
            "target": "static"
         },
         {
            "source": "layouts",
            "target": "layouts"
         },
         {
            "source": "data",
            "target": "data"
         },
         {
            "source": "assets",
            "target": "assets"
         },
         {
            "source": "i18n",
            "target": "i18n"
         },
         {
            "source": "archetypes",
            "target": "archetypes"
         }
      ]
   }
}

path Can be either a valid Go Module module path, e.g. github.com/gohugoio/myShortcodes, or the directory name for the module as stored in your themes folder.

menus

Directory traversal

I’ve developed my own homegrown way of recursively walking a Hugo site’s content tree as described in this discourse thread since the official way of putting the menu in the site config file isn’t what I want as I add and remove pages.

With more experience, I changed the deprecated template with an inline partial

My code for layouts/partials/menu.html looks like this:

<nav>
{{ partial "inline/walk.html" (dict "dir" .Site.Home.Pages) }}

{{ define "partials/inline/walk.html" }}
  <ol>
  {{ range .dir }}
    {{ if (eq .BundleType "branch") }}
        <li><a href="{{ .RelPermalink }}">{{ .Title }}</a>
        {{ partial "inline/walk.html" (dict "dir" .Pages) }}
        </li>
    {{ else }}
      <li><a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
    {{ end }}
  {{ end }}
  </ol>
{{ end }}
</nav>

Section menu

Hugo’s default menu system requires everything to be manually entered into the system configuration file.

branch

Sections vs Taxonomies

An imortant difference between content/mysection and content/mytaxonony is there may not be an _index.md file in a taxonomy whereas there should be for a section.

leaf

Files named index.md (as opposed to _index.md) are leaves, and use layouts/page.html (historically layouts/single.html) as their template.

The template for layouts/page.html by hugo new theme whatever_name looks like this.

{{ define "main" }}
  <h1>{{ .Title }}</h1>

  {{ $dateMachine := .Date | time.Format "2006-01-02T15:04:05-07:00" }}
  {{ $dateHuman := .Date | time.Format ":date_long" }}
  <time datetime="{{ $dateMachine }}">{{ $dateHuman }}</time>

  {{ .Content }}
  {{ partial "terms.html" (dict "taxonomy" "tags" "page" .) }}
{{ end }}

The variables .Title and .Date access the values stored in the frontmatter’s title and date entries.