After nearly 3 years using the PaperMod theme for my blog built on Hugo, I’ve decided to switch to another theme that can serve my current need better. This blog post will discuss the steps I took.
Why switch?
Blogging on technical topics, especially those involving multiple components, usually results in longer-form content. The native support for TOC in PaperMod is disappointing as it is only displayed on top of the page. Readers will easily lose the content structure when they burrow deeper. Having a TOC by the side that floats along your reading will serve this purpose much better.
I also find the tags
and category
in PaperMod somewhat overlapping. What I want are high level clusters that group related posts together and offer a single, clean place to organize my posts.
My requirements
Context-aware TOC
As my primary goal for this switch, I am looking for themes with support for floating, context-aware TOC in blog posts. This is always available for documentation themes such as the followings:
- Hugo-book: simple, dark mode, no title page, minimal blog support.
- Eureka: elegant, dark mode, nice title page with ability to add about page, no blog support
- Doks: modern landing page with separate docs and blog section. Unfortunately the blog section has no TOC.
- Lotus Docs: highly functional doc template with no blog.
My usage scenario requires a mix of content from blogs, project content and a personal profile. And my blog content spans different domains of various length, that can be categorized in multiple ways. Thus the structural layout of the above doc themes (which are built more like a book with fixed chapters) do not suit me. Hence I’m moving back to searching for blog-centric theme like the followings that must have a floating side TOC. The followings are my findings along with my thoughts on each of them.
- Stack: the 3 columns setting seem a bit cramped. Posts are arranged like cards. TOC doesn’t change color with context too. However, different levels of paragraphs are visually distinct. Also useful for building image galleries. This is a very strong candidate if not for the 3-column layout.
- Poison: just like Stack, I’m just not a big fan of 3-column layouts.
- Cleanwhite: clean and simple, but lacking a separate home page. I would imagine this be a great feed for a pure blog author.
- Gokarna: has a simple but adequate home page with posts and pages in dark mode. Floating TOC is context aware with nice to parse colors.
- CodeIT has everything I want, but it is not maintained anymore.
- Fixes to Papermod to include floating side TOC
- this one, another and yet another: didn’t work out for me
- PaperModX an updated theme with TOC support
- customize existing CSS and HTML files for any Hugo site.
- al-folio: Jekyll theme which requires more setup time due to platform change.
- Chirpy: Jekyll theme. 3 column layout. I really love the floating TOC’s implementation, which expand and collapse sections per context. This reduces clutter on the screen and make the reading experience cleaner and lighter.
Aesthetics
- minimal and bright: even though I really love the simple approach of Codex and Winston themes, the lack of side TOC kills them for me.
- dark mode to reduce eye strain.
Misc.
- title card: a homepage with my basic information, acting as an orientation page for my blog, resume, social links, etc.
- flexible support for analytics software of my choice.
Steps I took
My final pick is Gokarna due to its simple homepage, and native support for floating TOCs. I also like that the main column has more efficient utilization of screen real estate, as compared with the single column layout of PaperMod.
The following sections will outline the steps I take to migrate my existing PaperMod site to Gokarna.
Creating or moving to a new site always involves examining the new theme’s structure via its exampleSite
content, the config.toml file, and any instruction provided in its github repo readme file.
Two specific places are
- site config file:
hugo.toml
orconfig.yml
. This controls the look and information architecture of the site. - content in the
content
folder. This contains the actual blog posts, about page for personal profile, project pages, and misc. supporting content that can populate the homepage.
Configure global site settings
The first step is to create a clear mapping of how our old configuration file’s keys/values map to the new theme.
Create a new hugo site called NewBlog.
Clone the Gokarna theme as a submodule per instruction.
Copy the
hugo.toml
inthemes\gokarna\exampleSites
to our NewBlog root folder.In our code editor, open Gokarna’s
hugo.toml
side by side with our existingconfig.yml
from PaperMod.Follow the instruction to configure our new site in
hugo.toml
. For me it involves the following steps:change
baseURL
to our custom domainchange
title
.in
[params]
section- change the avatar to our own image.
- optionally, change
accentColor
to something that matches our avatar color palette by using a color picker tool. This will only change hover color for hyperlinks in a page. It will not affect top level menu items or TOC. To make a global change, make sure to copy thelayouts\partials\head.html
file from the theme folder to our root folder, and change the default HEX value to match.
:root { --accent-color: {{ .Site.Params.AccentColor | default "#FF4D4D" }};
- setup
socialIcons
for our social links on the home page. - Unlike PaperMod, there is no native Google Analytics support. Instead, we can take advantage of custom head element to insert whatever analytics script we use. This is more flexible than PaperMod. For example, we can setup Google Analytics this way in
[params]
customHeadHTML = """ <script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXX"></script> """
- Instead of just an avatar and social links, we can also choose to add some blog posts to the home page.
reconfigure the
[menu]
section to setup the upper right menu items. A nice touch is the use of feather icons in the menu items.Enable Table of Content with this new section:
[markup] [markup.tableOfContents] startLevel = 1 endLevel = 4 ordered = false
- Enable automatic generation of sitemap with this new section:
[sitemap] changeFreq = "weekly" filename = "sitemap.xml" priority = -1 # Default priority (-1 means omitted from rendered sitemap)
Then, go to the
layouts/_default/
folder and add a new file calledsitemap.xml
. This will be a template file to generate a sitemap for our site. Paste the content from the official Hugo github repo. We can also refer to this and this post to customize what content to exclude.
Convert content files
Now we are ready for action! We will convert all our existing content over.
- Start by moving all our existing blog posts from the
content\posts
folder over to our new site. - Check if any supporting content in
exampleSite\content\
are applicable. These might includeprojects
,data
and other profile pages that we can build new content from. - The major conversion happens in the frontmatter of each existing blog post (i.e., all the
index.md
files). As PaperMod and Gokarna uses different fields in the yaml frontmatter, we need to convert them to ensure the right fields are properly setup and old/irrelevant fields are removed. We also need to setup new fields Gokarna needs.
I whip up the following Python script to automatically parse all the index.md
files in each posts
subfolders. Make sure to change root_dir
to point to the new site’s posts folder.
import os
import re
import yaml
# Set the root directory to begin the walk
root_dir = r"\path\to\newBlog\content\posts"
# A regex to capture the YAML front matter (delimited by "---")
# and separate out the markdown body.
yaml_regex = re.compile(r"^---\s*\n(.*?)\n---\s*\n(.*)$", re.DOTALL)
for dirpath, dirnames, filenames in os.walk(root_dir):
if "index.md" in filenames:
filepath = os.path.join(dirpath, "index.md")
print(f"Processing {filepath}...")
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
match = yaml_regex.match(content)
if not match:
print(f" Skipping {filepath}: No valid YAML front matter detected.")
continue
yaml_content, body = match.groups()
# Parse the YAML front matter into a dictionary.
try:
data = yaml.safe_load(yaml_content)
except Exception as e:
print(f" Error parsing YAML in {filepath}: {e}")
continue
# Prepare new YAML front matter lines.
new_lines = []
new_lines.append("---")
# Title
if "title" in data:
# Ensure the title is wrapped in double quotes.
new_lines.append(f'title: "{data["title"]}"')
# Date
if "date" in data:
new_lines.append(f'date: "{data["date"]}"')
# Set #lastmod equal to the date – note the comment prefix.
new_lines.append(f'#lastmod: "{data["date"]}"')
# Draft
if "draft" in data:
new_lines.append(f'draft: {str(data["draft"]).lower()}')
# Description
if "description" in data:
new_lines.append(f'description: "{data["description"]}"')
# Image: get from cover.image if available.
cover_image = ""
if "cover" in data and isinstance(data["cover"], dict):
cover_image = data["cover"].get("image", "")
new_lines.append(f'image: "{cover_image}"')
# Set type to post.
new_lines.append('type: "post"')
# Convert showToc to showTableOfContents.
# If showToc exists and is truthy, set to true; otherwise false.
show_toc_val = "true" if data.get("showToc", False) else "false"
new_lines.append(f'showTableOfContents: {show_toc_val}')
# Preserve the commented "# weight:" line if present in original YAML.
weight_line = None
for line in yaml_content.splitlines():
if line.strip().startswith("# weight:"):
weight_line = line.strip() # keep the formatting as-is
break
if weight_line:
new_lines.append(weight_line)
# For tags, use values from the categories field.
tags_list = data.get("categories", [])
# Make sure each tag is converted to a string and wrapped in double quotes.
formatted_tags = ", ".join(f'"{str(tag)}"' for tag in tags_list)
new_lines.append(f'tags: [{formatted_tags}]')
new_lines.append("---")
new_yaml = "\n".join(new_lines)
# Write the new YAML front matter and append the remaining markdown body.
new_content = new_yaml + "\n" + body
with open(filepath, "w", encoding="utf-8") as f:
f.write(new_content)
print("Processing complete.")
Finally, run hugo server
in our site’s root folder to do a sanity check of everything before pushing it live.
Adding comment
For this “release”, I also added comment support via Giscus. Hugo comes with native support for Disqus but the free version is loaded with ads. I picked Giscus because of the followings:
- no need to self-hosted, and no additional paid cloud-hosted service is needed. All I need is a new public Github repo.
- simple and easy to configure UI.
- can even allow reactions!
- free and no ad!
To add Giscus to a Hugo blog, just do the followings:
- Create a new public Github repository
- Install Giscus to your repository
- Go to the new repository’s Settings page and make sure that under Features,
Discussion
is checked. You can safely uncheck everything else. - Click Discussion in the top menu bar.
- In the left pane, click the edit icon for Categories.
- Create a New category to host new comments for your blog.
- Choose Announcement for Discussion Format so only you can start new discussions but anyone can comment.
- You can safely remove the other discussion categories.
- Go to the Configuration section of Giscus. Provide your new repository and accept all the other default settings.
- Now Giscus will generate an HTML script block for you to enable it on your site.
- Go to your root folder, then
layouts\partials
. Create a new file calledgiscus.html
and paste the script you get at step 6. - Edit
layout\_default\single.html
to include the following code before the last{{- end }}
. If you don’t have this file in the folder, copy it fromthemes\gokarna\layouts\_default
.{{ if (.Site.Params.GiscusComments) -}} <div class="comments"> <h4>Comments</h4> {{ partial "giscus.html" . }} </div> {{- end }}
- In
config.toml
, add the following parameter in the[param]
section:GiscusComments = true
That’s it! Comments have now been enabled in blog posts only. Now your site’s visitors will be able to comment and pick a reaction after they login with their Github credentials.
Bonus: If you want to be notified of any new comment, you can go back to the Github repo you created, click the Unwatch menu item and check Discussions to subscribe to this event.
Note: While Gokarna claimed support for any commenting platform, do not following its documentation to add the Giscus script to
customFooterHTML
param inconfig.toml
. It might sound easy enough, but doing so will add commenting to EVERY page of your website, including homepage, tags, about, etc.
Conclusion
In this post, I described the theme hunting process (and candidates) for a new Hugo theme that supports context-aware TOC while providing a clean homepage for personal profile. This turned out to be the most time-consuming tasks as I needed to go to multiple theme portal sites and visually inspect the demo sites of each potential theme. I also went over the process of migrating an existing site configuration to the new theme, and moving all the blog posts over programmatically with a Python script to convert each file’s yaml frontmatter. Doing it in Python saves a lot of time especially if you have a sizeable chunk of content. Finally, I added commenting functionality from Giscus to the posts.
I would like to thank adityatelange for the PaperMod theme. It’s rock solid with a lot of features that I will miss (e.g., an Archives page breaking down posts into year/month, 3 types of homepages, Search, cover image for each post, share buttons, code copy button)!
That said, needs change and it’s always good to have more suitable options for our current situation. I’m grateful for the Hugo ecosystem with its good community support and a diverse choice of themes for different needs.
Comments