開始架 blog 之前,我花了一點時間選框架。不是因為選擇困難,而是選錯了之後換掉的代價太高——slug 一旦上線就不能改(= URL 不能變),框架綁定的風格也很難中途轉換。
我之前用過 Hexo。它很好上手,台灣開發者圈幾乎人手一個 Hexo blog,theme 多、文件多、踩坑文多。但用久了開始覺得卡:想改一個細節要鑽進 theme 的 EJS template,想加一個功能要找對應的 plugin 是否還有人維護。2023 年後整個生態明顯停滯,我決定這次重新選。
現代 Blog 框架比較
主流選項大概就這幾個:
| 框架 | 語言/生態 | 預設 JS 輸出 | Markdown | GitHub Pages |
|---|---|---|---|---|
| Astro | JS/TS,任意 UI | Zero JS(island) | 原生 + MDX | ✅ CI/CD |
| Hugo | Go,無框架 | 零 | 原生,shortcode | ✅ 原生支援 |
| Jekyll | Ruby | 零 | 原生 | ✅ 原生免 CI |
| Eleventy | JS,無框架 | 零(手動加) | 原生 + Nunjucks | ✅ CI/CD |
| VitePress | Vue | Vue runtime | 原生(文件導向) | ✅ CI/CD |
| Next.js | React | React bundle | 需套件 | ✅ CI/CD |
| Hexo | Node.js | 零 | 原生 | ✅ 原生支援 |
幾個快速刷掉的理由:
- Hexo:曾經是最多台灣開發者的第一選擇(包括我),但 2023 年後活躍度明顯下降,theme 生態停滯,想客製化幾乎要自己翻 EJS template。如果你有 Hexo blog,這篇文章的讀者就是你。
- Jekyll:GitHub Pages 原生支援是好事,但 Ruby 環境在 Mac 上一直有點麻煩,社群動能也在走下坡。
- Hugo:Build 速度是所有框架裡最快的(毫秒級),但 Go template 語法不直覺,想客製化 UI 要多學一套東西。
- Next.js:太重。Blog 不需要 SSR、不需要 API routes,為什麼要帶一個 React runtime?
最後剩下 Astro 和 Eleventy。Eleventy 很乾淨,但它對 UI 框架沒有意見——彈性太高反而要自己做很多決定。Astro 有主張,而且主張跟 blog 場景剛好對齊。
Astro 是什麼
Astro 是一個以「內容優先」為核心設計的現代靜態網站框架,開源在 GitHub,目前有超過 48k stars。npm 每週下載量超過 50 萬次,被 Google、Cloudflare、NordVPN 等公司的文件或行銷網站採用。
它在 2021 年發布 beta,2022 年出 v1,成長速度非常快。State of JS 2023 調查裡,Astro 的開發者滿意度排名第一——超過 Next.js、Remix、SvelteKit。
不是小眾實驗品,是已經被驗證的選擇。
核心設計哲學只有一句話:ship less JavaScript。具體怎麼做到的,就是孤島架構。
孤島架構(Island Architecture)
Astro 最核心的設計哲學:頁面預設是靜態 HTML,只有你標記 client:* 的元件才會下載 JS、才會 hydrate 成可互動的狀態。
<!-- 這個完全不輸出 JS --><CommentSection />
<!-- 這個才會 --><CommentSection client:load />
<!-- 瀏覽器閒置才載入 --><CommentSection client:idle />
<!-- 進入 viewport 才載入(天生 lazy load) --><CommentSection client:visible />client:visible 這個特別有意思——你不用自己寫 IntersectionObserver,框架幫你搞定。元件只有在使用者真的看到它的時候才會被下載。
對比 Next.js:每個頁面預設就帶 React runtime,就算頁面全靜態也一樣。你要手動 'use client' / 'use server' 來標界線。
兩者的思路剛好相反:
- Next.js:預設有 JS,需要靜態才標記
- Astro:預設無 JS,需要互動才標記
Blog 是「內容為主、互動為輔」的場景。大部分頁面就是 HTML + CSS,偶爾才有個搜尋框或留言系統。Astro 的哲學天然吻合。
寫起來像 Vue
.astro 的語法熟悉又直覺。記住一件事就夠了:--- 是前後端的分水嶺。
---// ▲ SERVER(build time)// 可以讀檔、call API、存 API key// client 完全看不到這段const { title } = Astro.props;const posts = await getCollection('blog'); // 可以直接 await---
<!-- ▼ HTML template,不需要 <template> 包 --><!-- 幾乎就是 Vue 的寫法,少一層 --><h1>{title}</h1>{posts.map(p => ( <a href={`/posts/${p.id}`}>{p.data.title}</a>))}
<style> h1 { color: var(--text); } /* 預設 scoped */</style>
<script> // ▼ CLIENT(browser) // 可以操作 DOM、監聽事件 // 但看不到上面 --- 的變數 console.log('hello from browser');</script>兩個跟 Vue 不一樣的地方值得說一下:frontmatter 可以直接 await,不需要 onMounted 或 useEffect;而且整個框架沒有 reactivity——沒有 ref、沒有 reactive、沒有 watch。對 blog 來說這不是缺點,是少了一層複雜度。
Props 怎麼傳
Component 化之後,資料從呼叫端傳進來,一樣在 frontmatter 接:
---// 呼叫端:<AuthorCard name="Jasper" avatar="./photo.jpg" />
interface Props { name: string; avatar?: string;}
const { name, avatar = '/default.jpg' } = Astro.props;---
<div> <img src={avatar} alt={name} /> <p>{name}</p></div>沒有 defineProps、沒有 withDefaults,直接解構、直接用。
Build time Astro 把 props 填進 template,輸出靜態 HTML。瀏覽器收到的已經是填好值的頁面——不需要 server、不需要 runtime,文章標題、作者名、圖片路徑在 build 當下就全部燒進去了。
CSS Scoping
<style> 預設 scoped,不用加任何 attribute:
<h1>Hello</h1>
<style> /* 只影響這個元件的 h1,不會外洩 */ h1 { color: red; }</style>Build 後 Astro 自動加 hash:
<h1 class="astro-abc123">Hello</h1><style>h1.astro-abc123 { color: red; }</style>要跳脫 scope 影響子元件用 :global():
<style> /* Markdown render 出來的 HTML 在 slot 裡,不在 scope 內 */ /* 所以要用 :global() 才能樣式化 <h1>、<blockquote> 這些 */ .article-body :global(h1) { font-size: 2rem; } .article-body :global(blockquote) { background: var(--surface); }</style>這個設計讓大型 component 樹不會有樣式污染問題,同時保留了「真的需要全域時」的逃生口。
一句話結論
如果你寫過 Vue、想架一個內容導向的 blog、不想為了偶爾一個互動元件而背負整個 React runtime——Astro 是目前最對的選擇。
它有主張,但主張剛好是對的。