My Hugo Workflow

(Photo by Christin Hume on Unsplash)

When I was in high school, I was the director of the school magazine club, that makes me be a little bit familiar with publishing. I started to use Hugo since 2017, gradually build the corresponding workflow and tools, and have some notes here. Regardless of entry or application, I hope you can get some clips that can improve your work efficiency :)



Advantages and Disadvantages

Several advantages of adopting Hugo at that time and continuing to use Hugo as my blog system:

  • Open source - https://github.com/gohugoio/hugo
  • Open format
    • From organizing notes, writing drafts, to Hugo content output, and even organizing slides, all in the common open format - Markdown - can be used throughout, saving me a lot of data conversion time.
    • The Hugo content file format is plain text (Front Matter + Markdown), there is a way out if I need to use another blog system in the future.
    • The Hugo content file format is plain text, with version control or file synchronization mechanism, I can choose any editor across multiple platforms.
    • The original content can be put into version control, while achieving remote backup.
  • After observing for a long time, Hugo has a large number of users and shows an increasing trend. It will not become an orphan too soon (I don’t need to switch again too soon).
  • The output are static files, and there is no need to maintain a host of a specific specification. By using GitHub Pages or Amazon S3, I can just upload, set the DNS, and done.
  • Fast (time saving in various senses) (for example, it only takes 1077 ms to build 343 pages on Macbook Air (M1, 2020), and the average time spending per page is 3.14 ms).
  • There is a command line tool (CLI) to facilitate workflow automation.
  • Few plugins/expansion kits (I think it’s an advantage, not sacrificing “fastness”).

Disadvantages:

  • Companions who participate in collaboration together have a learning curve. e.g. learn Markdown, shortcode code style, git and workflow .

Editor

Visual Studio Code (VSCode)

Hugo has some extensions contributed by the community to various editors.

I mainly use Visual Studio Code on macOS to edit Hugo content files. I did not install Hugo control related packages (I prefer using CLI), but installed some Hugo and Markdown syntax plugins:

Regarding the input of the image file path and preview, I am still looking for other efficient methods. At present, I try to fix the file name naming rule as much as possible, and then copy and paste a large number of ![](/img/2021/2021-06-19-hugo-workflow/filename_here.png), and then go to the Hugo local preview (browser) to view it, and hope to have the opportunity to preview it directly in the editor in the future.

Joplin

When it is inconvenient to turn on the computer in some occasions, or when it is too late to turn on the computer, I will use the cross-platform Joplin to edit Markdown, or make a quick note clip first. I will prepare some articles to discuss Joplin in the future.

Joplin is also an open source software, which can achieve my cross-platform needs, so I can synchronize notes across Android phone, iPhone, iPad, Macbook Pro/Air, Windows, and continue the next stage of editing and organizing.

Byword

Before I started using Joplin, I used Byword for a short while. The purpose is the same. It needs to be cross-platform and cross-device, to quickly enter the state of taking notes, and then there is no need to worry about the problem of synchronizing notes across devices. And Byword cannot work on Android devices, which is sometimes a bit troublesome, because my main device is an Android phone when traveling abroad.

Obsidian

More and more friends around me recommended Obsidian, and I couldn’t stop playing it when I started. Although it is not open source software, my bottom line is open data format. I am looking forward to using Obsidian’s ability to immediately preview after inserting pictures to edit Hugo content files, but at least my recipe notes have moved here. It cannot be my main editor force at the moment, because it can only be operated on a computer, and there is no public version on mobile devices. Keep watching on it.


Hugo

Configuration config.toml

The Hugo configuration information can be found at Hugo official document, and Hugo official Discourse discussion area.

At first, I don’t know which file to set it, but after reading a few more articles, you can lock several directory locations, and you can set it up and see each one. It will be a good way to get started.

