Introduction

To make this website, I used the static site generator Hugo with the PaperMod theme. I use Netlify to host.

In this post, I provide an overview of Hugo/PaperMod and describe the modifications I made to the original theme in setting up this website. All modifications are shown via diff output as HTML pages in PaperMod diff 1.

Changes that I don’t describe here will definitely show up in PaperMod diff, likely with comments about what they do and where I got them from if I got them from somewhere. I’ll try to keep this post updated, but please feel free to comment if I should describe something here!

Audience

This post was written with two audiences in mind: people who are new to Hugo and people with Hugo experience. If you’re new to Hugo, then I recommend reading this post from start to finish. If you have experience with Hugo, then I recommend skimming. Specifically, I would read Clone/fork for setup instructions. Then skip over the Hugo/PaperMod overview to get to My website, which describes specific features of my website. Please see the Table of Contents above if you haven’t already.

Credit

This post is inspired by Konstantin’s similarly-titled blog post.

Resources

Before proceeding, I’d like to share five excellent resources that I used and reference throughout this post. The top three are crucial because they cover Hugo and PaperMod. The bottom two describe very specific features that I reference in My website, so don’t worry about those yet.

My goal is to make the rest of this post self-contained (and link to brief external resources). If something doesn’t make sense, please use the first three resources.

As mentioned earlier, I also created PaperMod diff 1 to show modifications I made to the theme in a readable way. In short, to modify a file from the theme, you create a copy of the file and make edits in the copy, but this makes it hard to see what was modified. So, PaperMod diff shows diffs between the files.

Setup

First, follow the steps here to install Hugo. You should also have Git installed. 7

Clone/fork

Run these commands.

git clone --recurse-submodules --no-single-branch https://github.com/jesse-wei/jessewei.dev-PaperMod.git
cd jessewei.dev-PaperMod
hugo server

--recurse-submodule clones the PaperMod submodule.

Additionally, you may clone with --depth=1 to save some disk space.

hugo server starts up a server for you to view the site.

Why clone/fork?

At this point, you could start from scratch instead of cloning/forking my website. However, resources 2 and 3 already describe how to start from scratch.

So, I’ll cut to the chase and have you clone my website and describe changes I made.

If something doesn’t make sense, then I recommend first reading/watching the resources.

In addition, if you notice some specific feature of my website that I don’t explain, then use inspect element to inspect the code for that feature. Then run grep -ir in this repo to find the relevant code. -r makes the search recursive, and -i makes the search case-insensitive.

Overview of Hugo and PaperMod

Skim this section or skip to My website if you’re already familiar with Hugo and PaperMod (e.g., if you read/watched the resources).

Repo structure

Please read Hugo’s directory structure (3 min) and the top part of Hugo’s content organization (1 min) for a general overview of Hugo’s directory structure. I’ll describe more in-depth below.

Here’s the structure of my repository. I omit unimportant stuff and stuff I don’t use, and certainly changes will be made, but this is all the important stuff:

jessewei.dev-PaperMod
├── assets                  Overrides PaperMod/assets. Contains mostly CSS, some JS
│   └── css
├── config.yml              Site-wide configuration file
├── content
│   ├── about.md
│   ├── archives.md
│   ├── classes
│   ├── discord.md
│   ├── posts               List layout
│   ├── privacy.md
│   ├── projects            List layout
│   ├── search.md
│   └── teaching            List layout
│       └── act             List layout within list layout
├── layouts                 Overrides PaperMod/layouts
│   ├── _default            Layout of entire pages (specifically, the <main> element)
│   │   └── single.html
│   └── partials            Layout of components of a page
│       ├── comments.html
│       ├── extend_head.html
│       ├── footer.html
│       ├── header.html
│       ├── index_profile.html
│       └── social_icons.html
├── scripts                 My scripts
├── static                  Images, etc.
│   ├── SAPsim_still_cropped.jpg
│   ├── SAPsim_still_full.jpg
│   └── ...
└── themes
    └── PaperMod

There are 4 crucial parts of the repo: config, content, layouts and assets, and static.

config.yml

This is the configuration file for the website containing all site-wide parameters.

content/

This is the directory where site content (posts) goes.

Site content should be Markdown files.

The front matter of a Markdown file contains metadata about the post. For example, the front matter of this post is

