把 HackMD 當做部落格後臺 feat. Next.js
沒什麼主題能寫的時候,更新一下部落格版型準沒錯
by 我,2017
就是這句話讓我現在又來發文 XD。
時間過得飛快,距離上次更新也過了三年多,但部落格文章也沒多寫幾篇。老實說也不是沒有寫,但不是放在私人的 Evernote,就是散落在 HackMD 上的程式碼片段和指令操作記錄,要直接貼出來還是有些羞恥的。
去年十月,突然覺得舊部落格版型實在用太久了,速度也有點慢、設計有點過時。發新文章時,還得在 HackMD 上寫完文章、在 GitHub Repo 建新檔案、再貼內容過去。若能打造一個更流暢愉快的工作流程,那我的文章產量一定能更高啊!(不要瞎掰好嗎)
改版計劃
這次的部落格改版有幾個需求:
- 速度。原本舊的部落格是用 Jekyll 架設,純靜態的網頁,換頁也使用了 Turbolinks 這個 Pjax 技術加速,無論是載入時間以及換頁都十分順暢。新的部落格系統希望能達到同樣的速度,甚至在這之上。
- 構建的易用性。原本的部落格使用原生的 GitHub Pages,另外拆出成 octoflavor Jekyll 主題。解釋一下「原生」和「非原生」GitHub Pages 的差別:
- 原生的 GitHub Pages 就是 GitHub 幫你跑 Jekyll build,你只要推上 markdown 就好了,也不用綁什麼 CI 服務
- 非原生的 GitHub Pages,就是把網頁的原始碼上傳到
gh-pages
分支,剩下來的 GitHub 幫你搞定。
- Node.js 派。我只想 npm 裝好裝滿。
- 良好的開發體驗。如何方便的幫部落格擴充也是重要的一環。Jekyll 固然不錯,但是老舊的 Liquid 樣板語法,和沒有內建現代前端套件管理就輸了一大截。
- 在 HackMD 上寫作。業配 GOOD!
至於我要如何達成這幾項需求呢?先讓我們進入業配的環節吧 🤩
HackMD 的團隊文章列表 API
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 功能。
上圖就是新部落格的資料流:
- 從 HackMD 抓取文章資料
- 交由 Next.js 建置
- 完成靜態網站建置
程式碼放在 GitHub,接下來給各位帶來幾個值得一提的細節。
getStaticPaths
和 getStaticProps
的資料不互通
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 近年來進展迅速,還多了暗色主題的設計,色彩系統 開箱即用,不用自己慢慢調色。
Site Logo
Logo 也是自己畫的,Google 了一張參考圖就直上了,反正小貓熊哪張圖片都很可愛,對吧(啾
Markdown 渲染
和 HackMD/CodiMD 一樣,繼續採用高度可擴充、社群套件豐富的 markdown-it。有興趣的朋友可以參考 lib/markdown.js
。
值得炫耀的是程式碼區塊的渲染整個從零開始,高仿 GitHub 採用 Table 排版,除了有複製到剪貼簿,還有行號標色,效果如下:
為了讓程式碼區塊的標色和 GitHub 完全一致,還根據 Primer 顏色系統產生了 GitHub 用的 Highlightjs 主題,實作上則參考了 GitHub VSCode 的主題。
後續計劃
蛤不是改版改一改就好了哦?雖然整個網站大致已完成,但還是少了些東西,像是:
- 站內搜尋
- 舊站轉移
也只能慢慢做啦!一如往常。