Because I wanted to practice my English writing, I greedily set Hugo in the direction of multilingualism from the beginning. It’s a good topic of research about the structure, correspondence, translation, segment update and maintenance of the multi-language system.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
[languages]
  [languages.en]
    LanguageName = "English"
    weight = 1

    [[languages.en.menu.main]]
    url    = "/about"
    name   = "About"
    weight = 5 
    [[languages.en.menu.main]]
    url    = "/notes/aws/products/"
    name   = "AWS Product Lists"
    weight = 10   
    [[languages.en.menu.main]]
    url    = "/notes/aws/"
    name   = "AWS Notes"
    weight = 20

  [languages.zh]
    LanguageName = "中文"
    weight = 2

    [[languages.zh.menu.main]]
    url    = "/about"
    name   = "關於"
    weight = 5

    [[languages.zh.menu.main]]
    url    = "/notes/aws/products/"
    name   = "AWS 產品清單"
    weight = 10

    [[languages.zh.menu.main]]
    url    = "/notes/aws/"
    name   = "AWS 筆記"
    weight = 20      

Content types

I am using two types of content: post and note. The former records opinions in chronological order, while the latter attempts to connect to each other to form a web-like structure, perhaps in the direction of Digital Garden (from cleaning up spider webs to planting flowers and grass?!).

I also created individual archetypes files and individual create new shell scripts corresponding to the two content formats of post and note.

The following is a shell example of creating new post: (If you do not have the requirement to separate the image files by directories according to each article, you can ignore this example and just using hugo new)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/bin/sh

# If a command fails then the deploy stops
set -e

POSTNAME=$1
THISYEAR=`date +'%Y'`
TODAY=`date +'%Y-%m-%d'`

POSTPATH="${THISYEAR}/${TODAY}-${POSTNAME}"
IMGFOLDER="./static/img/${POSTPATH}/"
NEWPOSTZH="posts/${POSTPATH}.zh.md"
NEWPOSTEN="posts/${POSTPATH}.en.md"

echo "[  ] Initing..."
echo "THISYEAR = "${THISYEAR}
echo "TODAY = "${TODAY}
echo "POSTPATH = "${POSTPATH}
echo "IMGFOLDER = "${IMGFOLDER}
echo "NEWPOSTZH = "${NEWPOSTZH}
echo "NEWPOSTEN = "${NEWPOSTEN}
echo

# Create static image folder
mkdir -p ${IMGFOLDER}
echo "[OK] mkdir -p ${IMGFOLDER}"
echo

# Create hugo new post
hugo new ${NEWPOSTZH}
echo "[OK] hugo new ${NEWPOSTZH}"
echo

hugo new ${NEWPOSTEN}
echo "[OK] hugo new ${NEWPOSTEN}"
echo

lastmod (last modified)

I use blog as an extension of the note-taking system, so I often have an idea, or the relevance between thoughts, etc., and I will go back and make some changes or adjustments, so I hope to show the date when the article was created and my last modified. After refered to couple articles and implement the lastmod function in my Hugo setup.


Custom shell aliases and functions

Refered from Christian Mohn’s workflow, and did some modification to fit into my edition.

If you are interested in, I will update it at .aliases.local.example in my dotfiles project. (Currently my dotfiles project is in chaos due to Apple Silicon. Please take care XDD.)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
###################################################################################
# Blog Operation: Hugo
###################################################################################

PATH_BLOG=~/projects/ernestchiang/www
PATH_DEFAULT_SCREENCAPTURE="/Users/dwchiang/Dropbox/Screenshots"
alias blogd="cd '$PATH_BLOG'"

function blogserv() {
    cd $PATH_BLOG
    open "/Applications/Google Chrome.app" http://localhost:1313
    hugo server -D -F -w
}

function hugonew() {
    cd $PATH_BLOG && ./hugo-new-post.sh $1 && code .
}

function move-image-here() {
    mv ~/Downloads/image.jpg $(pwd -P)/index.jpg
}

When editing articles, sometimes a lot of screenshots are required (for example, AWS re:Invent Keynote Notes at the end of each year), after each screenshot, I also need to manually move the files to the Hugo project’s static/img/year/the-article-exclusive-directory, plus sometimes there are steps such as conversion and compression optimization. It’s really tiring. Therefore, I choosed to turn off Dropbox Screenshots, and make ~/Dropbox/Screenshots directory path as the default folder of screenshots. Then made a simple tool script. When I was editing an article, I changed the default directory for storing screenshots to the corresponding path where my Hugo static files of the article are stored, switch back to the default directory after my editing.

