Hugo 之旅

公子發表於2020-11-09

之前寫了篇文章《部落格遷移至 Hugo》,提了下使用 Typecho 多年後越發感受到運維的成本之高後,將部落格遷移到了靜態部落格程式 Hugo 下。使用 Vercel + Github 可以免費搭建高效能部落格,繫結域名還能自動幫忙建立 SSL 證書。當然偷懶的話也可以直接使用預設分配的二級域名。

搭建

建立 Hugo 部落格

點選上面的按鈕快速抵達建立頁面,未登入的會需要登入,這塊直接使用 Github 登入即可。登入後第一步會讓你選擇 Vercel 賬號,這裡直接選擇 Personal Account 即可。之後會讓你輸入倉庫名稱,Vercel 會自動幫你建立並初始化該倉庫。如果你的倉庫不想讓其它人看的話,這裡可以勾選 Private Git Repository 建立私有倉庫。

下一步這塊會讓你輸入 Vercel 中專案的名稱和一些配置。這裡需要注意一下,官方提供的預設 Hugo 編譯命令會把草稿文章也生成出來。需要在 BUILD COMMAND 那開啟 OVERRIDE 按鈕後輸入 hugo --gc 進行覆蓋。

稍等片刻之後,你就可以看到飄著滿屏的綵帶慶祝你建立部落格成功叻!點選 Visit 按鈕你就可以看到你的部落格的樣子了。

由於是靜態部落格,你所有的文章都會儲存在你剛才新建的倉庫中。你可以選擇將倉庫下來修改後提交,也可以利用 Github 的線上編輯功能線上修改提交。提交之後 Vercel 會自動觸發更新,重新構建並更新你的部落格。

配置

預設每次提交 Vercel 構建完成之後都會把構建後的地址評論在你的 Github 提交下。你可以透過設定關閉該功能。另外預設 Vercel 指定的 Hugo 版本比較老了,在 Markdown 編譯過程中會發生一些異常的行為。我們可以透過配置指定最新的 Hugo 版本進行編譯。在專案根目錄下新建 vercel.json 檔案,並加入以下內容。

{
  "github": {
    "silent": true
  },
  "build": {
    "env": {
      "HUGO_VERSION": "0.78.1"
    }
  }
}

Hugo 部落格本身的配置都在 config.toml 下。預設情況下 Hugo 生成的 URL 都是 /posts/hello-world/ 這種格式,不過之前做動態部落格為了做偽靜態,一般都將路由設定成了 /hello-world.html 這種格式。這種時候就需要在 config.toml 中增加 uglyurls 配置。

uglyurls = true
[permalinks]
  posts = ":slug"

後面的 permalinks 配置主要是用來去除文章的 /posts 字首的。除此之外,預設的配置文章中的 HTML 是會被轉義的,對於我這種偶爾會在 Markdown 中寫 HTML 的人來說操作有點多餘。這時候可以在配置中定義它不轉義。

[markup.goldmark.renderer]
  unsafe= true

域名

使用 Vercel 搭建的網站,它會預設提供一個 *.vercel.app 的二級域名,你可以直接使用這個域名訪問網站。如果你想要繫結自己的域名,也可以在後臺設定。進入網站後選擇自己的專案,選擇 Settings - Domains 進入域名配置介面,在輸入框中新增自己的域名。它會提示你需要給域名增加 A 記錄或者是 CNAME 解析。按照提示新增後後臺會自動檢測是否生效。

生效的時間視 DNS 伺服器的生效時間而定,我這邊使用 DNSPod 還挺快的,大概 30 秒之內就生效了。生效後 Vercel 會自動幫我們申請配置 SSL 證書,我們全然不用操心證書的問題。等待片刻之後我們就能直接使用新域名進行訪問了。

圖片

預設所有的靜態資源都放在 static/ 目錄下。你可以將圖片放在該目錄下,例如 static/hello-world.jpg。在文章中則直接使用 /hello-world.jpg 地址引用即可。

由於 Github 僅有單檔案小於 100M 的限制,Vercel 會將所有的資源部署到自己伺服器上。所以使用倉庫儲存的方式會非常方便和安全,而且還不損失速度。唯一美中不足的是,由於是 HTTP 路徑,在本地編寫文章的時候會不方便。如果是使用 Typora 的話可以點選標籤欄 格式 - 影像 - 設定圖片根目錄 將目錄指定到 static/ 目錄解決。VSCode 的話暫時沒有倒騰出來。

