遷移 Blog 到 Astro

日期:
分類: Changelog 頁面仔的自我修養
標籤: Astro

為什麼要遷移

我一直在使用 Hexo 作為我的部落格框架。但是 Hexo 有一些問題:

  • Hexo 的外掛生態不夠完善,很多外掛都是半死不活的狀態。
  • 我用的 NeXT 主題中動畫是採用 JavaScript 實現的,現在來說 CSS 已經足夠。2023 年了,一篇(沒有什麼水平的)文章需要什麼 Javascipt?
  • Hexo 稱不上是一個現代的 SSG 框架。比如它沒有 bundler,即使只用了幾個 Font Awesome 圖示,也要載入整個 stylesheet。
  • 2023 年了,Hexo 還是沒有熱過載,每次修改完文章都要重新整理頁面,這個體驗真的很差。

上面這些原因都是小問題。最大的問題在於 Hexo 很難注入自定義程式碼。比如我想寫一個自定義元件,用來顯示我的滿文文章,我必須把文章的內容包在兩層巢狀的 <div> 裡面,才能讓文字方向正確。這樣做的問題是什麼呢?

首先,這種手寫複雜 HTML 的做法與 Markdown 標記文字是不統一的。如果我想要在上述的滿文文章中插入一個連結,我必須在 Markdown 中寫 <a> 標籤,而不是寫 Markdown 的連結語法。雖然 Markdown 是一個旨在渲染到 HTML 的標記語言,但憑什麼我要在 Markdown 中費力地縫合 HTML 呢?

其次,這樣做不符合語義化,而且可維護性很差。如果我想要修改這個自定義元件的樣式(比如,一開始我只用了一層 div),我很可能要同時修改所有 Markdown 中巢狀的兩層 HTML,這樣很容易出錯。

我不想一下子就把所有的文章都遷移到 Astro,所以我決定漸進式遷移。所以起初我只將部分新的 Markdown 不足以完成需求的文章用 Astro 寫,而且採用了一種很扭曲的方法:先渲染一次 Hexo,然後再將 Astro 渲染為 HTML,補上 frontmatter,將輸出的檔案複製到 Hexo 的 source/_posts 目錄下當作 Markdown 檔案,再經 Hexo 渲染為 HTML。不過,這樣雖然能採用自定義的元件,但是構建時間長達十幾秒。

幾個月後,我終於無法忍受,將所有的文章都遷移到了 Astro。不過,這的確相當於自己寫一個網站了,非常耗時間,得全部寫好才能釋出。

那麼,在實際使用了幾個月後,下面就是我對 Astro 的特性的一些評價。

Astro 的語法

Astro 檔案的標記語言跟 JSX 類似,但本質上區別很大。JSX 可以寫在 JS 中,但 Astro 檔案程式碼和頁面結構分明,程式碼寫在頂部的兩行 --- 之間,這部分的程式碼會在服務端渲染時執行。如果要在客戶端執行,則需用 <script> 標籤包裹,這被稱為 client script。

Astro 並沒有一套狀態管理方法。繫結的變數值只在渲染階段可變,一旦進入 HTML 就不可變了。這是因為 Astro 的 HTML 元素屬性會被轉成字串。這樣一來就也沒有能繫結 onclick 之類事件的語法糖,你只能加一個 clilent script,然後在裡面 addEventListener()。比較巧妙的一點是同一個元件在一個頁面上多次出現,其 clilent script 也只會執行一次,所以對於大部分場景,你也不需要拿到當前元件的引用。

至於類似 JSX,if 只能使用三目運算子,我是覺得很噁心。好在 Astro 有豐富的 integration 生態和脫水機制,你可以選一個你喜歡的框架來寫,比如 Svelte。

不過,Astro 自己語法的 compiler 有點問題,<= 被 tokenize 成了 <Fragment> 標記。

再加一層括號可以避免這個問題,但括號會被格式話掉,只能把兩端反過來寫 >=。六月的 issue,今天還沒修好……

