[開發手記] 從 WordPress 出走:用 Astro × Cloudflare 打造近零費用的全端部落格

2,017
字數
9
分鐘

自建部落格,最怕兩件事:主機費每月咬一口,以及框架越用越重,改個版型就像在泥沼裡打滾。這兩個痛點,我們在 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 FunctionsSSR 動態路由 / API每天 10 萬次請求
D1SQLite 資料庫(留言、管理)每天 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 認證,不依賴任何第三方 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> 元件會自動加上 widthheight 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

Share your thoughts

No sparks yet. Waiting for your first word...

Scroll down to load more comments...