其實比較好的方案是建議大家建立一個資料夾,將該篇文章和它所用到的圖片都歸置到一塊。使用類似元件化的思路管理內容,會更加方便後續的修改。

除了使用本地儲存之外,你也可以選擇使用第三方的儲存服務。免費服務的話可以試試 又拍雲 提供的聯盟計劃,按照要求申請下即可。其他家的儲存服務要麼免費時間有限,要麼有部分收費功能。第三方儲存的好處在於帶有 CDN 加速,在速度上會很方便。但目前 Vercel 的速度我覺得還挺不錯的,所以最終還是選擇了直接存倉庫。

評論

使用靜態部落格之後,評論則只能選擇第三方評論系統了。鑑於國情,國內的第三方評論服務都已名存實亡。目前比較知名的 Disqus , HyperComments 以及 來必力 都是國外的服務。HyperComments 是付費的服務就不多做討論,Disqus 是老牌服務,提供了強大的 API 對開發者較為友好。來必力則是對國內使用者友好,整合了很多國內的社交賬號登入。

除了專門提供評論的第三方服務之外,也有一部分是選擇自建評論服務。其中比較知名的是 ValineGitalk 以及 Staticmanisso。Staticman 提供了強大的 API 但是缺少介面,isso 則是需要伺服器部署,偏離了我使用靜態部落格的初衷就是不想維護伺服器的目的,所以這兩者都不多做討論。Gitalk 是使用 Github issue 進行評論資料儲存的評論指令碼,適合純技術部落格和極客使用。Valine 則是基於LeanCloud Serverless 雲端儲存進行評論資料儲存的評論指令碼,同時它還帶有漂亮的外觀,適合各類人群使用。

部落格站點上與評論相關的地方一般有三個地方:

  1. 首頁顯示最近評論
  2. 文章下顯示當前文章評論數
  3. 文章底部顯示該文章的評論列表以及輸入框

最開始我選擇了 Disqus 作為本站的評論系統。但是它有個問題國內正常無法訪問。後來我發現 Hugo 的模板中是可以使用 getJSON 方法調取介面的。而 Vercel 的機器本身就在海外,那我實際上可以在部落格編譯階段就獲取到輸入寫到頁面中。最近評論和評論數的顯示還是比較簡單的,而評論列表的顯示則需要折騰一下了。不過 Disqus API 的使用頻率是 1000 次/小時,當短時間部署頻繁的話可能會有超過的風險。而且釋出評論的介面沒辦法走該邏輯,還是需要一個代理服務。

最終讓我決定不使用 Disqus 的原因是它的評論沒辦法讓評論者輸入網站地址,評論列表中的評論者暱稱點選也無法跳轉到使用者輸入的網站中。這對於有著大量的老式評論資料的我來說是不太能接受的。所以基於這些種種原因,我最後又將評論遷移到了 Valine 下。

我對 Valine 的主要問題在於兩點:

  1. 早前使用 Valine 的時候發現了大量的 XSS 漏洞
  2. 荒野無燈大大反饋的“洩露使用者隱私問題

以上兩個是非常重要的安全問題,不過我看 XSS 的問題作者已經修復,實際上對使用者輸入的所有 HTML 內容進行轉義可以規避該問題。而燈大反饋的 IP 洩露的問題,作者也透過不記錄 IP 來規避該問題。這兩個問題造成的原因其實還是因為 Serverless 。因為我想直接在編譯的時候將相關資料靜態化,所以我勢必是需要增加一個服務端來幫我進行介面的封裝,最後也能規避這些問題。

不過 Valine 不知因為什麼原因不開放原始碼,只在 Github 提供編譯後程式碼,這點除了讓我比較無語之外,改造工作也比較慢。本來我只需要將它 UI 中呼叫 LeanCloud API 的邏輯替換成我的介面邏輯即可。但是因為沒有原始碼,只能自己重新制作 UI 了。

更新:基於 Valine 衍生的帶後端評論系統已經完成 https://github.com/lizheming/... 已切換至該評論系統。2020/11/08

搜尋