---
title: "Overview of Hugo/PaperMod and Setting Up This Site"
date: 2023-05-14T20:13:59-04:00
draft: false
cover:
    image: img/hugo_logo_wide.svg
    alt: "Hugo logo"
    caption: "Hugo logo"
    hidden: false
summary: "This post provides an overview of Hugo (PaperMod theme) and details the steps I took in setting up this website."
tags: ["Hugo", "PaperMod", "Markdown", "HTML", "CSS", "Blog", "Website", "Portfolio"]
---

This information is used to generate the post’s page. It’s quite intuitive what these fields do (check by seeing how something in front matter renders on the page), so I won’t go into detail.

For a list of variables you can use, see Variables | Front Matter from PaperMod documentation.

layouts/ and assets/

The files in these directories override the files in themes/PaperMod/layouts/ and themes/PaperMod/assets/, respectively. If the path of a file in layouts/ exactly matches that of a file in themes/PaperMod/layouts/, then your site will use the file in layouts/ instead of the one in themes/PaperMod/layouts/. Same for assets/.

Essentially, themes/PaperMod/layouts/ and themes/PaperMod/assets/ specify defaults. If you want to make a change, override the default in your own repo.

layouts/ contains HTML files that specify the structure of pages.

Let’s look at PaperMod/layouts/_default/.

themes/PaperMod/layouts/_default
├── _markup
├── archives.html
├── baseof.html
├── index.json
├── list.html
├── rss.xml
├── search.html
├── single.html
└── terms.html

baseof

In particular, here’s baseof.html.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="{{ site.Language }}" dir="{{ .Language.LanguageDirection | default "auto" }}">

<head>
    {{- partial "head.html" . }}
</head>

<body class="
{{- if (or (ne .Kind `page` ) (eq .Layout `archives`) (eq .Layout `search`)) -}}
{{- print "list" -}}
{{- end -}}
{{- if eq site.Params.defaultTheme `dark` -}}
{{- print " dark" }}
{{- end -}}
" id="top">
    {{- partialCached "header.html" . .Page -}}
    <main class="main">
        {{- block "main" . }}{{ end }}
    </main>
    {{ partialCached "footer.html" . .Layout .Kind (.Param "hideFooter") (.Param "ShowCodeCopyButtons") -}}
</body>

This is the base template for all pages. Notice it has all parts of an HTML document: <!DOCTYPE html>, <html>, <head>, and <body>.

It’s mostly HTML. However, note the code in braces {{ ... }} or {{- ... -}}. This is Go template code. It’s a templating language that Hugo uses to generate HTML.

Note on line 2 that a site variable site.Language is directly inserted into an HTML attribute. Some site variables are in config.yml, and others are built-in to Hugo.

Note on line 5 that a partial head.html is inserted. A partial is an HTML snippet that can be inserted into a page. As we can see here, the partial head.html (which can be found under layouts/partials/head.html) is the <head> code of all pages.

Does this mean all pages have the same <head> code?

Nope, notice one last thing on line 9: conditionals! By checking the values of some variables, we can conditionally insert HTML code. Lines 9-14 just insert some classes, but it’s also possible to insert entire HTML snippets or partials. So although all pages have the same template for <head>, the actual <head> code depends on parameters.

On lines 16 and 20, we see partials defining the header and footer of a page. In the middle is <main> for the content of a page. All files in _default except baseof.html define <main>.

single

This page (and most pages) is a single (layouts/_default/single.html).

A portion of single.html is below.

{{- define "main" }}

<article class="post-single">
  <header class="post-header">
    {{ partial "breadcrumbs.html" . }}
    <h1 class="post-title">
      {{ .Title }}
      {{- if .Draft }}<sup><span class="entry-isdraft">&nbsp;&nbsp;[draft]</span></sup>{{- end }}
    </h1>
    {{- if .Description }}
    <div class="post-description">
      {{ .Description }}
    </div>
    {{- end }}
    {{- if not (.Param "hideMeta") }}
    <div class="post-meta">
      {{- partial "post_meta.html" . -}}
      {{- partial "translation_list.html" . -}}
      {{- partial "edit_post.html" . -}}
      {{- partial "post_canonical.html" . -}}
<!-- Rest of code omitted -->

Notice this code goes in the "main" block from line 17 of baseof.html. You could inspect element this page to confirm. The code is quite intuitive, so you should be able to see how this code (in addition to front matter and site variables) causes certain elements to appear at the top of this page.

Specifying layout of a page

