ch25-astro
在 GitHub 上檢視·12 個檔案·6.0 KB
取得並執行
這個範例可以獨立 clone 執行,不依賴其他章節。
npx degit mrsuner/cloudflare-serverless-platform-manuals/examples/ch25-astro
cd ch25-astro
npm install可用指令
npm run dev # astro dev
npm run deploy # astro build && wrangler deploy
npm run build # astro build
npm run check # astro check
npm run preview # astro preview說明
ch25 — Astro on Workers
Section titled “ch25 — Astro on Workers”A working Astro 7 + @astrojs/cloudflare 14 site, plus probes for the v13
breaking changes and one thing no documentation covers at all.
Verified with astro 7.1.6, @astrojs/cloudflare 14.1.7, wrangler 4.118.0, node 22.22.2.
npm installnpx astro buildnpx astro preview --port 9027 # runs in workerdastro dev and astro preview both run under workerd since adapter v13, so
request.cf is populated and cloudflare:workers imports resolve.
| Page | What it shows |
|---|---|
/ | The current binding surface: import { env } from "cloudflare:workers", Astro.request.cf, Astro.locals.cfContext, the global caches. Also dumps Object.keys(env) — note ASSETS, IMAGES and SESSION are auto-injected. |
/legacy | All four removed Astro.locals.runtime.* accessors, each caught. |
/content | Content Collections called from an on-demand rendered page. Undocumented territory — see below. |
/session | Zero-config Astro Sessions on the auto-provisioned SESSION KV binding. |
The version correction
Section titled “The version correction”The breaking change landed in @astrojs/cloudflare v13.0.0 / Astro 6
(2026-03-10), not v14 / Astro 7. The adapter’s v14.0.0 changelog has exactly
one Major Change: “Upgrade to Vite v8.” The runtime error strings say so
themselves:
curl -s localhost:9027/legacy{ "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." }}runtime is not undefined — it is a non-enumerable object whose getters
throw. So if (Astro.locals.runtime) passes and the next line explodes.
Content Collections at runtime — no source documents this
Section titled “Content Collections at runtime — no source documents this”/content calls getCollection, getEntry and render from a page with
prerender = false, so they execute inside the Worker.
curl -s localhost:9027/content# all: 2 entries, one: { title: "First post" }, rendered: { hasContentComponent: true }They all work. Verified with 202 entries too.
The cost is that the whole content store is bundled into the Worker script:
ls -l dist/server/chunks/_astro_data-layer-content_*.mjsMeasured with 202 posts of ~1.6 KB of unique body text each:
src/content/posts/ markdown | 812 KB |
_astro_data-layer-content_*.mjs | 836 KB |
dist/server gzipped | 385 KiB |
So markdown lands in the bundle roughly 1:1 uncompressed, ~45% gzipped. Against the 3 MB (Free) / 10 MB (Paid) script limits, that means on-demand content collections stop being viable somewhere around 6 MB / 20 MB of source markdown. Beyond that, prerender — then the store never enters the Worker.
(If you measure this yourself, use unique body text. The serialisation format shares references, so duplicated bodies dedupe and flatter the number: my first run with identical bodies reported 109 KB instead of 836 KB.)
Build output
Section titled “Build output”dist/ client/ _headers .assetsignore _astro/… server/ entry.mjs chunks/ wrangler.json.wrangler/deploy/config.jsonThere is no dist/_worker.js/ — that layout predates v13, yet both
Cloudflare’s Astro guide and Astro’s own deploy guide still print
"main": "./dist/_worker.js/index.js".
In your hand-written wrangler.jsonc, main is a package export, not a path:
"main": "@astrojs/cloudflare/entrypoints/server"Deploy with a bare npx wrangler deploy — .wrangler/deploy/config.json
points it at the generated config.
Environments are resolved at build time
Section titled “Environments are resolved at build time”CLOUDFLARE_ENV=staging npx astro build && npx wrangler deploywrangler deploy -e staging is a no-op after the build. Verified:
| default build | CLOUDFLARE_ENV=staging | |
|---|---|---|
generated name | ch25-astro | ch25-astro-staging |
generated vars.STAGE | production | staging |
The environment is baked into the artifact, so CI must build once per environment.
Do not scaffold with C3
Section titled “Do not scaffold with C3”npm create cloudflare -- --framework=astro overlays a src/env.d.ts
containing import("@astrojs/cloudflare").Runtime<Env>, which is
error TS2315: Type 'Runtime' is not generic — and is redundant, because the
adapter injects the correct non-generic declaration itself. It’s masked by
Astro’s skipLibCheck: true, so it won’t break your build; it will surface the
day you touch tsconfig. Its public/.assetsignore also still lists _worker.js
and _routes.json, neither of which exists any more.
Use instead:
npm create astro@latestnpx astro add cloudflareRemoved, verified by grep against dist/
Section titled “Removed, verified by grep against dist/”platformProxy, cloudflareModules, workerEntryPoint, _worker.js,
_routes.json — 0 occurrences each. The full Options surface today is
imageService, sessionKVBindingName, imagesBindingName,
prerenderEnvironment, experimental, plus five options passed straight
through to @cloudflare/vite-plugin.
nodejs_compat is not required — this project runs with only
global_fetch_strictly_public. Add it only if you import node:*.
原始碼
wrangler.jsoncpackage.jsonsrc/content.config.tssrc/content/posts/first.mdsrc/content/posts/second.mdsrc/pages/content.astrosrc/pages/index.astrosrc/pages/legacy.astrosrc/pages/session.astroastro.config.mjs.gitignoretsconfig.json
wrangler.jsonc
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "ch25-astro",
// The virtual entrypoint. NOT "./dist/_worker.js/index.js" -- that path has
// not existed since adapter v13.
"main": "@astrojs/cloudflare/entrypoints/server",
"compatibility_date": "2026-07-24",
"compatibility_flags": ["global_fetch_strictly_public"],
"assets": { "directory": "./dist", "binding": "ASSETS" },
"observability": { "enabled": true },
"vars": { "STAGE": "production", "GREETING": "hello from wrangler.jsonc" },
"kv_namespaces": [{ "binding": "CACHE", "id": "ch25-local-cache" }],
"env": {
"staging": {
"name": "ch25-astro-staging",
"vars": { "STAGE": "staging", "GREETING": "hello from the staging env" },
"assets": { "directory": "./dist", "binding": "ASSETS" },
"kv_namespaces": [{ "binding": "CACHE", "id": "ch25-local-cache" }],
"observability": { "enabled": true }
}
}
}package.json
{
"name": "ch25-astro",
"type": "module",
"private": true,
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"check": "astro check",
"deploy": "astro build && wrangler deploy"
},
"dependencies": {
"@astrojs/cloudflare": "^14.1.7",
"astro": "^7.1.6"
},
"devDependencies": {
"wrangler": "^4.118.0",
"typescript": "^5.9.2"
}
}src/content.config.ts
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
// Content Layer with the glob loader. Whether this survives at RUNTIME under
// workerd (as opposed to build time) is not documented anywhere -- src/pages/
// content.astro tests it.
const posts = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/posts" }),
schema: z.object({
title: z.string(),
date: z.coerce.date(),
}),
});
export const collections = { posts };src/content/posts/first.md
---
title: "First post"
date: 2026-07-01
---
Body of the first post.src/content/posts/second.md
---
title: "Second post"
date: 2026-07-15
---
Body of the second post.src/pages/content.astro
---
// UNDOCUMENTED TERRITORY: no official source says whether the content layer
// works at runtime under workerd, or is build-time only. This page is an
// on-demand rendered page (output: "server"), so getCollection() here runs
// inside the Worker.
export const prerender = false;
import { getCollection, getEntry, render } from "astro:content";
const t = async (fn: () => unknown) => {
try { return { ok: await fn() }; }
catch (e) { return { threw: String(e).slice(0, 240) }; }
};
const all = await t(async () => {
const posts = await getCollection("posts");
return posts.map((p) => ({ id: p.id, title: p.data.title, date: p.data.date }));
});
const one = await t(async () => {
const e = await getEntry("posts", "first");
return e ? { id: e.id, title: e.data.title, bodyLength: e.body?.length ?? null } : null;
});
// Rendering markdown to HTML at runtime is the heavier test.
const rendered = await t(async () => {
const e = await getEntry("posts", "second");
if (!e) return null;
const { Content } = await render(e);
return { hasContentComponent: typeof Content === "function" };
});
---
<html><body><pre>{JSON.stringify({ all, one, rendered }, null, 2)}</pre></body></html>src/pages/index.astro
---
// The v13+ way to reach bindings: a module import, no Astro.locals.runtime.
import { env } from "cloudflare:workers";
const cf = Astro.request.cf;
const cfContext = Astro.locals.cfContext;
// waitUntil now lives on Astro.locals.cfContext, not on locals.runtime.
cfContext.waitUntil(Promise.resolve());
// `caches` is the plain global -- no adapter-specific accessor.
const hasCaches = typeof caches !== "undefined" && "default" in caches;
const kvWrite = await (async () => {
try {
await env.CACHE.put("ch25", String(Date.now()));
return { ok: await env.CACHE.get("ch25") };
} catch (e) {
return { threw: String(e).slice(0, 160) };
}
})();
const info = {
greeting: env.GREETING,
stage: env.STAGE,
envKeys: Object.keys(env).sort(),
localsKeys: Object.keys(Astro.locals),
hasCfContext: typeof cfContext?.waitUntil === "function",
cfContextKeys: cfContext ? Object.getOwnPropertyNames(Object.getPrototypeOf(cfContext)) : null,
cfCountry: cf?.country ?? null,
cfColo: cf?.colo ?? null,
hasCaches,
kvWrite,
};
---
<html lang="en">
<head><title>ch25 — Astro on Workers</title></head>
<body>
<h1>ch25 — Astro on Workers</h1>
<pre id="info">{JSON.stringify(info, null, 2)}</pre>
</body>
</html>src/pages/legacy.astro
---
// Every removed accessor, probed. These are what every 2025-era tutorial
// (and Cloudflare's own Pages guide, still) tells you to use.
const t = (fn: () => unknown) => {
try { return { ok: String(fn()).slice(0, 80) }; }
catch (e) { return { threw: (e as Error).message }; }
};
const runtime = (Astro.locals as Record<string, any>).runtime;
const probes = {
runtimeIsUndefined: runtime === undefined,
runtimeTypeof: typeof runtime,
// Is it enumerable? (i.e. does Object.keys see it?)
runtimeEnumerable: Object.keys(Astro.locals).includes("runtime"),
env: t(() => runtime?.env),
cf: t(() => runtime?.cf),
caches: t(() => runtime?.caches),
ctx: t(() => runtime?.ctx),
};
---
<html><body><pre>{JSON.stringify(probes, null, 2)}</pre></body></html>src/pages/session.astro
---
// Sessions with zero configuration -- the adapter injects a SESSION KV binding.
export const prerender = false;
const n = ((await Astro.session?.get<number>("n")) ?? 0) + 1;
await Astro.session?.set("n", n);
---
<html><body><pre>{JSON.stringify({ sessionAvailable: Astro.session !== undefined, n })}</pre></body></html>astro.config.mjs
// @ts-check
import { defineConfig } from "astro/config";
import cloudflare from "@astrojs/cloudflare";
export default defineConfig({
// Zero-config. The adapter auto-injects the SESSION (KV), IMAGES and ASSETS
// bindings, generates the Worker config, and injects the App.Locals types.
adapter: cloudflare(),
output: "server",
vite: {
// Readable stack traces from workerd.
build: { minify: false },
},
});.gitignore
node_modules/
dist/
.astro/
.wrangler/
worker-configuration.d.tstsconfig.json
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*", "worker-configuration.d.ts"],
"exclude": ["dist"]
}