我的 Hugo 工作流程

(Photo by Christin Hume on Unsplash)

高中時曾任校刊社社長,也許對於出版不會太陌生。從 2017 年開始轉換使用 Hugo,逐步打造相對應的工作流程與工具,在這裡做個紀錄。不論入門或活用,希望大家能從中獲得能為自己提升工作效率的片段 :)



優缺點比較

當時採用以及現今繼續採用 Hugo 作為 blog 基礎系統的幾個優點:

  • 開放原始碼 - https://github.com/gohugoio/hugo
  • 開放格式
    • 從筆記整理、草稿撰寫、到 Hugo 內容產出、甚至整理投影片,全程得以使用共通的開放格式 Markdown,節省許多資料轉換時間。
    • 內容檔案格式為純文字 (Front Matter + Markdown),未來若需要轉換格式有出路
    • 內容檔案格式為純文字,搭配版本控制或檔案同步機制,可以自己跨平台、任選編輯器
    • 原始內容可以放進版本控制、同時達成異地備份
  • 當時觀察了好一陣子,Hugo 使用人數夠多且呈現增加趨勢,不會太快變成孤兒(不會太快再轉換一次)
  • 產出為靜態檔案,不需要維護特定規格的主機,找個 GitHub PagesAmazon S3 丟上去後設定 DNS 指向即可
  • (各種意義上的節省時間)(例如在 Macbook Air (M1, 2020)build 343 pages 只需要 1077 ms,平均每頁花費 3.14 ms)
  • 有指令列工具 (CLI),方便進行自動化
  • 擴充套件偏少(我覺得是優點,不會犧牲「快」)

缺點:

  • 一起參與協作的同伴,有學習門檻。學習 Markdown 等語法和作業流程

編輯器

Visual Studio Code (VSCode)

Hugo 對各種編輯器都有社群貢獻的一些擴充套件

我主要在 macOS 上使用 Visual Studio Code 來編輯 Hugo 的內容檔案。沒特地安裝 Hugo 控制相關套件(啊就 CLI 控),倒是安裝了些 Hugo、Markdown 語法周邊:

關於輸入圖檔路徑以及預覽,還在找尋其他有效率的做法,目前是盡量固定檔名命名規則,然後大量複製貼上 ![](/img/2021/2021-06-19-hugo-workflow/filename_here.png),接著到 Hugo 在地預覽(瀏覽器)上查看,希望未來有機會直接在編輯器內預覽。

Joplin

在某些場合不方便開啟電腦、或來不及開啟電腦時,我會使用跨平台的 Joplin 來編輯 Markdown,或是先快速做筆記片段暫存。等到整理文章時再來剪貼統整。之後再另外準備一些文章來討論 Joplin。

Joplin 也是開源軟體,可以達成跨平台,所以我在 Android phone、iPhone、iPad、Macbook Pro/Air、Windows 都可以同步筆記,繼續下一個階段的編輯整理動作。

Byword

在開始使用 Joplin 之前,使用過一小段時間的 Byword。目的相同,需要跨平台、任何裝置順手抓來就要可以快速進入做筆記的狀態,然後不用擔心跨裝置之間筆記同步掉資料的問題。且 Byword 無法運作在 Android 裝置上,這有時候有點困擾,因為出差時的主力機是 Android。

Obsidian

身邊陸續越來越多朋友跟我推薦 Obsidian,果然一開起來玩就停不下來。雖然不是開源軟體,但我的底線是開放的資料格式。我很期待運用 Obsidian 插入圖片後可以立即預覽的功能來編輯 Hugo 內容檔案,但至少食譜類的筆記已經往這邊移動了。但目前無法成為主力,因為只能在電腦上操作,行動裝置上還未有公開的版本。再觀察看看。


Hugo

環境設定 config.toml

設定檔可以多爬 Hugo 官方文件,以及 Hugo 官方 Discourse 討論區

一開始搞不清楚要在哪裡設定,但多看幾個文件後,可以鎖定幾個目錄位置,每個都動手設定看看,會比較快上手。

因為想練習英文寫作,所以很貪心地從一開始就將手上的 Hugo 往多國語系方向做設定(也為後來的某些客服系統打下基礎)。多國語系的架構、對應、翻譯、片段更新與維護,還是個值得研究的題目。

 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

目前使用了兩種內容:postnote。前者紀錄隨著時間順序而興起撰文者,後者嘗試相互連結形成網狀結構,也許會往 Digital Garden 方向整理(從清理蜘蛛網,到種花種草?!)。

我也建立了個別的 archetypes 檔案、以及個別的 create new shell scripts 對應到 postnote 兩種內容格式。

