Skip to content

用 Cloudflare Workers(Static Assets)部署靜態網站實戰

用 Cloudflare Workers(Static Assets)部署靜態網站實戰

如果你跟以前的我一樣,做完一個小專案後,最頭痛的就是「然後呢?要怎麼讓全世界看到它?」這件事。架伺服器太重、買 VPS 要自己管 nginx、Vercel/Netlify 免費額度又怕哪天被砍。後來我把自己手上的工具站(複利計算機、QR Code 產生器、郵遞區號查詢、英語學習 App)全部搬到 Cloudflare Workers 的 Static Assets 模式,到現在沒再為部署煩惱過。

這篇就用我自己這幾個站「正在線上跑」的真實設定,帶你從零搞懂這套部署方式。

為什麼選 Workers Static Assets?

你可能聽過 Cloudflare Pages,它也能部署靜態站。但 Cloudflare 後來主推的方向是 Workers + Static Assets——你可以把它想成「Pages 的進化版」:一樣免費吃到飽地用 CDN 全球發送靜態檔,但多了一個超大優勢——未來想加後端邏輯(API、資料庫),不用搬家、不用換平台,直接在同一個 Worker 裡長出來就好。

對我這種「先做純靜態、之後再慢慢加功能」的人來說,這個「可以無痛長大」的特性實在太重要了。我的複利計算機現在是純靜態,但哪天想接 0050 報酬 API,設定檔改三行就能接上,不用整個專案重來。

最小可用設定:純靜態站

先看最單純的例子。我的「郵遞區號查詢」站是用 vite-ssg 預渲染的純靜態站,wrangler.jsonc 短到只有這樣:

jsonc
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "taiwan-zipcode",
  "compatibility_date": "2025-05-30",
  "assets": {
    "directory": "./dist",
    "not_found_handling": "404-page"
  }
}

關鍵就兩個欄位,我一個一個拆給你看:

  • name:你的 Worker 名字,會變成 taiwan-zipcode.你的帳號.workers.dev 的子網域。這個欄位非常重要,等等會踩到坑。
  • assets.directory:你的靜態檔放哪。我用 Vite,build 出來就在 ./dist,所以指這裡。注意這是相對於 wrangler.jsonc 所在資料夾的路徑,不是專案根目錄的絕對概念。
  • not_found_handling:找不到路徑時怎麼辦,這是整篇最值得講的東西,下面專門開一節。

注意這個設定沒有 main 欄位——這代表它是「assets-only Worker」,沒有任何 JS 進入點,純粹當靜態檔伺服器。Cloudflare 看到沒有 main、只有 assets,就知道你要的是純靜態服務。

那個會害你抓狂的 404:SPA vs 預渲染

not_found_handling 是我覺得最多人搞錯、也最影響 SEO 的設定。它有三個值,差別在「使用者打了一個不存在的網址時,伺服器回什麼」:

  • single-page-application:不管打什麼路徑,一律回 index.html 並帶 HTTP 200。這是 React Router、Vue Router 這種「由瀏覽器端接管路由」的 SPA 標準做法——讓 client 端的 router 自己決定要顯示哪頁。
  • 404-page:找不到就回傳你 dist/ 裡的 404.html帶真正的 HTTP 404
  • none:什麼都不做,回 Cloudflare 預設的純文字 404。

這裡有個容易被忽略的雷:如果你的站是預渲染(每條路由都有實體 .html),千萬別用 single-page-application。因為 SPA 模式會把「不存在的網址」也回 200,Google 看到一個明明是錯誤頁卻回 200 的頁面,會判定成 soft 404,對 SEO 很傷。

我的郵遞區號站註解就是這樣寫的:所有合法路由都已預渲染成實體檔,所以找不到的就該回 dist/404.html 並帶 HTTP 404(hard 404),讓 Google 乾淨地知道「這頁不存在」。那個 404.html 我是在 vite-ssg render 時把含 noindex 的 NotFound 頁搬出來的。

一句話結論:純 client-side render 的 SPA → 用 single-page-application;每頁都預渲染好實體 HTML → 用 404-page

處理尾斜線:html_handling

接著看 QR Code 站,它多了 html_handling

jsonc
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "qrcode-studio",
  "compatibility_date": "2026-06-01",
  "assets": {
    "directory": "./dist",
    "html_handling": "force-trailing-slash",
    "not_found_handling": "404-page"
  }
}

