1---
2title: Static sites
3description: Ship a static site to S3 + CloudFront with AWS.Website.StaticSite — build-step support, Router composition, and cache invalidation on deploy.
4sidebar:
5 order: 4
6---
7
8`AWS.Website.StaticSite` deploys a directory of files as a website: it uploads
9the files to a private S3 bucket, creates a CloudFront distribution with a
10CloudFront Function that routes requests at the edge via a KeyValueStore file
11manifest, and invalidates the cache when your assets change. Use it whenever
12the output is plain files — docs, marketing pages, or a built SPA.
13
14## Deploy a directory
15
16Point `path` at a directory of files and return the site's `url` as a stack
17output:
18
19```typescript
20// alchemy.run.ts
21import * as Alchemy from "alchemy";
22import * as AWS from "alchemy/AWS";
23import * as Effect from "effect/Effect";
24
25export default Alchemy.Stack(
26 "MyStaticSite",
27 {
28 providers: AWS.providers(),
29 state: AWS.state(),
30 },
31 Effect.gen(function* () {
32 const site = yield* AWS.Website.StaticSite("MarketingSite", {
33 path: "./site",
34 forceDestroy: true,
35 });
36
37 return {
38 url: site.url,
39 };
40 }),
41);
42```
43
44One `StaticSite` expands into the whole stack — an S3 bucket, the asset
45upload, a CloudFront KeyValueStore holding the file manifest, a viewer-request
46CloudFront Function, and the distribution itself. `forceDestroy: true` lets
47`destroy` empty the bucket before deleting it; `site.url` resolves to the
48CloudFront domain (or `https://<your-domain>` when you pass `domain`).
49
50```sh
51bun alchemy deploy
52```
53
54The deployed example lives at
55[`examples/aws-static-site`](https://github.com/alchemy-run/alchemy/tree/main/examples/aws-static-site)
56if you want a runnable starting point.
57
58## Run a build step first
59
60For framework sites (Vite, Astro, etc.), add `build` so the framework's build
61runs before upload and the `output` directory is what gets deployed:
62
63```typescript
64const site = yield* AWS.Website.StaticSite("Web", {
65 path: "./frontend",
66 build: {
67 command: "bun run build",
68 output: "dist",
69 env: {
70 VITE_API_URL: api.url,
71 },
72 },
73});
74```
75
76The build step is a [Command.Build](/providers/command/build) resource —
77memoized, so unchanged inputs skip the build
78([details](/command/memoization)). The input hash covers, by default, all
79files under `path` (filtered by your gitignore rules) plus the nearest
80package-manager lockfile. Tune the hash with `build.include`, `build.exclude`, and
81`build.lockfile` — e.g. `include: ["src/**", "package.json"]` to re-run only
82when source files change. `build.env` variables are passed to the build
83command, so outputs from other resources (like an API URL) flow straight into
84the frontend bundle.
85
86## SPAs and 404 pages
87
88A miss — a request that matches no uploaded file — can be answered two ways,
89and they are mutually exclusive:
90
91```typescript
92// Single-page app: misses serve index.html with a 200 so the
93// client-side router takes over.
94const app = yield* AWS.Website.StaticSite("App", {
95 path: "./app",
96 build: { command: "bun run build", output: "dist" },
97 spa: true,
98});
99
100// Static site: misses return a real 404 status with your error page.
101const docs = yield* AWS.Website.StaticSite("Docs", {
102 path: "./docs/dist",
103 errorPage: "404.html",
104});
105```
106
107Pretty URLs work in both modes: `/about` serves `about.html` or
108`about/index.html` when one was uploaded, resolved at the edge from the file
109manifest.
110
111## Compose sites with a Router
112
113`AWS.Website.Router` is a shared CloudFront front door: one distribution whose
114routes live in a KeyValueStore, matched at the edge by a CloudFront Function.
115Create the router, then attach sites to it instead of giving each site its own
116distribution:
117
118```typescript
119const router = yield* AWS.Website.Router("WebsiteRouter", {
120 // hosted zone inferred from the hostname; pass hostedZoneId to pin it
121 domain: { name: "example.com" },
122});
123
124const site = yield* AWS.Website.StaticSite("Docs", {
125 path: "./docs",
126 domain: {
127 router,
128 path: "/docs",
129 },
130});
131```
132
133The site registers itself by writing its file manifest and metadata into the
134router's KV store — no distribution redeploy is needed to add or remove a
135site. In router mode, `domain.name` is an optional host pattern (exact
136hostname or `*.example.com`) matched at the edge; the hostname must also be
137covered by the Router's own `domain`. `edge` and `cloudfrontUrl` live on the
138`Router` — setting them on an attached site is an error.
139
140## Route to non-site origins
141
142The router also takes inline `routes` for origins that aren't managed by
143`StaticSite`, keyed by path pattern:
144
145```typescript
146const router = yield* AWS.Website.Router("WebsiteRouter", {
147 routes: {
148 "/*": { url: api.functionUrl },
149 },
150});
151```
152
153A route value can be a URL (proxied as a custom origin) or a
154`{ bucket }` route serving straight from S3, optionally with a
155`rewrite: { regex, to }` applied to the URI before forwarding. Longest
156host-then-path match wins, so a `StaticSite` mounted at `/docs` takes
157precedence over a `/*` fallback route.
158
159## Cache invalidation on deploy
160
161By default every deploy that changes your files creates a CloudFront
162invalidation for `/*` and moves on without waiting
163(`{ paths: "all", wait: false }`). The invalidation is keyed to a content hash
164of the uploaded assets, so deploys that change nothing don't invalidate
165anything. Tune it with `invalidation`:
166
167```typescript
168const site = yield* AWS.Website.StaticSite("MarketingSite", {
169 path: "./site",
170 invalidation: {
171 paths: "all",
172 wait: true,
173 },
174});
175```
176
177- `paths: "all"` — invalidate `/*` (the default)
178- `paths: "versioned"` — invalidate only the index page, for sites whose
179 assets are content-hashed and immutable
180- `paths: [...]` — an explicit list of paths
181- `wait: true` — block the deploy until CloudFront reports the invalidation
182 complete
183- `invalidation: false` — skip invalidation entirely
184
185## Local dev
186
187During `alchemy dev`, the `dev` prop replaces the build (and the whole
188S3 + CloudFront deployment) with your framework's own dev server:
189
190```typescript
191const site = yield* AWS.Website.StaticSite("Web", {
192 path: "./frontend",
193 build: { command: "bun run build", output: "dist" },
194 dev: { command: "bun run dev" },
195});
196```
197
198`dev.command` is spawned as a long-lived sidecar process tied to the
199stack's scope — the build is skipped, no AWS resources are created, and
200`site.url` is the dev server's local address, detected from the
201command's stdout. `dev` also accepts `cwd` and `env` overrides for the
202dev process, and `dev.url` to pin the URL explicitly when stdout
203detection fails. Wrap the site in `Alchemy.remote()` to deploy the real
204infrastructure even during dev.
205
206## When to use a framework resource instead
207
208`StaticSite` is the general fallback for any directory of files (with or
209without a build command). Frameworks with dedicated resources have a
210better path: [Astro](/aws/frontend/astro),
211[Next.js](/aws/frontend/nextjs), [Nuxt](/aws/frontend/nuxt),
212[SvelteKit](/aws/frontend/sveltekit), and [Waku](/aws/frontend/waku)
213run the framework's own programmatic build, deploy server-rendered
214routes on a streaming Lambda, and skip the build contract entirely.
215Reach for `StaticSite` when there is no dedicated resource — Zola,
216Hugo, or any other generator, or a pre-built SPA.
217
218## Where next
219
220- [Websites on AWS](/aws/frontend/websites) — the full websites surface, including
221 server-side rendering
222- [StaticSite reference](/providers/aws/website/staticsite) and
223 [Router reference](/providers/aws/website/router) — every prop and attribute
224- [AWS on Alchemy](/aws) — the AWS provider hub