You should rarely have to manually specify the layout of a page in front matter. Hugo determines whether a page is a single or list by directory structure. Most pages should be singles, of course.

You can see an example of a list layout at my projects page. However, I did not specify this layout manually: It’s automatically a list layout because projects/ has directories but no index.md file.

content/projects
├── 566
├── leds
├── mips_emulator
├── neuroruler
├── rubiks_541
└── sapsim

I have two pages where I manually set the layout in front matter. One is Search, and the other is Archives.

Here’s the front matter of search.md, and it’s similar for archives.md.

---
title: "Search"
layout: "search"
summary: "search"
---

This topic is further described in Content.

static/

The fourth and final important part is static/. This is where static files, such as images, go.

I don’t have very many images here because I prefer to group images with the post itself in content/.

Note that after compilation, files in static/ are copied to the root directory /. So, when accessing a file in static/, you should prepend a / to the file path. For example, the image static/1.jpg should be accessed as /1.jpg. In practice, I’ve found that the leading / can often be omitted. Just know that something like /static/1.jpg won’t work.

Site logos are configured in two places in the config.

params:
  # Image displayed when posting site link on socials
  # For example, if you post the link to the site in Discord, this image will be displayed
  images: ["logo_outlined_6.png"]

  # ...

  # Logo and name shown on top left of site
  label:
    text: "Jesse Wei"
    icon: /logo_filled_outlined_6.png
    iconHeight: 35

It’s pretty self-explanatory. I do want to show an example of where the images: logo is displayed.

params.images displayed when posting website link in Discord

params.images displayed when posting website link in Discord

Favicons

See the PaperMod documentation. favicon.io is very convenient for generating favicons!

Shortcodes

I want to mention shortcodes. Quite a few of them are built in to Hugo and PaperMod, and they’re very convenient.

