--- image: https://hackmd.io/_uploads/ryr7ih0_u.png --- # 把 HackMD 當做部落格後臺 feat. Next.js :::success 沒什麼主題能寫的時候,更新一下部落格版型準沒錯 > by [我,2017](https://yukai.dev/blog/2017/03/29/new-primer-css-github-theme) ::: 就是這句話讓我現在又來發文 XD。 時間過得飛快,距離上次更新也過了三年多,但部落格文章也沒多寫幾篇。老實說也不是沒有寫,但不是放在私人的 Evernote,就是散落在 [HackMD](https://hackmd.io/@yukai) 上的程式碼片段和指令操作記錄,要直接貼出來還是有些羞恥的。 去年十月,突然覺得[舊部落格](https://yukaii.tw)版型實在用太久了,速度也有點慢、設計有點過時。發新文章時,還得在 HackMD 上寫完文章、在 GitHub Repo 建新檔案、再貼內容過去。若能打造一個更流暢愉快的工作流程,那我的文章產量一定能更高啊!~~(不要瞎掰好嗎)~~ ## 改版計劃 這次的部落格改版有幾個需求: 1. **速度**。原本舊的部落格是用 [Jekyll 架設](https://github.com/Yukaii/yukaii.github.io/),純靜態的網頁,換頁也使用了 [Turbolinks](https://github.com/turbolinks/turbolinks) 這個 Pjax 技術加速,無論是載入時間以及換頁都十分順暢。新的部落格系統希望能達到同樣的速度,甚至在這之上。 2. **構建的易用性**。原本的部落格使用原生的 GitHub Pages,另外拆出成 [octoflavor](https://github.com/Yukaii/octoflavor) Jekyll 主題。解釋一下「原生」和「非原生」GitHub Pages 的差別: - **原生**的 GitHub Pages 就是 GitHub 幫你跑 Jekyll build,你只要推上 markdown 就好了,也不用綁什麼 CI 服務 - **非原生**的 GitHub Pages,就是把網頁的原始碼上傳到 `gh-pages` 分支,剩下來的 GitHub 幫你搞定。 4. **Node.js 派**。我只想 npm 裝好裝滿。 5. **良好的開發體驗**。如何方便的幫部落格擴充也是重要的一環。Jekyll 固然不錯,但是老舊的 Liquid 樣板語法,和沒有內建現代前端套件管理就輸了一大截。 6. **在 HackMD 上寫作**。業配 GOOD! 至於我要如何達成這幾項需求呢?先讓我們進入業配的環節吧 🤩 ## HackMD 的團隊文章列表 API ![HackMD Public Team - DailyOops](https://i.imgur.com/QjF4EIL.png "HackMD 團隊 - Daily Oops!,存放文章的地方") HackMD 有一個非公開的 API:`GET https://hackmd.io/api/@TEAM_PATH/overview`,回傳公開頁下所有能見的筆記。以我的部落格的團隊 [@DailyOops](https://hackmd.io/@DailyOops) 為例: ```javascript GET https://hackmd.io/api/@dailyOops/overview { "team": ..., "notes": [ { "id": ...., "content": "...", "title": "...", ... } ] } ``` 有了這些資料,就可以繼續我的部落格改版計劃啦! ## 隆重介紹:Daily Oops 2.0 feat. Next.js 我選擇 [Next.js](https://nextjs.org/) 作為這次部落格改版的主要技術,最大的原因就是雖然它是 React.js 框架,但內建「靜態網站產生」功能 (簡稱 **SSG**, Static-site generation)。 SSG 保有動態資料的彈性,以及靜態網站的簡潔與速度。你可以透過 API 取得的動態資料,來產生多個靜態頁面,而且是「預先渲染」好的靜態 HTML 網頁,不會有 Cilent side React,載入後才渲染的現象。在頁面切換時又能保有 React SPA 的順暢換頁,我全都要! 對於部落格這種更新頻率相對不用即時,靜態頁面居多的場景,Next.js 可說是相當適合。會採用 Next.js 的另一個原因,是當時看到了 [notion-blog](https://github.com/ijjk/notion-blog) 這個專案,在 Notion 還沒有公開 API 的當下反解出 API,把 Notion 當做 CMS 來用,同樣搭配的是 Next.js 的 SSG 功能。 ![](https://hackmd.io/_uploads/HytiiZnu_.png) 上圖就是新部落格的資料流: 1. 從 HackMD 抓取文章資料 2. 交由 Next.js 建置 3. 完成靜態網站建置 程式碼放在 [GitHub](https://github.com/Yukaii/daily-oops),接下來給各位帶來幾個值得一提的細節。 ## `getStaticPaths` 和 `getStaticProps` 的資料不互通 `getStaticPaths` 能拿到「靜態網站頁面列表」,`getStaticProps` 則是「靜態網站頁面資料」,但列表僅僅是列表。感覺還是有點抽象,我寫段簡化版的實作: ```typescript 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 取得文章資料了。 我心目中理想的資料架構應該是這樣: ```javascript 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](https://github.com/vercel/next.js/discussions/11272) 有相當多的討論,目前 upvote 最高的解答是自己在硬碟上快取一份資料,在 `getStaticProps` 時透過快取過的 API 去抓資料。我也自己實作了一份: [`lib/post.js`](https://github.com/Yukaii/daily-oops/blob/main/lib/post.js) ## 設計:Primer 設計系統 | ![](https://hackmd.io/_uploads/BJDq5jCdO.jpg) | ![](https://hackmd.io/_uploads/Bys9qi0u_.jpg) | | --------------------------------------------- | --------------------------------------------- | | ![](https://hackmd.io/_uploads/B1WOqs0O_.png) | ![](https://hackmd.io/_uploads/HJSFqsA_O.png) | 點擊圖片可看大圖。 在設計方面,目前還是個工程師的我,這次一樣採用了 GitHub 的設計系統 Primer。Primer 近年來進展迅速,還多了暗色主題的設計,[色彩系統](https://primer.style/css/support/color-system) 開箱即用,不用自己慢慢調色。 ![](https://hackmd.io/_uploads/rkTiioAOd.png "Primer 的 Color System,直接套 class 就有暗色效果啦") ## Site Logo ![](https://hackmd.io/_uploads/H1iqAs0_O.gif "這是一隻小貓熊,他很可愛,如果你沒看過,現在你看過了") Logo 也是自己畫的,Google 了一張參考圖就直上了,反正小貓熊哪張圖片都很可愛,對吧(啾 ![](https://hackmd.io/_uploads/SyWH13Cdu.png "附上 Figma 工作區") ![](https://hackmd.io/_uploads/rkwIJhC_O.png "Gif 圖也是用 Figma Plugin - GiffyCanvas 做的") ## Markdown 渲染 和 HackMD/CodiMD 一樣,繼續採用高度可擴充、社群套件豐富的 [markdown-it](https://github.com/markdown-it/markdown-it)。有興趣的朋友可以參考 [`lib/markdown.js`](https://github.com/Yukaii/daily-oops/blob/main/lib/markdown.js)。 值得炫耀的是程式碼區塊的渲染整個從零開始,高仿 GitHub 採用 Table 排版,除了有複製到剪貼簿,還有==行號標色==,效果如下: | ![](https://hackmd.io/_uploads/HJ0he3ROd.png) | ![](https://hackmd.io/_uploads/HkVTe2R_u.png) | | --------------------------------------------- | --------------------------------------------- | | | | 為了讓程式碼區塊的標色和 GitHub 完全一致,還根據 Primer 顏色系統產生了 [GitHub 用的 Highlightjs 主題](https://github.com/Yukaii/github-highlightjs-themes),實作上則參考了 [GitHub VSCode 的主題](https://github.com/primer/github-vscode-theme)。 ![](https://hackmd.io/_uploads/BkarZ20O_.png "GitHub highlight.js Theme") ## 後續計劃 蛤不是改版改一改就好了哦?雖然整個網站大致已完成,但還是[少了些東西](https://github.com/Yukaii/daily-oops/issues),像是: - 站內搜尋 - 舊站轉移 也只能慢慢做啦!一如往常。