把 HackMD 當做部落格後臺 feat. Next.js | Daily Oops!
Daily Oops

Daily Oops!

May 16, 2021

把 HackMD 當做部落格後臺 feat. Next.js

沒什麼主題能寫的時候,更新一下部落格版型準沒錯

- 我,2017

就是這句話讓我現在又來發文 XD。

時間過得飛快,距離上次更新也過了三年多,但部落格文章也沒多寫幾篇。老實說也不是沒有寫,但不是放在私人的 Evernote,就是散落在 HackMD 上的程式碼片段和指令操作記錄,要直接貼出來還是有些羞恥的。

去年十月,突然覺得舊部落格版型實在用太久了,速度也有點慢、設計有點過時。發新文章時,還得在 HackMD 上寫完文章、在 GitHub Repo 建新檔案、再貼內容過去。若能打造一個更流暢愉快的工作流程,那我的文章產量一定能更高啊!(不要瞎掰好嗎)

改版計劃

這次的部落格改版有幾個需求:

  1. 速度。原本舊的部落格是用 Jekyll 架設,純靜態的網頁,換頁也使用了 Turbolinks 這個 Pjax 技術加速,無論是載入時間以及換頁都十分順暢。新的部落格系統希望能達到同樣的速度,甚至在這之上。
  2. 構建的易用性。原本的部落格使用原生的 GitHub Pages,另外拆出成 octoflavor Jekyll 主題。解釋一下「原生」和「非原生」GitHub Pages 的差別:
    • 原生的 GitHub Pages 就是 GitHub 幫你跑 Jekyll build,你只要推上 markdown 就好了,也不用綁什麼 CI 服務
    • 非原生的 GitHub Pages,就是把網頁的原始碼上傳到 gh-pages 分支,剩下來的 GitHub 幫你搞定。
  3. Node.js 派。我只想 npm 裝好裝滿。
  4. 良好的開發體驗。如何方便的幫部落格擴充也是重要的一環。Jekyll 固然不錯,但是老舊的 Liquid 樣板語法,和沒有內建現代前端套件管理就輸了一大截。
  5. 在 HackMD 上寫作。業配 GOOD!

至於我要如何達成這幾項需求呢?先讓我們進入頁配的環節吧 🤩

HackMD 的團隊文章列表 API

HackMD Public Team - DailyOops
HackMD 團隊 - Daily Oops!,存放文章的地方

HackMD 有一個非公開的 API:GET https://hackmd.io/api/@TEAM_PATH/overview,回傳公開頁下所有能見的筆記。以我的部落格的團隊 @DailyOops 為例:

GET https://hackmd.io/api/@dailyOops/overview
{
"team": ...,
"notes": [
{
"id": ....,
"content": "...",
"title": "...",
...
}
]
}

有了這些資料,就可以繼續我的部落格改版計劃啦!

隆重介紹:Daily Oops 2.0 feat. Next.js

我選擇 Next.js 作為這次部落格改版的主要技術,最大的原因就是雖然它是 React.js 框架,但內建「靜態網站產生」功能 (簡稱 SSG, Static-site generation)。

SSG 保有動態資料的彈性,以及靜態網站的簡潔與速度。你可以透過 API 取得的動態資料,來產生多個靜態頁面,而且是「預先渲染」好的靜態 HTML 網頁,不會有 Cilent side React,載入後才渲染的現象。在頁面切換時又能保有 React SPA 的順暢換頁,我全都要!

對於部落格這種更新頻率相對不用即時,靜態頁面居多的場景,Next.js 可說是相當適合。會採用 Next.js 的另一個原因,是當時看到了 notion-blog 這個專案,在 Notion 還沒有公開 API 的當下反解出 API,把 Notion 當做 CMS 來用,同樣搭配的是 Next.js 的 SSG 功能。

上圖就是新部落格的資料流:

  1. 從 HackMD 抓取文章資料
  2. 交由 Next.js 建置
  3. 完成靜態網站建置

程式碼放在 GitHub,接下來給各位帶來幾個值得一提的細節。

getStaticPathsgetStaticProps 的資料不互通

getStaticPaths 能拿到「靜態網站頁面列表」,getStaticProps 則是「靜態網站頁面資料」,但列表僅僅是列表。感覺還是有點抽象,我寫段簡化版的實作:

const pathData = getStaticPaths()
assert(pathData).deepEqual([
{ params: { id: '1' }},
{ params: { id: '2' }},
])
// params 會用在 /posts/[id] 動態網址的參數
async function getPostData (postUUID: string) {
// 問題:我們只有 id 而沒有 postUUID
return fetch(`/api/${postUUID}`)
// getStaticProps 只能拿到 params 當做參數
const props = getStaticProps({ params: { id: '1' } })
function getStaticProps (params) {
getPostData(params.id) // 無效
}

有個問題是「getStaticProps 只能用網址參數當做函式參數」。以上面的程式碼為例,取得文章內容的函式 getPostData 需要文章的 UUID,但我們 getStaticPaths 想要拿來當做網址的是數字 ID,這樣就沒辦法直接在 getStaticProps 呼叫 API 取得文章資料了。

我心目中理想的資料架構應該是這樣:

const data = getStaticData() // new getStaticPath method
assert(data).deepEqual([{
path: '/slug-1',
data: {
pageProp1: 'value'
}
}])
const props = getStaticProps('/slug1')
assert(props).deepEqual({
pageProp1: 'value'
})

getStaticData 是改版的 getStaticPath,你可以回傳除了 params 之外的參數,讓 getStaticProps 可以直接取用。

這個問題在 Next.js 的討論區 #11272 有相當多的討論,目前 upvote 最高的解答是自己在硬碟上快取一份資料,在 getStaticProps 時透過快取過的 API 去抓資料。我也自己實作了一份: lib/post.js

設計:Primer 設計系統

點擊圖片可看大圖。

在設計方面,目前還是個工程師的我,這次一樣採用了 GitHub 的設計系統 Primer。Primer 近年來進展迅速,還多了暗色主題的設計,色彩系統 開箱即用,不用自己慢慢調色。

Primer 的 Color System,直接套 class 就有暗色效果啦
這是一隻小貓熊,他很可愛,如果你沒看過,現在你看過了

Logo 也是自己畫的,Google 了一張參考圖就直上了,反正小貓熊哪張圖片都很可愛,對吧(啾

附上 Figma 工作區
Gif 圖也是用 Figma Plugin - GiffyCanvas 做的

Markdown 渲染

和 HackMD/CodiMD 一樣,繼續採用高度可擴充、社群套件豐富的 markdown-it。有興趣的朋友可以參考 lib/markdown.js

值得炫耀的是程式碼區塊的渲染整個從零開始,高仿 GitHub 採用 Table 排版,除了有複製到剪貼簿,還有行號標色,效果如下:

為了讓程式碼區塊的標色和 GitHub 完全一致,還根據 Primer 顏色系統產生了 GitHub 用的 Highlightjs 主題,實作上則參考了 GitHub VSCode 的主題

GitHub highlight.js Theme

後續計劃

蛤不是改版改一改就好了哦?雖然整個網站大致已完成,但還是少了些東西,像是:

  • 站內搜尋
  • 舊站轉移

也只能慢慢做啦!一如往常。

本篇文章驕傲的使用 HackMD 發佈