1---
2title: Astro
3description: Deploy an Astro site to Cloudflare Workers with Cloudflare.Website.Astro — SSR in the Worker, prerendered pages as static assets, sessions backed by an auto-provisioned KV namespace.
4sidebar:
5 order: 9
6---
7
8import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
9
10export const devDeps = "@alchemy.run/frontend-frameworks";
11
12`Cloudflare.Website.Astro` deploys an [Astro](https://astro.build)
13project as a Cloudflare Worker. It runs Astro's programmatic build
14with a wrangler-free Cloudflare adapter: server-rendered pages
15execute in the Worker, prerendered pages and client assets deploy as
16static assets. Your `astro.config.*` loads natively — there is no
17adapter to install into it and no Wrangler file to write.
18
19## Install
20
21The build integration is not bundled with alchemy. Install
22`@alchemy.run/frontend-frameworks`; the resource loads its `/astro`
23export from your project at deploy time. It is only used at build time,
24so a dev dependency is enough:
25
26<Tabs syncKey="pkgManager">
27 <TabItem label="bun" icon="bun">
28 <Code code={`bun add -d ${devDeps}`} lang="sh" />
29 </TabItem>
30 <TabItem label="npm" icon="npm">
31 <Code code={`npm install -D ${devDeps}`} lang="sh" />
32 </TabItem>
33 <TabItem label="pnpm" icon="pnpm">
34 <Code code={`pnpm add -D ${devDeps}`} lang="sh" />
35 </TabItem>
36 <TabItem label="yarn" icon="seti:yarn">
37 <Code code={`yarn add -D ${devDeps}`} lang="sh" />
38 </TabItem>
39</Tabs>
40
41## Configure Astro
42
43Your `astro.config.*` file loads natively — integrations, Vite
44plugins, and any other non-serializable options work exactly as they
45do outside Alchemy. Alchemy merges a programmatic config *over* it:
46the Cloudflare adapter is injected for you (don't declare an
47`adapter` in your config — that fails the build with an actionable
48error), and options passed via the `astro` prop override the file's —
49see [Astro configuration](#astro-configuration) below for the
50details.
51
52## Declare the Website
53
54Declare the site as a module-level const (rather than inline in the
55Stack) and derive the typed shape of its bindings from it:
56
57```typescript
58// alchemy.run.ts
59import * as Cloudflare from "alchemy/Cloudflare";
60
61export const Website = Cloudflare.Website.Astro("Website");
62
63export type WebsiteEnv = Cloudflare.InferEnv<typeof Website>;
64```
65
66`WebsiteEnv` is the typed shape of the Worker's bindings, derived
67from the class — you'll import it into your Astro code in
68[Read bindings in server code](#read-bindings-in-server-code).
69
70## Add it to the Stack
71
72Yield the class from your Stack and return its URL:
73
74```typescript
75// alchemy.run.ts
76import * as Alchemy from "alchemy";
77import * as Effect from "effect/Effect";
78
79export default Alchemy.Stack(
80 "MyAstroSite",
81 {
82 providers: Cloudflare.providers(),
83 state: Cloudflare.state(),
84 },
85 Effect.gen(function* () {
86 const site = yield* Website;
87 return { url: site.url };
88 }),
89);
90```
91
92Pages are server-rendered by default. Pages that
93`export const prerender = true` are served as static assets. Astro's
94server runtime is built against Node APIs, so the `nodejs_compat`
95compatibility flag is added automatically — you don't need to pass
96it.
97
98During `alchemy dev`, Astro's dev server runs with SSR executing in
99workerd, so server code sees the same runtime and the same real
100bindings it will have in production.
101
102See
103[examples/cloudflare-website-astro](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-website-astro)
104for the checked-in example.
105
106## Add bindings
107
108The resource returns a plain `Worker`, so `env` accepts the full
109binding vocabulary — KV namespaces, R2 buckets, Durable Objects,
110secrets:
111
112```diff lang="typescript"
113// alchemy.run.ts
114+import * as Config from "effect/Config";
115
116+export const Cache = Cloudflare.KV.Namespace("Cache");
117
118export const Website = Cloudflare.Website.Astro("Website", {
119+ env: {
120+ CACHE: Cache,
121+ API_KEY: Config.Redacted("API_KEY"),
122+ },
123});
124```
125
126`Cache` is a description, not a deploy — Alchemy provisions the real
127namespace because the Website binds it. `Config.Redacted` reads
128`API_KEY` from your environment at deploy time and binds it as a
129Worker secret — see [Secrets & env](/cloudflare/security/secrets-env).
130
131## Read bindings in server code
132
133Astro code reads bindings via `Astro.locals.runtime.env` (or
134`import { env } from "cloudflare:workers"`). Type the locals once by
135augmenting `App.Locals` with the inferred env type:
136
137```typescript
138// src/env.d.ts
139import type { WebsiteEnv } from "../alchemy.run.ts";
140
141declare global {
142 namespace App {
143 interface Locals {
144 runtime: { env: WebsiteEnv };
145 }
146 }
147}
148
149export {};
150```
151
152Every server-rendered page and endpoint now gets fully typed
153bindings:
154
155```astro
156---
157// src/pages/index.astro
158const cached = await Astro.locals.runtime.env.CACHE.get("greeting");
159---
160
161<h1>{cached ?? "hello"}</h1>
162```
163
164`Astro.locals.runtime.env.CACHE` is typed as a KV namespace and
165`.API_KEY` as a string — renaming a binding in `alchemy.run.ts` is a
166type error in your pages.
167
168## Sessions
169
170Astro's session API is backed by a KV namespace. One is provisioned
171and bound as `SESSION` automatically, so `Astro.session` works with
172zero configuration.
173
174Bind your own namespace under that name to use it instead:
175
176```typescript
177export const Sessions = Cloudflare.KV.Namespace("Sessions");
178
179export const Website = Cloudflare.Website.Astro("Website", {
180 env: {
181 SESSION: Sessions,
182 },
183});
184```
185
186Or opt out of session provisioning entirely:
187
188```typescript
189export const Website = Cloudflare.Website.Astro("Website", {
190 sessionKVBindingName: false,
191});
192```
193
194## Fully static sites
195
196With `astro: { output: "static" }` every page is prerendered at
197build time and the deploy is assets-only — no server bundle is
198uploaded, and Cloudflare's asset layer answers every request:
199
200```typescript
201export const Website = Cloudflare.Website.Astro("Website", {
202 astro: { output: "static" },
203 assets: { notFoundHandling: "404-page" },
204});
205```
206
207`notFoundHandling: "404-page"` serves the built `404.html` for
208unmatched routes. Session provisioning is skipped for declared-static
209sites, since no Worker code runs at request time.
210
211## Astro configuration
212
213Your `astro.config.*` is the primary place to configure Astro —
214integrations, Vite plugins, and every other option (serializable or
215not) work as usual:
216
217```typescript
218// astro.config.ts
219import { defineConfig } from "astro/config";
220import react from "@astrojs/react";
221import tailwindcss from "@tailwindcss/vite";
222
223export default defineConfig({
224 site: "https://blog.example.com",
225 integrations: [react()],
226 vite: {
227 plugins: [tailwindcss()],
228 },
229});
230```
231
232A config file can't consume Alchemy `Output`s or vary by stage. For
233those values the `astro` prop is a deploy-time override bag — a
234serializable subset (`site`, `base`, `output`, `srcDir`, `publicDir`,
235`outDir`, `trailingSlash`) merged *over* the config file, so a value
236set here wins:
237
238```typescript
239// alchemy.run.ts
240import { Stack } from "alchemy/Stack";
241
242export const Website = Cloudflare.Website.Astro(
243 "Website",
244 Stack.useSync(({ stage }) => ({
245 astro: {
246 site:
247 stage === "prod"
248 ? "https://blog.example.com"
249 : `https://${stage}.example.com`,
250 },
251 })),
252);
253```
254
255Use `config` to load an alternate config file (relative to
256`rootDir`) instead of astro's own `astro.config.*` discovery:
257
258```typescript
259export const Website = Cloudflare.Website.Astro("Website", {
260 config: "astro.deploy.config.ts",
261});
262```
263
264Two options are managed for you regardless of the config file:
265
266- `adapter` — Alchemy injects its wrangler-free Cloudflare adapter.
267 Declaring an adapter in `astro.config.*` fails the build with an
268 actionable error.
269- `output` — Alchemy defaults it to `"server"` (superseding a
270 file-level `output`), so pages render on demand in the Worker.
271 Opt into a fully prerendered site with `astro: { output: "static" }`
272 on the resource.