Astro on Workers
第 24 章的 React Router 是「應用程式為主、內容為輔」;Astro 反過來 —— 它為內容站而生,SSR 是附加能力。LinkForge 的行銷頁、文件、部落格屬於後者。
和 React Router 一樣,Astro 也剛經歷一次大斷層:Astro.locals.runtime.* —— 2025 年每一篇 Astro on Cloudflare 教學存取 binding 的方式 —— 已經被移除。
但這裡有一個重要的修正。 本系列的大綱原本寫「adapter v14 移除了 Astro.locals.runtime」。實際查證後,這是錯的:
這個斷層發生在
@astrojs/cloudflarev13.0.0 / Astro 6,2026-03-10。adapter v14.0.0 的 Major Changes 只有一條:「Upgrade to Vite v8」。Astro 7 的升級指南裡完全沒有 Cloudflare 相關的破壞性變更。
而且移除的方式很體貼 —— 舊 API 沒有變成 undefined,而是變成會丟出「請改用 X」的 getter。實測就知道了。
25.1 那四個錯誤訊息
Section titled “25.1 那四個錯誤訊息”範例的 src/pages/legacy.astro 把四個被移除的存取器各戳一次:
---const runtime = (Astro.locals as Record<string, any>).runtime;const probes = { runtimeIsUndefined: runtime === undefined, runtimeTypeof: typeof runtime, runtimeEnumerable: Object.keys(Astro.locals).includes("runtime"), env: t(() => runtime?.env), cf: t(() => runtime?.cf), caches: t(() => runtime?.caches), ctx: t(() => runtime?.ctx),};---在 astro preview(跑在 workerd 裡)的實測輸出:
{ "runtimeIsUndefined": false, "runtimeTypeof": "object", "runtimeEnumerable": false, "env": { "threw": "Astro.locals.runtime.env has been removed in Astro v6. Use 'import { env } from \"cloudflare:workers\"' instead." }, "cf": { "threw": "Astro.locals.runtime.cf has been removed in Astro v6. Use 'Astro.request.cf' instead." }, "caches": { "threw": "Astro.locals.runtime.caches has been removed in Astro v6. Use the global 'caches' object instead." }, "ctx": { "threw": "Astro.locals.runtime.ctx has been removed in Astro v6. Use 'Astro.locals.cfContext' instead." }}三個值得注意的細節:
- 錯誤訊息裡寫的是「Astro v6」。 這是官方自己蓋章的版本號。任何說「v7 / adapter v14 移除了它」的教學(包括本系列的大綱初稿)都不準確。
runtime不是undefined,它是一個 non-enumerable 的物件。Object.keys(Astro.locals)實測只有["cfContext"],看不到runtime。所以if (Astro.locals.runtime)這種 feature detection 會通過,然後在下一行存取屬性時才炸 —— 和第 23 章 Pipelines binding 的typeof陷阱是同一類問題。- 這是我看過最好的破壞性變更設計。 每個訊息都直接告訴你替代品。對照第 24 章 React Router 的
Invalid context value...,那句話並沒有告訴你正確寫法長什麼樣。
25.2 版本對照
Section titled “25.2 版本對照”實測 npm registry:
| 套件 | 最新 | 發布 |
|---|---|---|
astro | 7.1.6 | 2026-07-29 |
@astrojs/cloudflare | 14.1.7 | 2026-07-29 |
adapter 的 peer dependencies(實測):
{ "astro": "^7.0.0", "wrangler": "^4.83.0" }wrangler 是 peer 而不是 dependency(v13 起改的),所以你必須自己裝。
| adapter major | Astro major | 關鍵事件 |
|---|---|---|
| 12 | 5 | |
| 13(2026-03-10) | 6 | 移除 Astro.locals.runtime.*、移除 Pages 支援、astro dev 改跑 workerd、imageService 預設改為 cloudflare-binding |
| 14(2026-06-22) | 7 | 升級到 Vite 8。沒有其他 Major change |
Astro 7.1.6 宣告 node >=22.12.0。adapter 本身沒有 engines 欄位。
25.3 現在的存取方式
Section titled “25.3 現在的存取方式”---import { env } from "cloudflare:workers";
const cf = Astro.request.cf; // request.cfconst cfContext = Astro.locals.cfContext; // ExecutionContext
cfContext.waitUntil(logSomething());const cached = await caches.default.match(req); // 全域 caches
const value = await env.CACHE.get("key");---四個對照:
| v12 之前 | v13 起 |
|---|---|
Astro.locals.runtime.env | import { env } from "cloudflare:workers" |
Astro.locals.runtime.cf | Astro.request.cf |
Astro.locals.runtime.caches | 全域 caches |
Astro.locals.runtime.ctx | Astro.locals.cfContext |
範例首頁的實測輸出:
{ "greeting": "hello from wrangler.jsonc", "stage": "production", "envKeys": ["ASSETS", "CACHE", "GREETING", "IMAGES", "SESSION", "STAGE"], "localsKeys": ["cfContext"], "hasCfContext": true, "cfContextKeys": ["waitUntil", "passThroughOnException", "constructor"], "cfCountry": "US", "cfColo": "DFW", "hasCaches": true, "kvWrite": { "ok": "1785566016639" }}幾件事一次確認:
Astro.locals只有cfContext一個 key。 整個locals表面就這麼小。cfContext的 prototype 是["waitUntil", "passThroughOnException", "constructor"]—— 就是第 3 章那個ExecutionContext。注意它沒有props;官方文件另外提到cfContext.exports.Greeter.greet(...)可以呼叫 Durable Object exports(第 14 章的 declarative exports),那是 own property 而非 prototype method。request.cf在astro preview裡有值(country: "US"、colo: "DFW")。因為 v13 起 dev 和 preview 都跑在 workerd 上。envKeys裡有三個我沒有宣告的 binding:ASSETS、IMAGES、SESSION。這是 adapter 自動注入的,下一節說明。
注意 env 是模組層匯入。 和第 24 章一樣,這受第 3 章那條規則約束:頂層 scope 不能做 I/O。Astro 的 frontmatter(--- 之間)是在請求處理中執行的,所以那裡呼叫 env.CACHE.get() 沒問題;但如果你在一個 .ts 檔的模組頂層直接呼叫,就會炸。
25.4 Zero-config:adapter 自動幫你做的四件事
Section titled “25.4 Zero-config:adapter 自動幫你做的四件事”import { defineConfig } from "astro/config";import cloudflare from "@astrojs/cloudflare";
export default defineConfig({ adapter: cloudflare(), output: "server",});cloudflare() 不帶任何參數。實測 build 時的輸出:
[@astrojs/cloudflare] Enabling image processing with Cloudflare Images for production with the "IMAGES" Images binding.[@astrojs/cloudflare] Enabling sessions with Cloudflare KV with the "SESSION" KV binding.[@astrojs/cloudflare] Injected immutable Cache-Control for /_astro/* into _headers.(一)Sessions:KV binding 自動注入、自動 provision
Section titled “(一)Sessions:KV binding 自動注入、自動 provision”實測產生的 dist/server/wrangler.json:
"kv_namespaces": [ { "binding": "SESSION" }, { "binding": "CACHE", "id": "ch25-local-cache" }]注意 SESSION 沒有 id。 這正是觸發 wrangler automatic provisioning 的條件 —— 部署時 wrangler 會自動建立 namespace 並綁上去。
用法零設定:
---export const prerender = false;const n = ((await Astro.session?.get<number>("n")) ?? 0) + 1;await Astro.session?.set("n", n);---實測(帶 cookie jar 連續三次請求):
{"sessionAvailable":true,"n":1}{"sessionAvailable":true,"n":2}{"sessionAvailable":true,"n":3}cookie 名稱是 astro-session。
一個 KV 特性帶來的限制(回到第 8 章): KV 是最終一致的,跨區域傳播最長可到 60 秒。所以 session 適合放偏好設定、購物車這類「稍微舊一點沒關係」的東西,不適合放需要立刻生效的授權狀態。要立即一致的 session,用 Durable Object(第 14 章)。
想換 binding 名稱用
sessionKVBindingName;如果你自己在 wrangler config 裡宣告了同名 binding,adapter 會用你的。
(二)Images:預設 cloudflare-binding
Section titled “(二)Images:預設 cloudflare-binding”v13.0.0 把 imageService 的預設值從 'compile' 改成 'cloudflare-binding',並自動注入 IMAGES binding(實測確認)。
改變的理由,官方 changelog 只說是「為了在處理圖片時有更好的體驗」,並提到舊選項「因為 Node.js 不相容導致 dev 中圖片壞掉」。
這裡本章要比常見說法保守一點。 網路上(含本系列大綱初稿)常見的說法是「Sharp 是原生 libvips addon,在 workerd 跑不起來,所以預設改了」。這個推論很合理,但官方文件從來沒有這樣寫。文件實際說的比較窄:
'custom'選項「會打包該 image service 供執行期使用,但不會檢查它與 workerd 的相容性」。所以本章的陳述是:預設值改了,理由是 Node.js 相容性問題。至於 Sharp 具體會怎麼壞,需要你自己實測。
完整型別:
imageService?: 'passthrough' | 'cloudflare' | 'cloudflare-binding' | 'compile' | 'custom' | { build: 'compile'; runtime?: 'cloudflare-binding' | 'passthrough' }物件形式(build 和 runtime 用不同的 image service)是 v13 新增的。
(三)TypeScript 型別自動注入
Section titled “(三)TypeScript 型別自動注入”adapter 會產生 .astro/integrations/_astrojs_cloudflare/cloudflare.d.ts,裡面 reference @astrojs/cloudflare/types.d.ts:
type Runtime = import('./dist/index.d.ts').Runtime;declare namespace App { interface Locals extends Runtime {}}所以你不需要手寫 src/env.d.ts。 這一點下一節會變成一個大問題。
(四)_headers 與 .assetsignore
Section titled “(四)_headers 與 .assetsignore”實測產生的 dist/client/_headers:
/_astro/* Cache-Control: public, max-age=31536000, immutable實測產生的 dist/client/.assetsignore:
wrangler.json.dev.varsadapter 是附加而非覆寫 —— 如果你自己在 public/.assetsignore 裡放了東西,最終檔案會是兩者的聯集。
完整的 Options 型別
Section titled “完整的 Options 型別”實測 dist/index.d.ts,cloudflare() 接受的全部選項:
export interface Options extends Pick<PluginConfig, 'auxiliaryWorkers' | 'configPath' | 'inspectorPort' | 'persistState' | 'remoteBindings'> { imageService?: ImageServiceConfig; sessionKVBindingName?: string; // 預設 'SESSION' imagesBindingName?: string; // 預設 'IMAGES' prerenderEnvironment?: 'workerd' | 'node'; // 預設 'workerd' experimental?: Pick<..., 'headersAndRedirectsDevModeSupport'>;}前五個是直接透傳給 @cloudflare/vite-plugin 的(adapter 內部就是用它,@cloudflare/vite-plugin 是 hard dependency)。
25.5 已移除的設定與那個「安靜的地雷」
Section titled “25.5 已移除的設定與那個「安靜的地雷」”實測在 node_modules/@astrojs/cloudflare/dist/ 全文搜尋:
| 字串 | 出現次數 |
|---|---|
platformProxy | 0 |
cloudflareModules | 0 |
workerEntryPoint | 0 |
_worker.js | 0 |
_routes.json | 0 |
Runtime 型別還在 export,但不再是泛型:
export interface Runtime { cfContext: ExecutionContext }實測寫 import("@astrojs/cloudflare").Runtime<Env>:
error TS2315: Type 'Runtime' is not generic.npm create cloudflare -- --framework=astro 產出的東西是壞的
Section titled “npm create cloudflare -- --framework=astro 產出的東西是壞的”C3(create-cloudflare@2.70.16)的 workers 變體流程本身是對的 —— 它跑 create-astro,再跑 npx astro add cloudflare -y。但它接著把自己的範本檔蓋上去,而那些範本是死的:
// templates/astro/workers/templates/ts/src/env.d.ts —— C3 現行產出type Runtime = import("@astrojs/cloudflare").Runtime<Env>;
declare namespace App { interface Locals extends Runtime {}}這一行就是上面那個 TS2315。而且它完全多餘 —— adapter 已經自動注入了正確的(非泛型)版本。
# templates/astro/workers/templates/ts/public/.assetsignore —— C3 現行產出_worker.js_routes.json這兩個產物在 v13 之後都不存在了。無害(adapter 是附加而非覆寫),但是垃圾。
為什麼沒人發現? Astro 內建的 tsconfig 設了
skipLibCheck: true,而env.d.ts是宣告檔 —— 所以tsc和astro check會直接跳過它。錯誤只有在你把同樣的運算式寫進.ts檔、或關掉skipLibCheck時才會浮現。這是潛伏的地雷,不是會擋住建置的錯誤。說得準確一點很重要:它不會讓你的專案跑不起來,但它會在你某天調整 tsconfig 時突然爆出一個看不懂的錯。
正確的做法:
npm create astro@latestcd my-sitenpx astro add cloudflareastro add cloudflare 在 14.1.7 產出的東西是乾淨的:
{ "$schema": "./node_modules/wrangler/config-schema.json", "compatibility_date": "2026-08-01", "compatibility_flags": ["global_fetch_strictly_public"], "name": "my-site", "main": "@astrojs/cloudflare/entrypoints/server", "assets": { "directory": "./dist", "binding": "ASSETS" }, "observability": { "enabled": true }}沒有 src/env.d.ts,也沒有 nodejs_compat。
Pages 支援已移除
Section titled “Pages 支援已移除”v13.0.0 的 changelog:
Drops official support for Cloudflare Pages in favor of Cloudflare Workers 「Astro Cloudflare adapter 現在預設只支援部署到 Cloudflare Workers,以符合 Cloudflare 對新專案的建議。」
⚠️ Cloudflare 官方的 Pages 指南(
/pages/framework-guides/deploy-an-astro-site/,頁面顯示 2026-04-21 更新)現在仍然推薦@astrojs/cloudflare用於 Pages SSR,展示cloudflare({ platformProxy: { enabled: true } }),並教你寫Astro.locals.runtime.env.MY_KV。三個都是已移除的 API,而且那一頁沒有任何棄用提示、沒有任何指向 Workers 的連結。照著那一頁做,會得到一個執行期直接 throw 的專案。
25.6 建置輸出與部署
Section titled “25.6 建置輸出與部署”實測 astro build 的輸出:
dist/ client/ _headers <- adapter 注入 immutable Cache-Control .assetsignore <- adapter 附加 wrangler.json / .dev.vars _astro/... server/ entry.mjs <- Worker 進入點 chunks/*.mjs virtual_astro_middleware.mjs wrangler.json <- 自動產生的部署設定.wrangler/ deploy/config.jsondist/_worker.js/ 這個目錄不存在。 那是 v13 之前的結構。
產生的 dist/server/wrangler.json 關鍵欄位:
{ "name": "ch25-astro", "main": "entry.mjs", "assets": { "directory": "../client", "binding": "ASSETS" }, "no_bundle": true, "kv_namespaces": [{ "binding": "SESSION" }, { "binding": "CACHE", "id": "..." }], "images": { "binding": "IMAGES" }}.wrangler/deploy/config.json:
{ "configPath": "../../dist/server/wrangler.json", "auxiliaryWorkers": [], "prerenderWorkerConfigPath": "../../dist/server/.prerender/wrangler.json"}(最後那個 prerenderWorkerConfigPath 對應 25.7 的 prerenderEnvironment。)
你手寫的 wrangler.jsonc 裡,main 應該是虛擬進入點:
"main": "@astrojs/cloudflare/entrypoints/server"不是路徑,是套件匯出。v13.0.0 起同一個進入點同時服務 astro dev 與 production。
⚠️ Cloudflare 官方 Astro 指南(頁面顯示 2026-04-23 更新)仍然寫
"main": "./dist/_worker.js/index.js"—— 實測 v14 的 build 產物裡根本沒有這個路徑。同一頁還寫著「Astro 6(目前在 beta)需要 Node.js 22 或更高」,而 Astro 6 在 2026-03-10 就 GA 了,Astro 7 也已經 GA 兩個月。⚠️ Astro 自己的 deploy 指南(
/en/guides/deploy/cloudflare/)也還在寫"main": "dist/_worker.js/index.js",並且無條件加上nodejs_compat—— 直接和 adapter 的 integration 指南矛盾。兩者之中,integration 指南(/en/guides/integrations-guide/cloudflare/)才是對的。
部署:
npx astro build && npx wrangler deploy不需要 -c,.wrangler/deploy/config.json 會指路。
多環境:環境在 build 時就被固化了
Section titled “多環境:環境在 build 時就被固化了”這是從 Astro 6 起改變、而且很容易搞錯的一點。官方文件:
「在 Astro 5.x,你可以建置一次然後用
wrangler deploy --env some-env部署到特定的 Cloudflare 環境。從 Astro 6.0 起,這個整合改為依賴 Cloudflare Vite plugin,環境現在是在 build 階段決定的。因此你必須為每個環境分別建置。」
Cloudflare 的 Vite plugin 文件講得更直接:
「由於 Cloudflare 環境是在 dev 與 build 時套用的,在執行
vite preview或wrangler deploy時指定CLOUDFLARE_ENV不會有任何效果。」
所以正確的指令是:
CLOUDFLARE_ENV=staging astro build && npx wrangler deploy不是 wrangler deploy -e staging —— 那個旗標在建置後是 no-op。
實測驗證,同一份 wrangler.jsonc(裡面有 env.staging),兩次建置產生的 dist/server/wrangler.json:
| 欄位 | 預設建置 | CLOUDFLARE_ENV=staging |
|---|---|---|
name | ch25-astro | ch25-astro-staging |
vars.STAGE | production | staging |
topLevelName | ch25-astro | ch25-astro(不變) |
環境被烤進了產物裡。 這對 CI/CD 的影響是實質的(第 40 章會展開):你不能建置一次然後推到多個環境,必須一個環境建一次。
25.7 Content Collections 在 workerd 上跑得起來嗎
Section titled “25.7 Content Collections 在 workerd 上跑得起來嗎”這一節是本章唯一沒有任何官方或非官方資料可以參考的部分。 我搜過 adapter 文件、deploy 指南、Astro 6/7 升級指南、Cloudflare 的 framework guide —— 沒有任何一處說明 content layer 在 Workers 上是否只能在 build 時使用。所以我實測了。
範例的 src/pages/content.astro 是一個 on-demand 渲染的頁面(export const prerender = false),裡面呼叫 content collections 的三個 API:
---export const prerender = false;import { getCollection, getEntry, render } from "astro:content";
const all = await getCollection("posts");const one = await getEntry("posts", "first");const { Content } = await render(one);---實測結果(astro preview,跑在 workerd 裡):
{ "all": { "ok": [ { "id": "first", "title": "First post", ... }, ... ] }, "one": { "ok": { "id": "first", "title": "First post", "bodyLength": 23 } }, "rendered": { "ok": { "hasContentComponent": true } }}三個都成功。 getCollection、getEntry,連 render() 把 markdown 轉成元件,全部在 Worker 執行期內可用。放 202 篇進去再測一次,all 回傳 202 筆,一樣正常。
但是有一個沒人講的成本
Section titled “但是有一個沒人講的成本”content layer 的資料存放在哪裡?實測 grep 建置產物:
dist/server/chunks/_astro_data-layer-content_*.mjs整個 content store 被打包進 Worker 腳本裡。 這很合理 —— Worker 沒有檔案系統,資料只能跟著程式碼走。但它的規模效應必須量化。
實測,202 篇每篇約 1.6 KB 不重複內容的 markdown:
| 項目 | 大小 |
|---|---|
src/content/posts/ 原始 markdown | 812 KB |
_astro_data-layer-content_*.mjs chunk | 836 KB |
dist/server 整體 gzip 後 | 385 KiB |
大致的比例是:原始 markdown 幾乎 1:1 進入 Worker bundle,gzip 之後約為原始大小的 45%。
(第一次測量時我用了完全相同的內文,chunk 只有 109 KB —— 序列化格式會做參考共用,重複字串會被去重。上表用的是不重複內容,這才是誠實的數字。)
Workers 的腳本大小上限是 3 MB(Free)/ 10 MB(Paid),壓縮後計算。推導出來的實用規則:
| markdown 總量 | gzip 後估計 | 判斷 |
|---|---|---|
| ~1 MB | ~470 KiB | 完全沒問題 |
| ~3 MB | ~1.4 MiB | 沒問題,但要開始注意 |
| ~6 MB | ~2.8 MiB | 逼近 Free 方案上限 |
| ~20 MB | ~9.4 MiB | 逼近 Paid 方案上限 |
超過這個量級,就不能再用 on-demand 渲染 content collections,必須改成 prerender(那樣 content 只在 build 時使用,不進 Worker)。
這個結論在任何文件裡都找不到,請自行以你的實際內容量驗證。 我的量測是 200 篇短文;長文、大量 frontmatter、或多個 collection 的比例可能不同。驗證方法很簡單:
astro build之後看dist/server/chunks/_astro_data-layer-content_*.mjs的大小。
順帶:prerenderEnvironment
Section titled “順帶:prerenderEnvironment”v13.1.0 新增的選項,預設 'workerd':
adapter: cloudflare({ prerenderEnvironment: "node" })如果你的預先渲染頁面需要 node:fs 或某個 workerd 不相容的套件,切成 'node'。on-demand 頁面則永遠跑在 workerd,不受此選項影響。
25.8 限制
Section titled “25.8 限制”workerd 不支援 CommonJS
Section titled “workerd 不支援 CommonJS”官方文件原文:
「新的 workerd 環境不支援 CommonJS 語法,包括
require與module.exports這類 Node.js 特有語法。這代表你的某些專案相依套件可能會拋出錯誤。」
解法是 vite.optimizeDeps.include。這和第 24 章 React Router 遇到的是同一件事 —— workerd 強制 ESM。
nodejs_compat 不是必需的
Section titled “nodejs_compat 不是必需的”實測:astro add cloudflare 產出的設定只有 ["global_fetch_strictly_public"],而完全不寫 wrangler.jsonc 時,產生的建置設定裡 compatibility_flags 是 []。兩者都能正常建置與執行。
只有在你 import 了 node:* 模組時才需要加。Cloudflare 的指南和 Astro 的 deploy 指南都無條件加上它 —— 那是過度規範,不是需求。
(注意:若要讓 wrangler 注入 polyfill,需要 nodejs_compat 加上 compatibility_date ≥ 2024-09-23。)
- StackBlitz 不支援 —— adapter 會直接 throw,因為 workerd 在那裡跑不起來。
- 錯誤訊息被 minify —— 加
vite: { build: { minify: false } }才看得懂堆疊。範例專案已經加了。 - 純靜態站不需要 adapter。 Cloudflare 指南:「如果你用 Astro 產生純靜態站,就不能使用 bindings。」全部 prerender 時,adapter 甚至不會產出 server 輸出。
25.9 LinkForge:apps/site
Section titled “25.9 LinkForge:apps/site”行銷頁 + 文件 + 部落格,其中一條 SSR API route。
apps/site/ src/ content.config.ts Content Layer 定義 content/ docs/*.md blog/*.md pages/ index.astro prerender(行銷首頁) docs/[...slug].astro prerender(文件) blog/[...slug].astro prerender(部落格) api/waitlist.ts SSR(寫進 D1) r/[slug].astro SSR(短網址預覽頁,讀 KV) astro.config.mjs wrangler.jsonc選型的關鍵在於哪些頁面要 prerender。 依 25.7 的量測,文件與部落格全部 prerender,content store 就不會進 Worker bundle;只有真正需要動態的兩條路由是 SSR。
---export const prerender = true; // 這一行決定 content 不進 Worker
import { getCollection, render } from "astro:content";
export async function getStaticPaths() { const docs = await getCollection("docs"); return docs.map((d) => ({ params: { slug: d.id }, props: { doc: d } }));}
const { doc } = Astro.props;const { Content } = await render(doc);---<Layout title={doc.data.title}><Content /></Layout>SSR 端點讀 D1(第 9 章):
import type { APIRoute } from "astro";import { env } from "cloudflare:workers";
export const prerender = false;
export const POST: APIRoute = async ({ request, locals }) => { const { email } = (await request.json()) as { email: string }; if (!/^[^@\s]+@[^@\s]+$/.test(email)) { return new Response("bad email", { status: 400 }); }
await env.DB.prepare("insert into waitlist (email, country) values (?1, ?2)") .bind(email, request.cf?.country ?? "XX") .run();
// 通知信不該擋住回應 —— 這正是 cfContext 存在的理由(第 3 章) locals.cfContext.waitUntil(sendWelcome(email));
return new Response(null, { status: 204 });};注意 locals.cfContext.waitUntil —— 這是 import { env } 給不了的東西,也是 Astro.locals 上僅存的那一個 key 的用途。
短網址預覽頁把第 8 章的 KV 接上來:
---export const prerender = false;import { env } from "cloudflare:workers";
const { slug } = Astro.params;const target = await env.LINKS.get(`link:${slug}`, { cacheTtl: 300 });if (!target) return Astro.redirect("/404", 302);---<Layout title={`→ ${target}`}> <p>你即將前往 <code>{target}</code></p> <a href={target} rel="nofollow noopener">繼續</a></Layout>25.10 本章實測結論彙整
Section titled “25.10 本章實測結論彙整”| # | 結論 | 影響 |
|---|---|---|
| 1 | 斷層在 adapter v13 / Astro 6(2026-03-10),不是 v14 / Astro 7 | v14 的唯一 Major change 是升級 Vite 8 |
| 2 | Astro.locals.runtime 仍然存在,是 non-enumerable 且 getter 會 throw | if (locals.runtime) 會通過,下一行才炸 |
| 3 | 四個錯誤訊息都寫「removed in Astro v6」並直接給出替代品 | 已於本章逐字記錄 |
| 4 | Object.keys(Astro.locals) 實測只有 ["cfContext"] | locals 表面只剩一個 key |
| 5 | cfContext 的 prototype 是 ["waitUntil","passThroughOnException","constructor"] | 就是第 3 章的 ExecutionContext |
| 6 | request.cf 在 astro preview 有值(US / DFW) | dev/preview 自 v13 起跑在 workerd |
| 7 | adapter 自動注入 SESSION(KV)、IMAGES、ASSETS 三個 binding | 你沒宣告卻出現在 Object.keys(env) |
| 8 | SESSION binding 沒有 id | 觸發 wrangler 部署時自動 provision |
| 9 | Sessions 零設定可用,cookie 名 astro-session,實測 n=1,2,3 | KV 最終一致,不適合放授權狀態 |
| 10 | Runtime<Env> 實測 TS2315: Type 'Runtime' is not generic | 這正是 C3 範本寫的東西 |
| 11 | C3 的 src/env.d.ts 是死的,但被 skipLibCheck: true 遮住 | 潛伏地雷,非建置錯誤;請用 npm create astro + astro add cloudflare |
| 12 | platformProxy / cloudflareModules / workerEntryPoint / _worker.js / _routes.json 在 dist 中各出現 0 次 | 全部移除 |
| 13 | 建置輸出是 dist/client/ + dist/server/entry.mjs,沒有 dist/_worker.js/ | Cloudflare 與 Astro 兩邊的指南都還在寫舊路徑 |
| 14 | 手寫設定的 main 應為 @astrojs/cloudflare/entrypoints/server(套件匯出,非路徑) | |
| 15 | 產生的設定 main: "entry.mjs"、assets.directory: "../client"、no_bundle: true | 部署用裸 wrangler deploy |
| 16 | CLOUDFLARE_ENV=staging astro build 實測改變 name 與 vars;wrangler deploy -e 是 no-op | 環境固化在產物裡,一環境一建置 |
| 17 | Content Collections 在 workerd 執行期可用 —— getCollection / getEntry / render 全部成功(202 筆亦然) | 任何文件都沒說過 |
| 18 | content store 被打包進 dist/server/chunks/_astro_data-layer-content_*.mjs | 812 KB markdown → 836 KB chunk → gzip 385 KiB |
| 19 | 由 18 推得:markdown 約 1:1 進 bundle、gzip 約 45%,需對照 3 MB / 10 MB 腳本上限 | 大型內容站必須 prerender |
| 20 | nodejs_compat 非必需(實測 [] 與 ["global_fetch_strictly_public"] 都能跑) | 兩份官方指南都無條件加上,屬過度規範 |
| 21 | Cloudflare 的 Pages Astro 指南仍教 platformProxy 與 locals.runtime.env | 照做會得到執行期 throw 的專案 |
| 22 | Astro 的 deploy 指南與 integration 指南互相矛盾 | 以 integration 指南為準 |
25.11 動手練習
Section titled “25.11 動手練習”- 在
astro dev(而非 preview)下重跑/legacy,確認 dev 也是 workerd,錯誤訊息一致。 - 把
src/pages/content.astro改成prerender = true,重新 build,比較dist/server/chunks/_astro_data-layer-content_*.mjs是否消失 —— 這是 25.7 那條規則的直接驗證。 - 用你自己的部落格內容重做 25.7 的量測,算出你的「markdown → gzip Worker bundle」比例。
- 建一個
env.preview,用CLOUDFLARE_ENV=preview astro build建置,然後故意加上wrangler deploy -e staging,確認-e確實是 no-op。 - 在
astro.config.mjs加上imageService: 'custom'並引入 Sharp,實測它在 workerd 上到底怎麼壞 —— 這是 25.4 那個我拒絕臆測的問題。
- @astrojs/cloudflare 整合指南(這一頁是正確的)
- Astro deploy to Cloudflare 指南(⚠️
main路徑過時,與上一頁矛盾) - Adapter CHANGELOG
- Upgrade to Astro v6 · Upgrade to Astro v7
- Cloudflare Workers Astro 指南(⚠️
main路徑與版本說明過時) - Cloudflare Pages Astro 指南(⚠️ 嚴重過時,教的三個 API 都已移除)
- Vite plugin — Cloudflare environments · Wrangler automatic provisioning
- 版本與型別來源:npm registry 中繼資料,以及
node_modules/@astrojs/cloudflare@14.1.7的實際dist/
下一章(第 26 章):前後端整合 —— 型別共享與 monorepo。把第 24、25 章的兩個前端與前面的 Worker API 放進同一個 repo,用 Workers RPC(第 18 章)取代 HTTP 做內部呼叫,並讓型別真正跨 package 流動。