Astro 的元件

是的,除了 Astro 的內建元件,你可以自由地使用 Svelte、React、Vue、Solid、Qwik、Lit、Preact、Alpine 的元件作為框架!若要跨框架放置內容,也可透過 <slot> 來傳遞。既然是靜態站點,那麼選擇不自己再造一套狀態管理的輪子這種務實的做法很讓我喜歡。

更重要的是 MDX 的 integration,即可以在 Markdown 中匯入以上這些元件,和文字混排。MDX 和 remark / rehype 元件,作為文件型站點的核心,是受到 Astro 官方的支援的,有 e2e test 保證可靠性。MDX 雖然是 HTML 語法,但的確算是很符合我需求的(真正的)模板系統了。行內元件,只要你不寫,就不會有多餘的空格;塊級的元件在起始標記之後和閉合標記之前各空一行,即可又在其中巢狀 Markdown。雖然 import 語句之類的缺少語法高亮和編輯器補全,但相比於能實現相同功能的 MediaWiki 那一套,我覺得已經足夠簡單易用了。

路由

Astro 稱自己的路由為「檔案路由」,即每個檔案都對應一個路由。這樣做當然很直觀,但是對我來說就很痛苦了。Astro 的示例中的部落格文章統一放在 /posts 下,但是我的文章頁面是直接放在根目錄的。我不想把所有的文章都放在 /posts 下,因為這樣會導致我的文章連結發生變化,而且發在其他地方也不直觀。

Astro 2.0 支援了 content collection,目的是將渲染時不同型別的文章頁面分類,這在一定程度上解決了我的問題。我只要建立一個 [...slug].astro,就可以接管 pages 內不匹配其他任何檔案的路徑了。這跟 SvelteKit 很像。Astro 由於要 SSG,還需要自己寫一個函式提供所有可能的 slug 才能在構建時生成所有的 HTML 檔案。

不過這個檔案路由也是問題重重。pagescontent 中所有的 html、astro、md、mdx 檔案都會被嘗試渲染。如果你想放一個其他型別的檔案,Astro 會有不支援的檔案型別的警告,需要在前面加 _ 阻止其輸出。但是我在 content collection 遇到了問題,我提了一個 issue

Files with the _ prefix won’t be excluded and appear in the result of getCollection. However, accessing them will cause an error.

Create a _folder in a collection with markdown files. The docs say “Files with the _ prefix won’t be recognized by the router and won’t be placed into the dist/ directory”. If folders are expected to not exclude files, then accessing both of them raises an error. If folders are expected to work with underscores, then the two posts show up in the result of getCollection.

demo
demo

https://stackblitz.com/edit/github-lhq8uj

不過這個問題很快就 被修 了,是一個 maintainer 自己改壞了。嘗試 debug 的時候還 debug 到了 content collection 的 typegen 部分,雖然找到了問題的根源但是也沒看懂。

diff --git a/packages/astro/src/content/vite-plugin-content-virtual-mod.ts b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts
index 62ba612e962e..021a26314b72 100644
--- a/packages/astro/src/content/vite-plugin-content-virtual-mod.ts
+++ b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts
@@ -213,7 +213,7 @@ function globWithUnderscoresIgnored(relContentDir: string, exts: string[]): stri
 	const contentDir = appendForwardSlash(relContentDir);
 	return [
 		`${contentDir}**/*${extGlob}`,
-		`!${contentDir}_*/**${extGlob}`,
-		`!${contentDir}_*${extGlob}`,
+		`!${contentDir}**/_*/**${extGlob}`,
+		`!${contentDir}**/_*${extGlob}`,
 	];
 }

圖片最佳化

今天釋出的 Astro 3.0 提供了一個 <Image> 元件,採用 Sharp 最佳化圖片。