以下是 create new post 的 shell 範例:(若你沒有將圖檔依照各篇文章用目錄區隔開的需求,可以忽略這個範例,直接 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

我是將 blog 當作筆記系統的延伸,所以常常有個想法、或想到之間的關聯性等等,就會回頭做些修改或調整,因此希望能呈現出該文章建立與最後一次修改的日期。參考了這幾篇文章,實作了 lastmod 這個功能。


自定義 shell aliases and functions

參考自 Christian Mohn 的工作流程,稍作修改成為自己的版本。

若大家有興趣,之後我會更新在我的 dotfiles.aliases.local.example 裡頭(現在 dotfiles 裡頭因為 Apple Silicon 而一片混亂中,敬請小心服用 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
}

因為在編輯文章時,有時需要大量螢幕截圖(例如每年年底 AWS re:Invent Keynote 筆記),每次螢幕截圖後還需要手動搬移檔案到 Hugo 專案的 static/img/年份/該篇文章專屬目錄 底下,加上有時還要做轉檔與壓縮優化等步驟,實在頗累人。後來索性將 Dropbox Screenshots 關閉,但沿用 ~/Dropbox/Screenshots 目錄路徑為預設值,同時新增一個小工具,當我在編輯某篇文章時,將預設儲存螢幕截圖的目錄,改成我的 Hugo 靜態檔案存放的對應路徑,用完再切回預設目錄。

Script 中使用的 info/success,大家替換成 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

使用 ImageOptim 讓圖片載入加速。

圖片來源主要是 Unsplash 或是個人出差出遊累積的大量照片(早期出門一天平均產生一兩千張照片,近年有比較收斂些)(好啦,都是拍吃的或是拍飛機居多,哈哈)。每篇文章會特地做 ogimage 以及 twitter card 處理,都在 Front Matter 搞定。你說偷懶、沒空的時候怎麼辦?身為一位負責任的工程師,凡走過必留下預設值 :p


Favicon

參考 Add Favicon to Hugo-Based Website | Igor Baiborodine

  • 新增一個 layouts/partials/favicon.html
    • 修改 layouts/partials/head.html 引入 favicon partials。
  • Step 1: Add Favicon Using favicon.io
    • 快速簡單先做一個基底 favicon。
    • 然後再丟給下一步做更完整的描述檔案。
  • Step 2: Add Favicon Using realfavicongenerator.net
    • 上傳前一步產生的 android-chrome-512x512.png 進去產生完整檔案包。
    • 我在 Favicon Generator Options 這個段落,加上使用分頁 Compression 做壓縮。
    • 下載檔案包,全部放到 hugo site static/ 目錄下。
    • 產生的 HTML code 放進 layouts/partials/favicon.html 裡頭。

Footnotes and Citations

參考 Hugo Footnotes and Citations - GeekThis

在 Hugo 設定檔案中開啟 Footnotes 參數:

現在新的 Hugo 版本都使用 Goldmark。

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

Footnotes 語法格式:

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

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

CSS 調整使之長相老派:

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

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

Paginator

(新增於 2023-09-19)

這陣子嘗試提早在 hugo 建立未來想要使用的草稿文章 (draft post),有將 front matter 設定 draft = true。但我使用的 hugo theme 頗為老舊,上游已經停止更新,我自己也做了滿多改動,暫時沒有心力套用近代剛發行的其他 hugo theme,也因此累積了超過一定數量之後的草稿文章後,我的首頁文章清單會因為 paginator 計算分母出錯(誤將草稿文章也計入分母)而出現空白的情況。在 GPT-4 的協助、以及交叉檢查文件作確認之下,完成了修改,現在 paginator 可以正確計算分子分母、正確顯示頁面內容了。

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 }}

以下是 GPT-4 修改 paginator 的步驟說明

感謝你提供的詳細內容。現在讓我們來解決這個問題。

首先,我們需要在 layouts/index.html 的開頭正確設置分頁(paginator)。

  1. layouts/index.html 中的最上方,加入以下程式碼:

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

    這將會建立一個名為 $paginator 的新分頁物件,該物件已經過濾掉設為隱藏和草稿的頁面。

  2. 現在,在你的 layouts/index.htmlrange 循環中,替換原本的 .Paginator.Pages 參照為 $paginator.Pages

    從:

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

    改為:

    {{ range $paginator.Pages }}
    

    因為我們已經過濾掉設為隱藏的頁面,所以現在不再需要進一步的 where 條件。

  3. 修改 layouts/partials/pagination.html

    將:

    {{ $pag := $.Paginator }}
    

    改為:

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

    但考慮到我們已經在 layouts/index.html 中建立了 $paginator,因此我們實際上應該在 pagination.html 中使用 $paginator

    為了達到這個目的,你應該傳遞 $paginatorpagination.html partial。這是如何做的:

    layouts/index.html 中,將:

    {{ partial "pagination.html" . }}
    

    改為:

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

    然後,在 layouts/partials/pagination.html 中,你可以直接使用 $pag(因為它現在指向你傳遞的 $paginator)。

    請試著進行這些修改,然後重新生成你的網站。如果你遇到其他問題或有疑慮,請隨時讓我知道!


