[開發手記] 從 WordPress 出走:用 Astro × Cloudflare 打造近零費用的全端部落格
自建部落格,最怕兩件事:主機費每月咬一口,以及框架越用越重,改個版型就像在泥沼裡打滾。這兩個痛點,我們在 WordPress 時代都完整體驗過了。
首陽問路這個部落格,就是我們重新來過的成果。這篇文章介紹整個技術棧——為什麼選這套組合、每個零件怎麼搭配、踩了哪些坑,以及最後我們對自己的選擇滿不滿意。
技術選型:為什麼是 Astro + Cloudflare?
過去我們使用自建的 WordPress,它是世界上 CMS 排名的前段班,功能、套件也非常豐富,後台操作好上手,但營運久了覺得它比較肥大,外掛相依性複雜、更新風險高、維護成本也不低,對一個以寫作為主的部落格來說,有點殺雞用牛刀的感覺。
更關鍵的是,我們習慣用 Markdown 寫文章。Markdown 的好處是顯而易見的:純文字格式、不綁定任何平台,搭配 Git 就能做版本控制,文章的修改歷程一目了然;寫作時不用在 GUI 編輯器裡點來點去,專注在內容本身;搬家或換框架時,所有文章直接帶走,沒有資料被鎖在資料庫裡的煩惱。Astro 對 Markdown / MDX 的支援是一等公民待遇,這也是我們最終選擇它的重要原因之一。
因此先快速介紹一下我們的核心需求:
- 以靜態內容為主(文章、列表頁),但局部需要動態功能(留言、後台管理)
- 希望維運成本趨近於零——不想每個月對著主機費用發愁
- 需要支援繁中 / 英文雙語
- 部署流程要自動化,推上 Git 就完事
帶著這幾個條件去評估,我們最後選定了 Astro + Cloudflare 這個組合。
Astro:靜態優先,動態隨需
Astro 是一個「Islands Architecture」框架,核心理念是預設零 JavaScript——所有頁面預先渲染成靜態 HTML,只在需要互動的元件上才注入 JS。這對部落格來說簡直量身打造:文章頁面全靜態、速度飛快,只有後台管理和留言功能才需要動態渲染。
它同時支援 SSG(靜態生成) 和 SSR(伺服器渲染),可以在同一個專案裡混用。部落格文章設定 prerender = true,API 路由設定為動態,非常靈活。
Cloudflare 全家桶:一站搞定部署到資料庫
我們把所有雲端基礎設施都押在 Cloudflare 上,主要原因有兩個:免費額度真的夠用,以及與 Astro 的整合無縫。
整個架構長這樣:
%%{init: {'theme': 'base', 'themeVariables': {
'primaryColor': '#fdf8f0',
'primaryTextColor': '#111827',
'primaryBorderColor': '#d4c4a8',
'lineColor': '#8b6355',
'secondaryColor': '#f5f0e8',
'clusterBkg': '#f5f0e8',
'clusterBorder': '#8b6355',
'labelBkg': 'transparent'
}}}%%
graph TD
MD(["📝 Markdown 文章<br>Obsidian 管理"])
User(["🌐 使用者瀏覽器"])
subgraph CF["☁️ Cloudflare Pages"]
Pages["📄 靜態 HTML / CDN"]
Functions["⚡ Pages Functions<br>SSR + API 路由"]
end
D1[("🗄️ D1<br>SQLite")]
R2["🪣 R2<br>圖片 / 媒體"]
MD -->|"Astro 建置"| Pages
User -->|"靜態請求"| Pages
User -->|"動態請求"| Functions
User -->|"媒體載入"| R2
Functions -->|"讀寫資料"| D1
classDef user fill:#fdf8f0,stroke:#c4927a,color:#111827,font-weight:bold
classDef content fill:#f0f5f1,stroke:#8fa68c,color:#111827,font-weight:bold
classDef db fill:#fffbeb,stroke:#f59e0b,color:#111827
classDef storage fill:#fdf8f0,stroke:#d4c4a8,color:#374151
class User user
class MD content
class D1 db
class R2 storage
整個架構用到的 Cloudflare 服務:
| 服務 | 用途 | 免費額度 |
|---|---|---|
| Pages | 靜態網站托管 + CI/CD | 每月 500 次 Build、無限請求 |
| Pages Functions | SSR 動態路由 / API | 每天 10 萬次請求 |
| D1 | SQLite 資料庫(留言、管理) | 每天 500 萬次讀 / 10 萬次寫 |
| R2 | 圖片與媒體資產儲存 | 每月 10 GB、流出零費用 |
對於一個中小型部落格來說,以上這些免費額度根本用不完,省下了大筆主機費。如果對這些服務的細節和免費額度有興趣,藥藥之前有寫過一篇完整介紹:Cloudflare 超佛心上雲方案推薦,可以搭配閱讀。
架構深入解析
靜動分離:SSG 與 SSR 的混用之道
src/
├── content/ # Markdown 文章(Astro Content Collections)
├── pages/ # 路由頁面(.astro)
│ ├── api/ # 動態 API 路由(Pages Functions)
│ └── [admin]/ # 後台管理頁面(限制存取)
├── components/ # UI 元件
└── layouts/ # 頁面版型
靜態頁面(文章、列表)透過 export const prerender = true 在 Build 時預渲染成 HTML,由 Pages CDN 直接提供,速度極快。API 路由則跑在 Pages Functions 上,處理留言 CRUD 和後台認證等動態邏輯。
雙語 i18n:中文預設,英文加前綴
部落格支援繁體中文(預設)和英文,路由設計如下:
- 繁體中文:
firstsun.org/blog/slug(無語系前綴) - 英文:
firstsun.org/en/blog/slug(加/en/前綴)
這是透過 Astro 的 i18n 設定實現的:
// astro.config.ts
i18n: {
defaultLocale: "zh-tw",
locales: ["zh-tw", "en"],
routing: {
prefixDefaultLocale: false // 預設語系不加前綴
}
}
小提醒:靜態預渲染頁面(
prerender = true)裡無法使用Astro.currentLocale,因為 Build 時無法得知請求語系。需要偵測語系時,改用window.location.pathname.startsWith('/en/')做客戶端判斷。
D1 資料庫:邊緣運算的 SQLite
Cloudflare D1 是跑在 Pages Functions 邊緣節點上的 SQLite,讀寫都直接在離用戶最近的節點完成,延遲極低。在 Astro 的 API 路由裡,透過 context.locals.runtime.env 取得 D1 的 binding:
// src/pages/api/comments.ts
export const POST: APIRoute = async (context) => {
const db = context.locals.runtime.env.DB;
const result = await db.prepare(
"INSERT INTO comments (post_slug, content, author) VALUES (?, ?, ?)"
).bind(slug, content, author).run();
// ...
};
本地開發需要使用 wrangler dev 才能取得 D1 binding,一般的 pnpm dev 無法連接 D1,這是剛開始最容易踩到的坑。
R2 媒體儲存:Egress Free,流量不再是包袱
所有的圖片和媒體資產都放在 R2,透過自訂網域 blog-assets.firstsun.org 對外服務。R2 最大的優勢就是不收流出頻寬費(Egress Free),比起 AWS S3 省下不少成本。
認證系統:簡單乾淨的 Cookie 簽名
後台管理採用簽名 Cookie 認證,不依賴任何第三方 Auth 服務。登入後由 Pages Functions 發放已簽名的 Cookie,每次 API 請求在伺服器端驗簽即可。架構刻意保持簡單,後台只有自己用,不需要過度設計。
CI/CD:推上 GitLab,剩下的交給機器
程式碼托管在 GitLab,部署流程完全跑在 GitLab CI 上,而不是直接使用 Cloudflare Pages 內建的自動部署。原因很簡單:我們需要在部署前跑完整的功能測試,確保沒有壞掉才送上去。
# .gitlab/ci/build.yml(簡化版)
stages:
- check
- test
- deploy
check:
script:
- pnpm run check # TypeScript 型別檢查
- pnpm lint # ESLint
test:
script:
- pnpm test:e2e # Playwright E2E 功能測試
deploy:
script:
- pnpm run build
- wrangler pages deploy dist/
rules:
- if: $CI_COMMIT_BRANCH == "main"
每次推送到 main branch,Pipeline 會依序執行型別檢查、Lint、E2E 功能測試,全部通過才會觸發部署。這樣能確保上線的每一個版本都是可用的,不用擔心手滑推壞。
踩坑紀錄:這些坑藥藥都替你踩過了
1. prerender 頁面拿不到 currentLocale
如前面提到的,靜態預渲染頁面在 Build 時 Astro.currentLocale 永遠是預設語系 zh-tw,導致雙語靜態頁面的語系偵測失靈。解法是把語系判斷移到客戶端 JS,改用 window.location.pathname 偵測。
2. 本地開發無法用 D1
pnpm dev(Vite Dev Server)不能取得 Cloudflare 的 runtime binding,D1 / R2 都讀不到。需要跑 wrangler dev 才能完整模擬 Pages Functions 執行環境,兩個指令的開發體驗差很多,要注意切換。
3. <Image> 的尺寸破壞 CSS aspect-ratio
Astro 的 <Image> 元件會自動加上 width 和 height attribute,這會蓋掉 CSS 設定的 aspect-ratio,在圖片容器裡跑版。解法是加 scoped 的 CSS 覆寫:
:global(.hero-img) {
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
object-fit: cover;
}
結論
Astro + Cloudflare 這個組合,對「以靜態內容為主、局部動態功能」的部落格場景來說,幾乎是教科書級別的選型:零冷啟動的靜態頁面、幾乎免費的邊緣運算、開箱即用的 CI/CD,維運門檻低,還能隨流量自動擴展,不用擔心突然爆紅撐不住。
當然,這套架構也不是沒有代價——本地開發環境的設定比一般 Node.js 專案複雜(要用 wrangler),雙語路由和 prerender 的眉角也需要多花時間摸清楚。但整體來說,我們對這個技術選擇非常滿意。
如果你也在考慮打造自己的部落格,希望這篇文章的實作筆記能給你一些參考方向!有任何問題歡迎留言,首陽問路關心你的每一次技術抉擇 ☀️
參考資料
站內延伸閱讀
官方文件
Conversation
No sparks yet. Waiting for your first word...