You can replace the info/success used in the script with echo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
###################################################################################
# Screen Capture Location
###################################################################################

function scapture-here() {
    info 'Changing default screencapture location from:'
    defaults read com.apple.screencapture location

    defaults write com.apple.screencapture location $(pwd -P)
    success 'Changed default screencapture location to:'
    defaults read com.apple.screencapture location

    info 'Remember to execute `screencapture-default`'
}

function scapture-default() {
    info 'Changing default screencapture location from:'
    defaults read com.apple.screencapture location

    defaults write com.apple.screencapture location ${PATH_DEFAULT_SCREENCAPTURE}
    success 'Changed default screencapture location to:'
    defaults read com.apple.screencapture location
}

Images

I use ImageOptim to speed up image loading.

The source of the pictures is mainly Unsplash or a large number of photos accumulated during my trips (I took average one or two thousand photos per day when I go out in the early days, but in recent years it has been more convergent) (well, mostly food and aviation pictures, haha). Each article will be specially processed for ogimage and twitter card, which is done in Front Matter. You may ask that what do you do when you feel lazy or have no time to find a picture? I would say… as a responsible engineer, I will leave a default value wherever I pass by :p


Favicon

Reference to Add Favicon to Hugo-Based Website | Igor Baiborodine.

  • Create a new file: layouts/partials/favicon.html
    • Modify layouts/partials/head.html to include this new created favicon partials.
  • Step 1: Add Favicon Using favicon.io
    • It’s a easy way to have a base favicon package.
    • Then go to next step to have wider manifest coverage.
  • Step 2: Add Favicon Using realfavicongenerator.net
    • Upload android-chrome-512x512.png (generated in Step 1) to generate a full package.
    • I choose to do the compression at the tab named Compression in Favicon Generator Options section.
    • Downlod the package. Unzip and place them all at the static/ folder of the hugo site.
    • Copy and paste the generated HTML code into layouts/partials/favicon.html.

Footnotes and Citations

Reference to Hugo Footnotes and Citations - GeekThis.

Enable footnotes parameter in your Hugo config file:

Nowaday, the newer Hugo versions are using Goldmark.

[markup]
  defaultMarkdownHandler = "goldmark"
  [markup.goldmark]
    [markup.goldmark.extensions]
      footnote = true

Footnotes markdown format:

That's some text with a footnote.[^1]

[^1]: And that's the footnote.

Adjust CSS for the old school looking:

a.footnote-ref::before {
    content: '[';
}

a.footnote-ref::after {
    content: ']';
}

Paginator

(Added on 2023-09-19)

Lately, I’ve been trying to create draft posts in Hugo ahead of time by setting the front matter to draft = true. However, the Hugo theme I’m using is quite outdated, with no recent updates from the upstream, and I’ve made quite a few modifications myself. I currently don’t have the energy to switch to a more modern Hugo theme that has just been released. As a result, after accumulating a certain number of draft posts, my homepage article list would display blank due to a paginator calculation error (wrongly including draft posts in the denominator). With the assistance of GPT-4 and cross-referencing the documentation, I’ve made the necessary modifications, and now the paginator can correctly calculate the numerator and denominator, displaying page content correctly.

