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.
- 2 Hugo Quick Start
- Super quick to read and follow along, < 5 min.
- 3 Getting Started With Hugo (47:41)
- 4 PaperMod demo site/documentation and its source
- 5 Konstantin’s How to Set Up This Blog
- 6 Check links in Hugo with htmltest
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 diff
s 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
.
|
|
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"> [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.
Logo
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.
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">}}
YouTube embed
{{ < youtube hjD9jTi_DQ4 >}}
GitHub gist
{{ < gist jesse-wei 0b2472f020b41b8767882291c536102c >}}
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.
Social icons in footer
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.
Other footer changes
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.
Links are Carolina blue on hover
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
|
|
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.
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.
- Modify htmltest to ignore specific hashes but check the rest of the URL 10
- 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.
IgnoreExternalBrokenLinks
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.
PaperMod demo site/documentation and its source ↩︎
I assume you already do, surely. ↩︎
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. ↩︎
TODO: Create shortcode for a link with
data-proofer-ignore
attribute. ↩︎This would ignore any URL with
#center
suffix, not just image URLs. ↩︎