html_handling 決定網址跟實體檔怎麼對應、以及要不要統一尾斜線。常用的值:

  • force-trailing-slash:把 /wifi 用 308 轉址到 /wifi/
  • drop-trailing-slash:反過來,把 /fire/ 轉成 /fire,對應到 dist/fire.html(我的複利計算機就用這個)。

為什麼要管尾斜線?因為同一個內容如果 /wifi/wifi/ 都能開,就是兩個網址、會被當成重複內容。我的做法是讓站內 canonical、sitemap 全部統一成「帶尾斜線」,再用 force-trailing-slash 強制對外網址一致。一個內容只有一個正規網址,SEO 才乾淨。

綁定自訂網域

workers.dev 子網域拿來測試很方便,但正式上線當然要自己的網域。QR Code 站的做法:

jsonc
{
  "routes": [
    { "pattern": "qrcode-studio.pocketool.app", "custom_domain": true }
  ],
  "workers_dev": false
}

只要 pocketool.app 這個 zone 在你同一個 Cloudflare 帳號下,custom_domain: true自動幫你建好 DNS 記錄跟 SSL 憑證,你什麼都不用手動點。我第一次看到它自動長出 DNS 跟憑證時還愣了一下,真的就一行設定的事。

那個 workers_dev: false 是把 *.workers.dev 子網域關掉。為什麼?因為如果同一份內容同時掛在 qrcode-studio.workers.dev 和正式網域上,又是重複內容問題。關掉它,全世界就只有一個正版網址。(小提醒:DNS 還沒生效前想先測,可以暫時改 true。)

部署流程

設定好之後,部署就是兩步:

bash
# 1. 先 build 出靜態檔到 dist/
pnpm run build

# 2. 部署上去
npx wrangler deploy

第一次跑會跳出瀏覽器要你登入授權 Cloudflare 帳號,授權完就一路自動。之後每次更新內容,重跑這兩行就好,幾秒鐘全球生效。

有個我繳過學費的坑要特別提醒:name 一定要對齊你「現有那個 Worker」的名字。如果打錯,wrangler deploy 不會報錯,而是默默幫你新建一個 Worker——結果你的正式網域內容完全沒更新,你還在那邊 F5 想說怎麼沒變。部署前可以先用 npx wrangler deploy --dry-run 確認,或 npx wrangler whoami 後去 dashboard 對一下既有 Worker 名稱。

進階:靜態站長出後端(Static Assets + D1)

最後給你看「無痛長大」的威力。我的英語學習 App 在純靜態基礎上加了登入和資料庫:

jsonc
{
  "name": "english-speak-cards",
  "main": "worker/index.ts",
  "compatibility_date": "2026-06-01",
  "compatibility_flags": ["nodejs_compat"],
  "assets": {
    "directory": "./dist",
    "binding": "ASSETS"
  },
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "speak-cards-db",
      "database_id": "<你的-d1-database-id>",
      "migrations_dir": "migrations"
    }
  ]
}

跟純靜態站比,多了三件事:

  1. main:加上 Worker 進入點後,它就能處理動態請求(例如 /api/*)。靜態檔仍然會被優先服務,只有 assets 找不到的請求才落到你的 Worker 程式。
  2. assets.binding: "ASSETS":給靜態資產一個綁定名稱,這樣你在 Worker 程式裡可以用 env.ASSETS.fetch(request) 把請求交還給靜態服務——典型寫法是「/api/* 走我的邏輯,其餘 fetch env.ASSETS」。
  3. d1_databases:綁一個 D1(Cloudflare 的 SQLite 資料庫)。

重點是:前面那個純靜態站,跟這個有資料庫的站,是同一套部署方式、同一個 wrangler deploy 指令。 你的專案從靜態變成全端,平台一行都不用換。這就是我當初選 Workers Static Assets 最大的理由。

最後叮嚀一句:機密資訊(像 auth secret、OAuth client secret)千萬別寫進 wrangler.jsonc,這個檔是要進 git 的。正式環境請用 wrangler secret put <NAME> 設定,非機密的設定才放 vars

好啦,這套組合我自己跑了四個站,省心又省錢,推薦給同樣在找部署方案的你。有問題歡迎留言,我們下篇見!