Background
A few days ago, I wanted to implement a feature that adds a tag in the blog’s front-matter to distinguish lifestyle articles from technical articles, with the homepage displaying different link colors.
Problem
While searching for solutions on the Hexo website and Google, I found several issues:
- Hexo’s default template paularmstrong/swig is no longer maintained1
- The documentation on the Hexo website was still quite sparse
As I mentioned in hexo + gitlab serving hidden static files:
Having previous experience with hexo, plus the exceptionally thriving node ecosystem for personal blogs (lots of third-party node-related plugins available), I decided to migrate over.
This is actually one of the causes of problem one. Problem one has been discussed in hexo’s GitHub issues: Why not totally replace Swig with Nunjucks? #1593, where mozilla/nunjucks2 as an upgrade to the Swig template, can serve as a replacement after Swig is no longer maintained. But the hexo project wasn’t so quick to replace the default template.3
Problem 2 has been an issue for a long time, being quite unfriendly to theme developers. Here you can see one theme developer’s feelings https://blessing.studio/get-hexo-posts-by-category-or-tag/:
Today while porting my blog theme to Hexo, I wanted to get all posts under a certain category or tag (more accurately, I wanted to get the total number of posts). When searching with Chinese keywords, I didn’t get any useful information (perhaps my search technique was wrong). After switching to the English keyword “hexo category all posts”, I found the information I needed, so I decided to write a post to document this, hoping to help others later.~~~~
I have to complain here - Hexo’s documentation is really terrible, just terrible. Writing a theme, sometimes wanting to implement a feature requires frantically reading Hexo source code, I’m speechless.
Solution
My solution options were:
- Option 1: Keep the
swigtemplate unchanged, search for similar implementation methods. - Option 2: Use a
nunjucksplugin to enable hexo to support that template, then implement the feature. - Option 3: Switch to other template engines supported by Hexo, such as hexojs/hexo-renderer-ejs, https://github.com/hexojs/hexo-renderer-haml, hexojs/hexo-renderer-jade, re-implement the theme, and implement the feature along the way
- Option 4: Switch to another static blog generation platform
My solution process happened to follow the order I listed
Option 1
Since hexo’s custom front-matter parts require hexo import scripts, and I didn’t want to spend too much time on the hexo framework itself. So I gave up.
During the process, I used another method: using a rarely-used front-matter field to mark link colors (I used the layout variable). The actual effect achieved a similar result, but the code looks confusing.4
Option 2
nunjucks only has a few hexo-related plugins, hexo-renderer-nunjucks and hexo-nunjucks, and opening them shows these two projects’ last update time was locked to three years ago. So I gave up.
Option 3
These template engines are directly supported by Hexo official, but when actually using them, I still felt the outdated swig was slightly better (mainly because it matched my impression of templates). So I gave up.
Option 4
First, let me list the alternatives:5
- JavaScript: Next.js & Gatsby (for React), Nuxt & VuePress (for Vue), Hexo, Eleventy, GitBook, Metalsmith, Harp, Spike.
- Python: Pelican, MkDocs, Cactus.
- Ruby: Jekyll, Middleman, Nanoc, Octopress.
- Go: Hugo, InkPaper.
- .NET: Wyam, pretzel.
I finally chose Hugo from these, mainly because I was learning Go recently. And Hugo’s “text/template” is also a standard extension module for Golang, so it shouldn’t have the jumping between multiple templates like Hexo^(problem 1)^. Hugo’s official documentation is also visibly more extensive^(problem 2)^.
Implementation
Having determined the solution, let me organize what needs to be done after choosing this solution:
- Article migration
- Template migration6
- Feature implementation
- Deployment plan
1. Article Migration
Since both use articles written in markdown, migration is basically just a cp command. I won’t elaborate here.
2. Template Migration
For the template part, it was basically pixel-level COPY: implementing swig template functionality line by line using text/template. Of course, the syntax style follows Hugo’s official documentation.
I still encountered a few small challenges during the process, and I’ll post the solutions here:
- When implementing the
/tags/page, I needed to first group articles by tag, then iterate through articles in each tag group. Intext/template, variable scoping is very strange, code as follows:
{{ $v := "init" }}
{{ if true }}
{{ $v := "changed" }}
{{ end }}
v: {{ $v }} {{/* => init */}}
Intuitively judging this code, $v should output "changed". However, the actual result is surprising. My understanding of this situation: in text/template’s implementation, each code block has an independent scope, and this scope doesn’t inherit when nesting occurs.
This is probably the cleanest and simplest code implementation. In such cases, the official recommendation is to use .Scratch to create a page-level scope readable-writable variable, but this is a bit heavy for theme templates.
With Google’s help, I found this gist: https://gist.github.com/Xeoncross/203d8b1459463a153a3c734c98b342a9
<ul class="tags">
{{ range $name, $taxonomy := .Site.Taxonomies.tags }}
<li><a style="text-transform: capitalize" href="#{{ $name | urlize}}">{{ $name }}</a>
<!-- <span>({{ len $taxonomy }})</span> -->
</li>
{{ end }}
</ul>
- In articles with a table of contents, I found the automatically rendered TOC would have empty
·appearing.
In some articles, my subheadings are marked with <h3>, but the auto-generated TOC template didn’t automatically remove unused <h1> and <h2>
· · · Heading level 3 1
· Heading level 3 2
This is also reflected in Hugo’s GitHub Issues: Heading levels in Markdown table of contents #1778, and this involves another issue - the markdown rendering template russross/blackfriday used by Hugo is still v1 version in Hugo, and v1 version outputs an HTML fragment after parsing markdown. Because of this, there’s this ugly inefficient code when generating .TableOfContents: https://github.com/gohugoio/hugo/blob/master/helpers/content.go#L416
func ExtractTOC(content []byte) (newcontent []byte, toc []byte) {
if !bytes.Contains(content, []byte("<nav>")) {
return content, nil
}
origContent := make([]byte, len(content))
copy(origContent, content)
first := []byte(`<nav>
<ul>`)
last := []byte(`</ul>
</nav>`)
replacement := []byte(`<nav id="TableOfContents">
<ul>`)
startOfTOC := bytes.Index(content, first)
peekEnd := len(content)
if peekEnd > 70+startOfTOC {
peekEnd = 70 + startOfTOC
}
if startOfTOC < 0 {
return stripEmptyNav(content), toc
}
// Need to peek ahead to see if this nav element is actually the right one.
correctNav := bytes.Index(content[startOfTOC:peekEnd], []byte(`<li><a href="#`))
if correctNav < 0 { // no match found
return content, toc
}
lengthOfTOC := bytes.Index(content[startOfTOC:], last) + len(last)
endOfTOC := startOfTOC + lengthOfTOC
newcontent = append(content[:startOfTOC], content[endOfTOC:]...)
toc = append(replacement, origContent[startOfTOC+len(first):endOfTOC]...)
return
}
Here, string processing methods are used to parse content in .Content, then piece together the content to be generated and add it to the original content. This problem is solved in blackfriday.v2, which outputs an AST for other programs to process, which also ensures compatibility with subsequent versions. But in Hugo, the author has repeatedly postponed this feature’s milestone Upgrade to Blackfriday v2 #39497.
But in the Issue discussion, various experts proposed their own solutions - you can click into Heading levels in Markdown table of contents #1778 to see details. I adopted the template solution from there:
{{ $toc := .TableOfContents }}
{{ $toc := (replace $toc "<ul>\n<li>\n<ul>" "<ul>") }}
{{ $toc := (replace $toc "<ul>\n<li>\n<ul>" "<ul>") }}
{{ $toc := (replace $toc "<ul>\n<li>\n<ul>" "<ul>") }}
{{ $toc := (replace $toc "</ul></li>\n</ul>" "</ul>") }}
{{ $toc := (replace $toc "</ul></li>\n</ul>" "</ul>") }}
{{ $toc := (replace $toc "</ul></li>\n</ul>" "</ul>") }}
<!-- count the number of remaining li tags -->
<!-- and only display ToC if more than 1, otherwise why bother -->
{{ if gt (len (split $toc "<li>")) 2 }}
{{ safeHTML $toc }}
{{ end }}
{{ end }}
This solution can be easily removed from the template after future feature version merges. It’s one of the simpler approaches.
3. Feature Implementation
After template migration was complete, I started implementing the feature I originally wanted. In Hexo, front-matter can include user-defined fields (documentation at: https://gohugo.io/variables/page/#page-level-params).
I chose to use the linkcolor field here.
---
title: test
linkcolor: #7076c7
---
Then add the variable judgment where the homepage iterates through titles:
{{ if .Params.linkcolor }}
{{ $color := .Params.linkcolor }}
<a href="{{ .Permalink }}" class="post-list-item" style="color:{{ $color }};">
{{ else }}
<a href="{{ .Permalink }}" class="post-list-item">
{{ end }}
I felt that writing an abstract color #7076c7 each time didn’t look good, so I added the following content to the data/color.toml directory (documentation at: https://gohugo.io/templates/data-templates/)
[link]
blue = "#7076c7"
Modified the homepage template
{{ if .Params.linkcolor }}
{{ $color := index .Site.Data.color.link .Params.linkcolor }}
<a href="{{ .Permalink }}" class="post-list-item" style="color:{{ $color }};">
{{ else }}
<a href="{{ .Permalink }}" class="post-list-item">
{{ end }}
This way, in front-matter you only need to write linkcolor: blue to achieve the same effect. Future color-related extension features can also be conveniently implemented.
4. Deployment Plan
The official documentation covers this in detail. I use the Netlify platform to publish my blog, documentation at https://gohugo.io/hosting-and-deployment/hosting-on-netlify/
Other commonly used platforms are also covered in the documentation.
The main focus here is migrating commit history from the old blog. I put the Hexo blog files and directories into a separate directory hexo_archive, and put Hugo platform code in the project root directory. This way, previous commit history and files are preserved, while leaving a relatively clean directory for Hugo.
See Also
- https://gist.github.com/Xeoncross/203d8b1459463a153a3c734c98b342a9: Various situations you’ll encounter with Hugo templates
- https://epatr.com/blog/2017/hugo-and-netlify/: Using Hugo with Netlify
GitHub page https://github.com/paularmstrong/swig already shows: “This repository has been archived by the owner. It is now read-only.” ↩︎
Seeing the mozilla prefix, I feel the Nunjucks project should be stably maintained for a while ↩︎
As of 2019-01-18, there is a related PR in the hexo project Replace default swig engine with nunjucks #2903. ↩︎
Code details at: https://github.com/wukra/izhengfan.github.io/commit/2e3d6782b1bf5c43d149fabe961b7fb09c84a2c5 ↩︎
From https://snipcart.com/blog/choose-best-static-site-generator ↩︎
As mentioned in the NexT & izhengfan Combined Theme article, I migrated this theme from Jekyll to Hexo. I really like this current theme ↩︎
As of 2019-01-18, this feature was originally planned to be added in the
v0.31version in October 2017, but was repeatedly postponed, and is now placed in thev0.55release plan ↩︎