I’ll show a few examples. Note that when I show the code, I put a space between { and < so that the shortcode is parsed as text and not executed. To use it, remove that space.

Raw HTML

{{ < rawhtml >}}
<p align="center" style="color: red;"><strong>This is raw HTML</strong></p>
{{ < /rawhtml >}}

This is raw HTML

Figure

See above for an example of a figure. Here’s the code that generates it:

{{ < figure src="img/social_logo.jpg" caption="params.images displayed when posting website link in Discord" alt="params.images displayed when posting website link in Discord" align="center">}}

Hugo documentation

YouTube embed

{{ < youtube hjD9jTi_DQ4 >}}

Hugo documentation

GitHub gist

{{ < gist jesse-wei 0b2472f020b41b8767882291c536102c >}}

Hugo documentation

Deploy

Resource 2 describes how to deploy to Netlify. Here’s a timestamp for that portion of the video.

The build process is incredibly simple. In the video, the only command you input for the build process is hugo.

My build process involves slightly more than just hugo since I also have to build PaperMod_diff 1. So, I use scripts/netlify. My build command in Site settings > Build & deploy > Build command is chmod +x scripts/netlify;./scripts/netlify.

You can probably ignore that unless you also want to set up a PaperMod_diff page.

My website

Now I’ll describe specific features of my website.

config.yml

The latest version of my config.yml is here.

I think I use reasonable values, and I use comments to explain decisions I consider non-obvious. I’ll explain some specific decisions I made in this file in the below sections as they come up.

PaperMod diff

I created PaperMod diff 1 using the scripts in scripts/. diff.py runs diff between corresponding files, helpers/generate_directory_index_caddystyle.py creates index.html files recursively, and build wraps diff.py to deploy its output to Netlify (see Deploy). helpers/cd.py is also used.

Do note that content/posts/papermod_diff is gitignored. This is because if it weren’t, then the content there would change and be shown on GitHub every time I modify assets/ and/or layouts/, which is redundant and would make the commit history harder to read. My solution (gitignore the folder and generate it during the build process in Netlify) is a bit roundabout, but it’s already implemented and works well.

Content

This section is a continuation of Specifying layout of a page.

Let’s look more closely at the structure of content/teaching, which is a list layout.

content/teaching
├── act               List layout within list layout
│   ├── _index.md     Note, _index.md, not index.md!
│   ├── binary
│   ├── desmos
│   ├── eulers_formula
│   ├── ...
├── comp110
│   ├── img
│   └── index.md
├── comp210
│   ├── img
│   └── index.md
└── comp311
    ├── img
    ├── index.md
    └── review

My Teaching page has a list layout because teaching/ doesn’t have an index.md. The comp110/ directory is a single because it has an index.md. It’s accessible by /teaching/comp110. It also contains an img/ directory that’s accessible from index.md. The images could go in /static/, but I prefer bundling them with the page.

It’s possible to have a list layout within a list layout, and /teaching/act is an example. However, notice act/ must have an _index.md file (note the underscore) since it’s a non-leaf.

See Page Bundles for more details.

$\LaTeX{}$

I enabled $\LaTeX{}$ via KaTeX in layouts/partials/extend_head.html.

I followed Math Typesetting from PaperMod documentation. Specifically, the code in extend_head.html is mostly from Issue #236.

I modified the condition for loading the KaTeX script. The site param math must be true. Then KaTeX will be loaded by default in all pages. Setting the local param math to false in front matter will cause that page to not load KaTeX.

I think having to opt-in is super annoying, so I’d rather enable it globally and be able to opt-out.

Comments

I enabled comments using giscus.

I followed the directions on the giscus site to install giscus in my repo and pasted code from the giscus website into layouts/partials/comments.html. I also added comments: true to config.

As you can see, there are comments at the bottom of almost every page. PaperMod automatically disables comments in the index profile, search, and archives layouts. I manually disabled comments in my Privacy policy page in the front matter with comments: false.

Comments show up in GitHub Discussions. Make sure to enable Discussions in your GitHub repo.

I added social icons to the footer, as in resource 5. I sort of follow what it describes but make some of my own adjustments.

As described there, adding social icons to footer messes with CSS spacing values. For example, a scrollbar appeared on the homepage and Search page (haven’t solved this and don’t plan to) even though there’s enough room for both header and footer to be visible without scrolling. This issue is described more in-depth in resource 5, under problem 2.

In short, I modified CSS in 4 files. layouts/partials/footer.html, layouts/partials/social_icons.html, assets/css/core/theme-vars.css, and assets/css/common/profile-mode.css. The comments in each file describe the changes I made. Most comments in social_icons.html are for htmltest, described below, so ignore those for now.

I disabled footer social icons on the homepage because the homepage already has social icons.

Beyond that, I made some other minor changes to the footer.

I added the separator character • between phrases in the footer.

The links were originally like this:

Powered by
<a href="https://gohugo.io/" rel="noopener noreferrer" target="_blank">Hugo</a> &
<a href="https://github.com/adityatelange/hugo-PaperMod/" rel="noopener" target="_blank">PaperMod</a>

I removed target=_blank and rel="noopener noreferrer" because links should not usually open new tabs.

I added a privacy policy page and a link to it in the footer.

Lastly, I removed “Powered by Hugo and PaperMod” in the homepage specifically to keep it minimal.

Single

I slightly modified layouts/_default/single.html. I moved the ToC above the cover. Notice on this page that the ToC is above the cover image.

I modified CSS in the following CSS files in assets/css/common/: archive, footer, header, main, post-entry, and post-single.

For example, main.css has these important lines:

/* Change color on hover */
a:hover {
    color: var(--carolina_blue);
}

a.anchor:hover {
    color: var(--carolina_blue) !important;
}

svg:hover {
    color: var(--carolina_blue);
}

For modifications, look for comments and the variable carolina_blue.

Since I made links blue on hover, I removed underline on hover (the link is still underlined if it had an underline before hovering though).

Syntax highlighting via Chroma

I disabled highlight.js (default) and enabled Hugo Chroma following the steps in PaperMod documentation. This required a few changes in config.yml and assets/css/extended/*.css.

I disabled line numbers by default for readability. Most code blocks you’ve seen so far have not had line numbers.

However, you can enable line numbers for a specific code block, as shown in the baseof code block, by adding {lineNos=true} to the code block. 8

1
2
<!DOCTYPE html>
<!-- Omitted -->

Google Analytics

For Google Analytics, just add your Google Analytics tag to googleAnalytics in the config. For example, mine has googleAnalytics: G-Q603T56FWT.

PaperMod automatically uses the Google Analytics script if env is production (default). See the bottom of layouts/partials/head.html:

{{- /* Misc */}}
{{- if hugo.IsProduction | or (eq site.Params.env "production") }}
{{- template "_internal/google_analytics.html" . }}
{{- template "partials/templates/opengraph.html" . }}
{{- template "partials/templates/twitter_cards.html" . }}
{{- template "partials/templates/schema_json.html" . }}
{{- end -}}

