1---
2title: Static sites
3description: Deploy any build command's output directory as Cloudflare Worker static assets with Cloudflare.Website.StaticSite — custom edge Workers, framework-native local dev, and memoized rebuilds.
4sidebar:
5 order: 3
6---
7
8`Cloudflare.Website.StaticSite` runs a build command, content-hashes the
9output directory, and deploys it as a Cloudflare Worker serving static
10assets. The build is a plain shell command (a
11[`Command.Build`](/providers/command/build) resource under the hood — see
12[Memoization](/command/memoization) for what gets hashed and when it
13re-runs), so it works for anything that produces a directory of files — a
14Zola or Hugo site, a bash script, or the static output of a framework the
15[Vite resource](/cloudflare/frontend/vite) doesn't support yet.
16
17If you provide neither `main` nor `script`, the site deploys as an
18**assets-only Worker**: no Worker code is uploaded at all. Cloudflare's
19asset layer serves every request itself — static asset requests are free
20and never invoke a Worker — and applies your `notFoundHandling` /
21`htmlHandling` config, exactly like an assets-only `wrangler deploy`.
22Provide `main` to put a real Worker in front instead (see
23[below](#a-custom-worker-in-front-of-your-assets)).
24
25If your files need no build step at all, you can skip `StaticSite` and
26give a plain [Worker](/cloudflare/compute/workers) just an assets
27directory:
28
29```typescript
30const site = yield* Cloudflare.Worker("Site", {
31 assets: {
32 directory: "./public",
33 htmlHandling: "drop-trailing-slash",
34 },
35 domain: "static.example.com",
36});
37```
38
39## Deploy a Zola site
40
41Point `command` at the build, `outdir` at where it writes files, and return
42the Worker's `url` as a stack output:
43
44```typescript
45// alchemy.run.ts
46import * as Alchemy from "alchemy";
47import * as Cloudflare from "alchemy/Cloudflare";
48import * as Effect from "effect/Effect";
49
50export default Alchemy.Stack(
51 "CloudflareStatic",
52 {
53 providers: Cloudflare.providers(),
54 state: Cloudflare.state(),
55 },
56 Effect.gen(function* () {
57 const site = yield* Cloudflare.Website.StaticSite("Website", {
58 command: "zola build",
59 outdir: "public",
60 dev: {
61 command: "zola serve",
62 },
63 assets: {
64 notFoundHandling: "404-page",
65 },
66 });
67
68 return { url: site.url };
69 }),
70);
71```
72
73`alchemy deploy` runs `zola build`, hashes `public/`, and uploads it as the
74Worker's static assets; `notFoundHandling: "404-page"` serves Zola's
75generated 404 page for unmatched paths. The runnable version lives at
76[`examples/cloudflare-static-site`](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-static-site).
77
78## Props
79
80The build contract is two required props, plus a handful of optional ones:
81
82- `command` — the shell command that builds the site (e.g. `"zola build"`).
83- `outdir` — the directory the command writes, relative to the working
84 directory. This is what gets deployed.
85- `cwd` — working directory for the command. Defaults to `process.cwd()`.
86- `env` — extra environment variables passed to the build command on top of
87 `process.env`, so outputs from other resources (like an API URL) flow into
88 the build. `Redacted` values stay out of logs and state.
89- `memo` — `MemoOptions | boolean`. By default every non-gitignored file in
90 `cwd` (plus the nearest lockfile) is hashed to decide whether the build
91 re-runs; `memo: { include: [...] }` narrows the scope, and `memo: false`
92 disables memoization so the build runs on every deploy.
93- `main` — an optional custom Worker entrypoint served in front of the
94 assets, instead of the fallback passthrough.
95- `assets` — asset routing behavior (`notFoundHandling`, `htmlHandling`,
96 `runWorkerFirst`, ...), passed flat. `runWorkerFirst` accepts `true` to
97 route every request through the Worker ahead of assets, or a glob array
98 like `["/api/*"]` to intercept only matching paths.
99
100Everything else a [Worker](/cloudflare/compute/workers) accepts —
101`domain`, `compatibility`, bindings via `env` — applies too, since a
102`StaticSite` is a Worker underneath.
103
104## A custom Worker in front of your assets
105
106The alchemy.run docs site itself — an Astro/Starlight app — deploys this
107way, with a hand-written Worker at the edge:
108
109```typescript
110// website/alchemy.run.ts — how alchemy.run deploys itself
111const Website = Cloudflare.Website.StaticSite(
112 "Website",
113 Alchemy.Stack.useSync((stack) => ({
114 command: "bun run build",
115 outdir: "dist",
116 main: "./src/worker.ts",
117 domains: stack.stage === "prod" ? ["alchemy.run"] : undefined,
118 memo: {
119 include: [
120 "src/**",
121 "astro.config.mjs",
122 "package.json",
123 "plugins/**",
124 "public/**",
125 "scripts/**",
126 "../bun.lock",
127 ],
128 },
129 assets: {
130 runWorkerFirst: true,
131 },
132 })),
133);
134```
135
136`main` points at a Worker that runs *before* asset serving thanks to
137`assets: { runWorkerFirst: true }` — the docs site uses it to rewrite OG and
138canonical tags with `HTMLRewriter` and serve 301 redirects, then delegates
139everything else to `env.ASSETS.fetch(request)`.
140
141## Local dev
142
143During `alchemy dev`, the `dev` prop replaces the build with your
144framework's own dev server:
145
146```typescript
147const site = yield* Cloudflare.Website.StaticSite("Website", {
148 command: "zola build",
149 outdir: "public",
150 dev: {
151 command: "zola serve",
152 },
153});
154```
155
156`dev.command` is spawned as a long-lived sidecar process tied to the stack's
157scope — the build is skipped and the Worker runs in external mode against
158the dev server's URL, detected from the command's stdout. `dev` also accepts
159`cwd` and `env` overrides for the dev process, and `dev.url` to pin the URL
160explicitly when stdout detection fails. The sidecar mechanics — spawn, URL
161detection, teardown — are covered in [Dev servers](/command/dev-servers).
162
163## When to use a framework resource instead
164
165`StaticSite` is the general fallback for any build command that produces a
166directory of files. Frameworks with dedicated resources have a better path:
167[Vite](/cloudflare/frontend/vite) for pure-Vite apps,
168[Astro](/cloudflare/frontend/astro),
169[Nuxt](/cloudflare/frontend/nuxt),
170[SvelteKit](/cloudflare/frontend/sveltekit), and
171[Waku](/cloudflare/frontend/waku) for their frameworks. Those resources run
172the framework's own programmatic build, deploy server-rendered routes, and
173skip the build contract entirely. Reach for `StaticSite` when there is no
174dedicated resource — Zola, Hugo, or any other generator.