layouts/index.html:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
{{ $pages := where (where .Site.RegularPages "Params.hidden" "ne" "true") "Draft" "!=" true }}
{{ $paginator := .Paginate $pages }}
{{ partial "head.html" . }}
<body>
{{ partial "google_gtm_body.html" . }}
<div id="container">
    {{ partial "header.html" . }}
    <section id="main" class="outer">
        {{ range $paginator.Pages }}
        <article class="article article-type-post" itemscope itemprop="blogPost">
            <div class="article-inner">
                <!--<%- partial('post/gallery') %>-->
                {{ if .Title }}
                <header class="article-header">
                    <h1 itemprop="name"><a class="article-title" href="{{ .RelPermalink }}">{{ .Title }}</a></h1>
                </header>
                {{ end }}
                <div class="article-meta">
                    <a href="{{ .RelPermalink }}" class="article-date">
                        <strong>Published: </strong><time datetime='{{ .Date.Format "2006-01-02T15:04:05.000-07:00" }}' itemprop="datePublished">{{ .Date.Format "2006-01-02" }}</time>
                    </a>
                    {{ $Site := .Site }}
                    {{ if .Params.categories }}
                    <div class="post-categories">
                        <div class="article-category">
                            {{ range $i, $e := .Params.categories }}
                            {{ if gt $i 0 }}
                            <span>&gt;</span>
                            {{ end }}
                            <a class="article-category-link" href="{{ $Site.Home.Permalink }}categories/{{ $e | urlize }}">{{ $e }}</a>
                            {{ end }}
                        </div>
                    </div>
                    {{ end }}
                </div>
                <div class="article-entry" itemprop="articleBody">
                    <p>
                        {{ if .Params.featuredImage }}<img src="{{.Params.featuredImage}}"><br>{{ end }}
                    {{ .Summary }}
                    </p>
                    <p class="article-more-link">
                        <a href="{{ .RelPermalink }}">Read More</a>
                    </p>
                </div>

                {{ if .Params.tags }}
                <footer class="article-footer">
                    <ul class="article-tag-list">
                        {{ range .Params.tags }}
                        <li class="article-tag-list-item">
                            <a class="article-tag-list-link" href="{{ $Site.Home.Permalink }}tags/{{ . | urlize }}">{{ . }}
                            </a>
                        </li>
                        {{ end }}
                    </ul>
                </footer>
                {{ end }}
            </div>
        </article>
        {{ end }}

        {{ partial "pagination.html" $paginator }}
    </section>
    {{ partial "footer.html" . }}
</div>
</body>
</html>

layouts/partials/pagination.html:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{{ if gt $.TotalPages 1 }}
<nav id="page-nav">
    {{ if $.HasPrev }}
    <a href="{{ $.Prev.URL }}" rel="prev" class="extend prev">&laquo; Prev</a>
    {{ end }}
    {{ range $.Pagers }}
    {{ if eq . $ }}
    <span class="page-number current">{{ $.PageNumber }}</span>
    {{ else }}
    <a href="{{ .URL }}">{{ .PageNumber }}</a>
    {{ end }}
    {{ end }}

    {{ if $.HasNext }}
    <a href="{{ $.Next.URL }}" rel="next" class="extend next">Next &raquo;</a>
    {{ end }}
</nav>
{{ end }}

Below are the steps to modify the paginator by GPT-4

Thank you for providing the detailed information. Let’s address this issue.

1. Set up Pagination in layouts/index.html:

  • At the very top of layouts/index.html, add the following code:

    {{ $pages := where (where .Site.RegularPages "Params.hidden" "ne" "true") "Draft" "!=" true }}
    {{ $paginator := .Paginate $pages }}
    

    This creates a new pagination object named $paginator that filters out hidden and draft pages.

2. Update the Range Loop:

  • Replace the original reference from .Paginator.Pages to $paginator.Pages within your layouts/index.html’s range loop:

    Change:

    {{ range where .Paginator.Pages "Params.hidden" "ne" "true" }}
    

    To:

    {{ range $paginator.Pages }}
    

    Since hidden pages are already filtered out, you don’t need further where conditions.

3. Modify layouts/partials/pagination.html:

  • Replace:

    {{ $pag := $.Paginator }}
    

    With:

    {{ $pag := $.Site.Paginator }}
    

    Considering that you’ve created $paginator in layouts/index.html, use $paginator in pagination.html.

  • To achieve this, pass $paginator to the pagination.html partial. Update the following:

    In layouts/index.html, change:

    {{ partial "pagination.html" . }}
    

    To:

    {{ partial "pagination.html" $paginator }}
    

    Then, in layouts/partials/pagination.html, you can directly use $pag (as it now points to the $paginator you passed).

Please implement these changes and regenerate your website. If you encounter any other issues or have concerns, feel free to let me know!


Workflow

(2016. It was taken in a bookstore in Madrid, Spain. The library that I wanted to go to after the meeting was closed that day.)