工作流程

(2016 拍攝於西班牙馬德里一家書店,那天會議下班後原本想去的圖書館沒開放。)


前置作業

  • 閱讀,大量閱讀 ,這部份另開一局做拆解。基本流程設計是從閱讀後的筆記與總結中,抽取出可以轉寫成文的靈感,放入待辦清單中。
  • 筆記,剛才有提過,目前主力使用 Joplin 搭配 Dropbox 同步,有在考慮 Joplin Server。
  • 筆記系統的主力在 2021Q4 逐步改用 視覺系筆記 Heptabase
  • 筆記,另外還有使用 Instapaper 等工具,待另開一文分享。可以先參考 我的 個人知識系統 工作流程
  • 紙本筆記本,從小學開始我身邊一定都會帶或大或小的紙本筆記本,也曾一度用便條紙做實驗,直到遇到 FIELD NOTES 之後就沒換過了。對我來說大小適中,我通常使用 Original Dot-Graph 或是 Original Graph Paper 方便書寫時對齊。通常都趁著 Black Friday 或 Prime Day 進一些庫存。
  • 待辦清單,之前使用 OmniFocus ,但後來為了完整跨平台與協作改用另一套,未來一併整理到 我的 個人知識系統 工作流程 做介紹。

作業中

  • 從待辦清單中安排一個時段,撰寫一個寫作主題,例如本篇。
  • 接著在 CLI 中開始建立 Hugo 內容檔案,並將對應該篇文章的靜態圖檔目錄建立起來。未來若覺得 slug 想要修改,直接在 Front Matter 中修改即可,檔名和目錄名稱就不做更動,例如本篇我就從 hugo-workflow 修改成 my-hugo-workflow
  • 或是想要回頭修改既有內容檔案,則快速切換到 Hugo 專案目錄。
1
2
3
4
5
6
❯ hugonew hugo-workflow

or

❯ blogd
❯ code .
  • 開啟本地端預覽,
 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
  • 因為這個 terminal 已被 hugo server 使用中,所以使用 iTerm2 Cmd + Shift + T 開啟同一路徑在另一個分頁。
  • 切換到 Hugo 專案中的靜態圖檔目錄後,
1
❯ scapture-here
  • 暫離、或是完成編輯,不再需要將螢幕截圖傳送到靜態圖檔目錄:
1
❯ scapture-default
  • 書寫時,從前述各個筆記系統搜尋相關材料,盡力讓相關的資訊或知識藉由 URL 關聯起來,方便自己或讀者交叉參照。
  • 相比於 WordPress 之類的系統,Hugo 讓我專注在內容寫作。
  • 完成草稿後,使用 Hugo 在地預覽模式閱讀個幾遍,若內容有值得標記畫底線之處,使用 自定義的 Hugo Shortcode 來畫底線,範例可以參考這一篇 AWS re:Invent Keynote 筆記

後製作業

  • 初版內容整理完成後,會再抽空多看幾次。檢查中英文版本是否能正確切換、檢查站內相對應連結能否順利抵達、回顧寫作過程中有沒有觀察到新的需求,要來更新這個 Hugo 工作流程,讓未來的自己更有效率(效率偏執但應該還不到狂,我有導入適量的偷懶來做平衡(炸 XDD)。
  • Blog 主要是寫給自己(以及未來的家人?!)看的,倒也不追求發表速度(Hugo 很快,但不急),對於內容的查證相對比較在意。
  • 抽空批次讓 ImageOptim 整理一下圖檔們。
  • 版本控制、備份與部署。
  • 目前是部署到 GitHub Pages 然後搭配自己的 domain name。未來也許會試試看 Amazon S3 + CloudFront 玩些變化。
    • GitHub Pages Docs 提到:可以在發布的根目錄加入 .nojekyll 檔案,可以跳過預設的 Jekyll build 流程步驟,節省一些時間。

後記

Hugo 以及這套個人知識系統 工作流程,不一定適合每一個人(倒是,這個句型幾乎適合每一個場景)。若您對其中哪個環節感興趣,歡迎一起交流討論,一起增進效率 :)

Buy Me a Coffee 如果這個分享對你有幫助,歡迎拍打餵食一杯咖啡 ☕ 😍

Loading comments…