跳到內容

Astro on Workers

查證日期
驗證環境astro@7.1.6·@astrojs/cloudflare@14.1.7·wrangler@4.118.0·node@22.22.2·compatibility_date: 2026-07-24

第 24 章的 React Router 是「應用程式為主、內容為輔」;Astro 反過來 —— 它為內容站而生,SSR 是附加能力。LinkForge 的行銷頁、文件、部落格屬於後者。

和 React Router 一樣,Astro 也剛經歷一次大斷層:Astro.locals.runtime.* —— 2025 年每一篇 Astro on Cloudflare 教學存取 binding 的方式 —— 已經被移除。

但這裡有一個重要的修正。 本系列的大綱原本寫「adapter v14 移除了 Astro.locals.runtime」。實際查證後,這是錯的:

這個斷層發生在 @astrojs/cloudflare v13.0.0 / Astro 6,2026-03-10。adapter v14.0.0 的 Major Changes 只有一條:「Upgrade to Vite v8」。Astro 7 的升級指南裡完全沒有 Cloudflare 相關的破壞性變更。

而且移除的方式很體貼 —— 舊 API 沒有變成 undefined,而是變成會丟出「請改用 X」的 getter。實測就知道了。


範例的 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." }
}

三個值得注意的細節:

  1. 錯誤訊息裡寫的是「Astro v6」。 這是官方自己蓋章的版本號。任何說「v7 / adapter v14 移除了它」的教學(包括本系列的大綱初稿)都不準確。
  2. runtime 不是 undefined,它是一個 non-enumerable 的物件。 Object.keys(Astro.locals) 實測只有 ["cfContext"],看不到 runtime。所以 if (Astro.locals.runtime) 這種 feature detection 會通過,然後在下一行存取屬性時才炸 —— 和第 23 章 Pipelines binding 的 typeof 陷阱是同一類問題。
  3. 這是我看過最好的破壞性變更設計。 每個訊息都直接告訴你替代品。對照第 24 章 React Router 的 Invalid context value...,那句話並沒有告訴你正確寫法長什麼樣。

實測 npm registry:

套件最新發布
astro7.1.62026-07-29
@astrojs/cloudflare14.1.72026-07-29

adapter 的 peer dependencies(實測):

{ "astro": "^7.0.0", "wrangler": "^4.83.0" }

wrangler 是 peer 而不是 dependency(v13 起改的),所以你必須自己裝。

adapter majorAstro major關鍵事件
125
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 欄位


---
import { env } from "cloudflare:workers";
const cf = Astro.request.cf; // request.cf
const 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.envimport { env } from "cloudflare:workers"
Astro.locals.runtime.cfAstro.request.cf
Astro.locals.runtime.caches全域 caches
Astro.locals.runtime.ctxAstro.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.cfastro preview 裡有值country: "US"colo: "DFW")。因為 v13 起 dev 和 preview 都跑在 workerd 上。
  • envKeys 裡有三個我沒有宣告的 bindingASSETSIMAGESSESSION。這是 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 自動幫你做的四件事”
astro.config.mjs
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 新增的。

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 這一點下一節會變成一個大問題。

實測產生的 dist/client/_headers