缺少服務端之後,靜態部落格的搜尋功能也無法自己完成。目前比較知名的是第三方搜尋服務 Agolia,提供了免費的文章索引和搜尋的功能。除了使用第三方服務之外,還發現有一種比較簡單的做法就是將所有資料生成到一個檔案中,前端下載該檔案進行搜尋結果展示。這裡頭比較知名的是 fuse,它是一個 JS 模組,將輸入傳入之後它能幫我們快速的匹配到命中結果。

目前本站使用的是後一種方法,該方法的優點是不依賴第三方服務,自己就能完成搜尋功能。缺點是第一次搜尋的時候需要下載完整的資料,對文章比較多的網站使用者鴨梨會比較大。使用第二種方法第一步是需要先建立全量的資料索引,透過以下配置告知 Hugo 編譯時建立 index.json 索引檔案。

[outputs]
  home = ["HTML", "RSS", "JSON"]

當然我們還需要為新的資料編寫生成的模板檔案,在 layouts/ 下增加 index.json 檔案,並加入以下內容:

{{- $.Scratch.Add "index" slice -}}
{{- range .Site.RegularPages -}}
    {{- $.Scratch.Add "index" (dict "title" .Title "contents" .Plain  "summary" .Summary "permalink" .Permalink "date" (.Date.Format "2006年01月02日")) -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}

其中 dict後的字典可以新增多個欄位,視你 JS 渲染指令碼中需要的欄位而定。完成這些之後 Hugo 編譯就會生成 index.json 檔案了。

最後我們需要增加一個搜尋頁面,該頁面會先載入 index.json 然後使用 fuse 進行資料查詢,最後渲染成 HTML 輸出搜尋結果。具體的程式碼可以直接檢視原始碼 https://imnerd.org/search.htm... 參考本站的搜尋頁。

//以下為示例程式碼
let fuse;
async function search(text) {
  if(!fuse) {
    const indexData = await fetch('/index.json', {method: 'GET'}).then(resp => resp.json());
    fuse = new Fuse(indexData, {...});
  }
  const result = fuse.search(text);
  renderHTML(result, '#app');
}

其它

Hugo 靜態部落格能提供給我們發揮的空間非常多,比如說我參考 屈屈的部落格 增加了一個部落格文章釋出 >180 天的話,就在文章詳情頁黃條提醒:

提醒:本文最後更新於 313 天前,文中所描述的資訊可能已發生改變,請謹慎使用。

正常來說由於我沒有服務端,所以只能使用前端來計算當前時間與釋出時間的時間差。但其實轉念一想,編譯時也是可以拿到時間差的,只是這個時間差無法隨著時間的變化而變化。不過我只要每天編譯一次,就能解決變化的問題了。在 Vercel 專案 Settings - Git - Deploy Hooks 中你可以輸入 Hook 名稱和觸發分支建立一個 Hook URL。只要訪問該 URL 就可以觸發 Vercel 更新部落格。

{{ if eq .Type "posts" -}}
    {{ $ageDays := div (sub now.Unix .Date.Unix) 86400 }}
    {{ if gt $ageDays 180 }}
        <p class="expired-tips"> 提醒:本文最後更新於&nbsp;{{$ageDays}}&nbsp;天前,文中所描述的資訊可能已發生改變,請謹慎使用。</p>
    {{ end }}
{{ end }}

而在 Vercel 的 Marketplace 中,有一個 EasyCron 的服務提供了定時任務的功能。我在上面設定了每天零點訪問 Hook URL 觸發部落格更新,就這樣解決每天需要更新時間的問題。

另外屈屈部落格中的“檢視本文Markdown版本”也是個不錯的功能,在 Hugo 中也可以實現,本質是在編譯的時候順便生成一份 .md 的 Markdown 檔案即可。

首先我們需要在 layouts/ 下增加 single.md 檔案,表示的是當文章頁(Single)需要匯出 .md 的資料的時候使用該模板。模板內容不用填寫,為空即可。然後在 config.toml 中新增 .md 檔案型別,並告知 Hugo 文章需要增加編譯 Markdown 檔案型別。

[mediaTypes]
  [mediaTypes."text/plain"]
    suffixes = ["md"]

[outputFormats.MarkDown]
  mediaType = "text/plain"
  isPlainText = true
  isHTML = false

[outputs]
  page = ["HTML", "MarkDown"]

相關文章