在 Astro 3.0 前,圖片若放在 Markdown 檔案同目錄下,則需要用 ESM import 語法匯入並且使用 <Image> 元件,無法使用 Markdown 的語法。所以我把它們都放在了 public 資料夾中,路徑與文章相對 website root 的絕對路徑一致。這樣,我在 src/content/blog/some-article.md(渲染後的路徑為 /some-article.md)中寫 ![](./some-image.jpg) 就可以使用 public/some-article/some-image.jpg 了。在 3.0 後,![](./some-image.jpg) 會匯入 src/content/blog/some-image.jpg。所以我把文章放到了 src/content/blog/some-article/index.md,圖片放在 src/content/blog/some-article/some-image.jpg

在 Astro 2.0 加入了 content collection 後,我們可以寫一個 config.ts 來用 Zod 宣告所有集合的 frontmatter 型別。這確實是一個很好的功能,方便你在生成文章列表的場合不會出問題。Astro 還提供了 frontmatter 的 Zod 型別,看上去不錯, 對吧?

所以,我想在 frontmatter 中使用 image() 來提升首頁的載入速度。一切都很順利,直到我修改到 Open Graph 的部分。

在使用 astro:assets 之前,我會指定遠端資源的URL。但是在 Astro 中,無論如何都無法載入遠端圖片,可能是由於代理設定的原因。Image 物件裡只有類似 "@fs/E:/blog" 的路徑,也沒辦法裁剪路徑,因為輸出的影象名稱中包含雜湊值。而且大部分的連結預覽(比如 Telegram)都只支援 jpg / png / gif,不支援 webp 等,需要確保提供的圖片的格式。

我將這些圖片移動到src目錄中後,發現只能使用 <Image> 元件來顯示它們。當我嘗試使用 getImage() 時,會丟擲一個錯誤:

`Expected 'src' property to be either an ESM imported image or a string with the path of a remote image. Received 'undefined'.`

Astro 文件中說,如果你需要一個 asset 的直鏈,那還是使用 public 資料夾吧。1

我甚至還嘗試在 Zod 中使用 z.union([z.string(), image()]),但是這樣 Typescript 也沒有辦法正確地進行 type narrowing,型別還是不可用。看來不能兩全其美了。

RSS 輸出

最痛苦的一點是,Astro 雖然提供了 RSS 的 integration,但是想要輸出全文內容居然要自己找一個 Markdown renderer 來渲染 Markdown。

如果是一個 .astro 檔案,我可以透過在元件內部 import 一個 Astro 元件並用 Astro.slots.render() 獲取其 slot 渲染後的 HTML,但是,在 atom.xml.ts 裡的 API endpoint 中就不好辦了。雖然我的 Astro 元件基本都是 noscript 的,但 Astro 中渲染 Markdown 實際上也是在渲染一個 component,需要保證安全性,所以沒有實現。2所以現在本部落格的 RSS 輸出含 MDX 程式碼。

總結

Astro 的 SSG 和脫水機制的確很符合我的想法的。儘管有以上問題,Astro 作為一個 SSG first 框架,可能是 2023 年 blog 這種靜態站點的最佳實踐了。Blog 可以體現一個頁面仔對於前端技術的一切追求,沒有自己寫一個 blog 的頁面仔不是一個好的頁面仔。以上。

---

  1. Images 🚀 Astro Documentation # Where to store images↩

  2. 相關的 issue:Container API: render components in isolation #533↩

本作品採用 知識共享署名-非商業性使用 4.0 國際許可協議CC BY-NC 4.0 International) 進行許可。閣下可自由地共享(複製、發行)和演繹(修改、轉換或二次創作)本作品,唯須遵守許可協議條款。

評論

評論將在稽覈後顯示,閣下可以在本部落格的 Github 倉庫的 拉取請求列表 中檢視。提交成功後會自動跳轉。

本站不支持 Dark Reader 的暗色模式,请对本站关闭后再访问。
(亮色模式的对比度、亮度等选项不受影响)