/_astro/*
Cache-Control: public, max-age=31536000, immutable

實測產生的 dist/client/.assetsignore

wrangler.json
.dev.vars

adapter 是附加而非覆寫 —— 如果你自己在 public/.assetsignore 裡放了東西,最終檔案會是兩者的聯集。

實測 dist/index.d.tscloudflare() 接受的全部選項:

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/ 全文搜尋:

字串出現次數
platformProxy0
cloudflareModules0
workerEntryPoint0
_worker.js0
_routes.json0

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 是宣告檔 —— 所以 tscastro check 會直接跳過它。錯誤只有在你把同樣的運算式寫進 .ts 檔、或關掉 skipLibCheck 時才會浮現。

這是潛伏的地雷,不是會擋住建置的錯誤。說得準確一點很重要:它不會讓你的專案跑不起來,但它會在你某天調整 tsconfig 時突然爆出一個看不懂的錯。

正確的做法:

Terminal window
npm create astro@latest
cd my-site
npx astro add cloudflare

astro 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

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 的專案。


實測 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.json

dist/_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/)才是對的。

部署:

Terminal window
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 previewwrangler deploy 時指定 CLOUDFLARE_ENV 不會有任何效果。」

所以正確的指令是:

Terminal window
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
namech25-astroch25-astro-staging
vars.STAGEproductionstaging
topLevelNamech25-astroch25-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 } }
}

三個都成功。 getCollectiongetEntry,連 render() 把 markdown 轉成元件,全部在 Worker 執行期內可用。放 202 篇進去再測一次,all 回傳 202 筆,一樣正常。

content layer 的資料存放在哪裡?實測 grep 建置產物:

dist/server/chunks/_astro_data-layer-content_*.mjs

整個 content store 被打包進 Worker 腳本裡。 這很合理 —— Worker 沒有檔案系統,資料只能跟著程式碼走。但它的規模效應必須量化。

實測,202 篇每篇約 1.6 KB 不重複內容的 markdown:

項目大小
src/content/posts/ 原始 markdown812 KB
_astro_data-layer-content_*.mjs chunk836 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 的大小。

v13.1.0 新增的選項,預設 'workerd'

adapter: cloudflare({ prerenderEnvironment: "node" })

如果你的預先渲染頁面需要 node:fs 或某個 workerd 不相容的套件,切成 'node'。on-demand 頁面則永遠跑在 workerd,不受此選項影響。


官方文件原文:

「新的 workerd 環境不支援 CommonJS 語法,包括 requiremodule.exports 這類 Node.js 特有語法。這代表你的某些專案相依套件可能會拋出錯誤。」

解法是 vite.optimizeDeps.include。這和第 24 章 React Router 遇到的是同一件事 —— workerd 強制 ESM。

實測:astro add cloudflare 產出的設定只有 ["global_fetch_strictly_public"],而完全不寫 wrangler.jsonc 時,產生的建置設定裡 compatibility_flags[]兩者都能正常建置與執行。

只有在你 importnode:* 模組時才需要加。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 輸出。

行銷頁 + 文件 + 部落格,其中一條 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。

src/pages/docs/[...slug].astro
---
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 章):

src/pages/api/waitlist.ts
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 接上來:

src/pages/r/[slug].astro
---
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>

#結論影響
1斷層在 adapter v13 / Astro 6(2026-03-10),不是 v14 / Astro 7v14 的唯一 Major change 是升級 Vite 8
2Astro.locals.runtime 仍然存在,是 non-enumerable 且 getter 會 throwif (locals.runtime) 會通過,下一行才炸
3四個錯誤訊息都寫「removed in Astro v6」並直接給出替代品已於本章逐字記錄
4Object.keys(Astro.locals) 實測只有 ["cfContext"]locals 表面只剩一個 key
5cfContext 的 prototype 是 ["waitUntil","passThroughOnException","constructor"]就是第 3 章的 ExecutionContext
6request.cfastro preview 有值(US / DFWdev/preview 自 v13 起跑在 workerd
7adapter 自動注入 SESSION(KV)、IMAGESASSETS 三個 binding你沒宣告卻出現在 Object.keys(env)
8SESSION binding 沒有 id觸發 wrangler 部署時自動 provision
9Sessions 零設定可用,cookie 名 astro-session,實測 n=1,2,3KV 最終一致,不適合放授權狀態
10Runtime<Env> 實測 TS2315: Type 'Runtime' is not generic這正是 C3 範本寫的東西
11C3 的 src/env.d.ts 是死的,但被 skipLibCheck: true 遮住潛伏地雷,非建置錯誤;請用 npm create astro + astro add cloudflare
12platformProxy / 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
16CLOUDFLARE_ENV=staging astro build 實測改變 namevarswrangler deploy -e 是 no-op環境固化在產物裡,一環境一建置
17Content Collections 在 workerd 執行期可用 —— getCollection / getEntry / render 全部成功(202 筆亦然)任何文件都沒說過
18content store 被打包進 dist/server/chunks/_astro_data-layer-content_*.mjs812 KB markdown → 836 KB chunk → gzip 385 KiB
19由 18 推得:markdown 約 1:1 進 bundle、gzip 約 45%,需對照 3 MB / 10 MB 腳本上限大型內容站必須 prerender
20nodejs_compat 非必需(實測 []["global_fetch_strictly_public"] 都能跑)兩份官方指南都無條件加上,屬過度規範
21Cloudflare 的 Pages Astro 指南仍教 platformProxylocals.runtime.env照做會得到執行期 throw 的專案
22Astro 的 deploy 指南與 integration 指南互相矛盾integration 指南為準

  1. astro dev(而非 preview)下重跑 /legacy,確認 dev 也是 workerd,錯誤訊息一致。
  2. src/pages/content.astro 改成 prerender = true,重新 build,比較 dist/server/chunks/_astro_data-layer-content_*.mjs 是否消失 —— 這是 25.7 那條規則的直接驗證。
  3. 用你自己的部落格內容重做 25.7 的量測,算出你的「markdown → gzip Worker bundle」比例。
  4. 建一個 env.preview,用 CLOUDFLARE_ENV=preview astro build 建置,然後故意加上 wrangler deploy -e staging,確認 -e 確實是 no-op。
  5. astro.config.mjs 加上 imageService: 'custom' 並引入 Sharp,實測它在 workerd 上到底怎麼壞 —— 這是 25.4 那個我拒絕臆測的問題。

下一章(第 26 章):前後端整合 —— 型別共享與 monorepo。把第 24、25 章的兩個前端與前面的 Worker API 放進同一個 repo,用 Workers RPC(第 18 章)取代 HTTP 做內部呼叫,並讓型別真正跨 package 流動。