CI

I added a GH workflow for checking links in my site and spellcheck.

htmltest GH action output

htmltest GH action output

See .github/workflows/ci.yml and its configuration file .github/.htmltest.yml. This follows resource 6, with some modifications. In particular, I want to note that I run my scripts/build in ci.yml instead of just hugo, as in the original file.

Behavior

Here is the intended behavior of the htmltest job after making the modifications below.

If an internal link (e.g., a page or image) doesn’t work, the workflow will fail, causing a red X to appear on GH Actions.

If an external link doesn’t work, htmltest will warn, but the workflow will not fail. That is, an external link could be “broken,” and a green checkmark will be shown on GH Actions. This is fine because when using a lot of external links, it’s unlikely that all will work in any single run. There could be a timeout or non-200 HTML response code, etc.. Getting a red X when just a single external link breaks is annoying. htmltest does still warn, so I manually check GH actions every now and then.

Getting rid of garbage output

There was originally >100 lines of garbage output. htmltest complained that the site logo’s link at the top left had no alt text, and my LinkedIn link in social icons in the footer returned non-OK exit status 999. Since the header and footer are in all pages, this caused a lot of errors, which made the output unreadable.

I fixed this in layouts/partials/header.html by adding non-empty alt text to the logo and in layouts/partials/social_icons.html by excluding the LinkedIn link from htmltest using the data-proofer-ignore attribute, as specified in htmltest’s README.

The top 3 links are ones I want to keep even though they don’t actually work. So I manually ignored them in the post (not layouts/) using the data-proofer-ignore attribute in rawhtml. 9

And the #center thing is how you can center an image in Markdown syntax in PaperMod. #center also gets appended to an image URL if you use align="center" in figure shortcode. But since this causes htmltest to freak out, I added this to .htmltest.yml:

IgnoreURLs:
  # Ignore <img src="*#center"> for centered images in PaperMod, which would cause "hash not found"
  # This is suboptimal because we ideally want to check the image URL without the #center suffix

  # Match internal image path ending in #center
  - .*\.(apng|gif|ico|cur|jpg|jpeg|jfif|pjpeg|pjp|png|svg)#center$
  # Match external image URL ending in #center
  - (https://|http://|www\.).*\.[A-Za-z]+#center$

As you can tell by the comments, this is suboptimal and can lead to false negatives. I can think of two solutions.

  1. Modify htmltest to ignore specific hashes but check the rest of the URL 10
  2. Modify PaperMod to center the image without appending #center

Make GH Actions display red X on failure

Instead of continue-on-error: true from resource 6, I use if: always().

- name: Test HTML
  # https://github.com/wjdp/htmltest-action/
  uses: wjdp/htmltest-action@master
  with:
    config: ./.github/.htmltest.yml
- name: Archive htmltest results
  # Archive result even if Test HTML fails
  # Use if: always() instead of continue-on-error, as in the original file
  # Source: https://stackoverflow.com/questions/62045967/github-actions-is-there-a-way-to-continue-on-error-while-still-getting-correct
  if: always()

if: always() will cause logging to occur even if Test HTML fails. continue-on-error: true does the same. However, continue-on-error: true would cause GH Actions to display a green checkmark when Test HTML fails, which is misleading.

I added this to .htmltest.yml:

# This does not "ignore" the broken links, but it does not fail the action
# From the htmltest README:
# When true produces a warning, rather than an error, for broken external links.
IgnoreExternalBrokenLinks: true

With a lot of external links, it’s unlikely that all external links will work during any one run. Maybe there’ll be a timeout or bad HTML response code, etc.


  1. PaperMod_diff ↩︎ ↩︎ ↩︎ ↩︎

  2. Hugo Quick Start ↩︎ ↩︎

  3. Getting Started With Hugo ↩︎ ↩︎

  4. PaperMod demo site/documentation and its source ↩︎

  5. Konstantin’s How to Set Up This Blog ↩︎ ↩︎ ↩︎

  6. Check links in Hugo with htmltest ↩︎ ↩︎ ↩︎

  7. I assume you already do, surely. ↩︎

  8. You might also be able to enable it by default in a specific post by adding it to front matter, but this didn’t work for me. ↩︎

  9. TODO: Create shortcode for a link with data-proofer-ignore attribute. ↩︎

  10. This would ignore any URL with #center suffix, not just image URLs. ↩︎