Pre-work

  • Read, read a lot, this part I will describe and decouple more in another article. The basic process design is to extract inspiration that can be transcribed into an article from the notes and after-reading summary, and put it on the to-do list.
  • Note, as mentioned earlier, currently my main force is using Joplin and synchronize with Dropbox. Joplin Server is being considered.
  • Note, there are also tools such as Instapaper, etc., to be shared in another article in the future.
  • Paper notebook, since elementary school, I will always bring large or small paper notebooks around me. I also used note paper for experiments until I met FIELD NOTES I haven’t changed it since. For me the size is moderate, I usually use Original Dot-Graph or Original Graph Paper to facilitate the alignment when writing. Usually take advantage of Black Friday to get some inventory.
  • To-do list, previously used OmniFocus, but now I changed to another one for complete cross-platform and collaboration.

Working in progress

  • Arrange a time period from the to-do list to write a topic, such as this article.
  • Then start to create the Hugo content file in the CLI (in iTerm), and create the static image file directory corresponding to the article. In the future, if I want to modify the slug, I can modify it directly in Front Matter, and the file name and directory name will stay the same. For example, this article, I have changed slug from hugo-workflow to my-hugo-workflow.
  • Or if I want to go back and modify existing content files, I can just quickly switch to the Hugo project directory by using custom alias.
1
2
3
4
5
6
❯ hugonew hugo-workflow

or

❯ blogd
❯ code .
  • Open local preview in browser,
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
❯ blogserv
Start building sites …

                   | EN  | ZH
-------------------+-----+------
  Pages            | 123 | 220
  Paginator pages  |   6 |  18
  Non-page files   |   2 |   2
  Static files     | 487 | 487
  Processed images |   0 |   0
  Aliases          |  65 |  83
  Sitemaps         |   2 |   1
  Cleaned          |   0 |   0

Built in 1077 ms
Watching for changes in /Users/dwchiang/projects/ernestchiang/www/{archetypes,content,data,layouts,static,themes}
Watching for config changes in /Users/dwchiang/projects/ernestchiang/www/config.toml
Environment: "development"
Serving pages from memory
Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender
Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)
Press Ctrl+C to stop

Change detected, rebuilding site.
2021-06-19 15:14:59.831 +0800
Source changed "/Users/dwchiang/projects/ernestchiang/www/content/posts/2021/2021-06-19-hugo-workflow.zh.md": WRITE
Total in 374 ms
  • Because this terminal has been using by the hugo server, I use iTerm2 Cmd + Shift + T key binding to duplicate another terminal tab with the same path.
  • After switching to the static image file directory in the Hugo project,
1
❯ scapture-here
  • Leave or finish editing, no longer need to send screenshots to the static image file directory:
1
❯ scapture-default
  • When writing, search for relevant materials from the aforementioned note-taking systems, and try to make relevant information or knowledge related by URL, so that readers or myself can cross-reference.
  • Compared to systems like WordPress, Hugo allows me to focus on content writing.
  • After finishing the draft, I will use Hugo local preview mode to read it several times. If the content is worthy of marking highlight line, I will use custom Hugo Shortcode to draw the highlight line, the example can refer to this article AWS re:Invent Keynote notes.

Post-work

  • After finishing the content of the first edition, I will take the time to read it a few more times. Check whether the Chinese and English versions can be switched correctly, whether the corresponding links can arrive smoothly, review whether there are any new requirements observed during the writing process, and update my Hugo workflow to make the future self more efficient (sort of efficiency paranoid…).
  • The blog is mainly written for myself (and future family members?!), but it does not pursue the speed of publication (Hugo is fast, but no need to be in a hurry), and is relatively concerned about the verification of the content.
  • Take the time out of batches and let ImageOptim organize the images.
  • Version control, backup and deployment.
  • Currently it is deployed to GitHub Pages and matched with my own domain name. In the future, I might try Amazon S3 + CloudFront to implement some ideas.
    • GitHub Pages Docs: You can add a file named .nojekyll i your publish root folder, so that GitHub Pages will skip the Jekyll build step and save some build time.

Bottom line

Hugo and this personal knowledge management workflow may not be suitable for everyone (instead, this sentence pattern is suitable for almost every scene). If you are interested in any of these links, welcome to communicate and discuss together to increase efficiency together :)

Buy Me a Coffee If this sharing is helpful to you, feel free to buy me a coffee ☕ 😍

Loading comments…