Static sites
Fly.Website.StaticSite runs a build command, content-hashes the
output directory, and deploys a Service
that serves those files (plus GET /health) from a tiny Node
static-file server. Use it when the site has its own build step —
Hugo, Zola, Eleventy, or any custom pipeline.
For Vite-based projects, prefer Fly.Website.Vite.
Declare the Website
Section titled “Declare the Website”build.command and build.output are required. Declare the site
as a module-level const:
import * as Fly from "alchemy/Fly";
export const Website = Fly.Website.StaticSite("Website", { build: { command: "hugo --minify", output: "public" },});The build is a Command.Build —
memoized, so unchanged inputs skip the rebuild
(details). By default every non-gitignored
file in path (plus the nearest lockfile) is hashed. Narrow it with
memo: { include: [...] }, or set memo: false to rebuild on every
deploy.
Add it to the Stack
Section titled “Add it to the Stack”Yield the site from your Stack and return its URL:
import * as Alchemy from "alchemy";import * as Effect from "effect/Effect";
export default Alchemy.Stack( "MyStaticSite", { providers: Fly.providers(), state: Alchemy.localState(), }, Effect.gen(function* () { const site = yield* Website; return { url: site.url }; }),);One StaticSite expands into a Fly App
(created if you omit app), a Service running Node on a Machine
(port 3000), and a shared IPv4 so https://{app}.fly.dev answers.
Built files are packed at /app/dist. site.url is that fly.dev
URL (or https://{domain} when you pass domain). app,
service, ip, and certificate are undefined during
alchemy dev.
Pass app to put the site on an App you already declared:
export const Site = Fly.App("Site");
export const Website = Fly.Website.StaticSite("Website", { build: { command: "hugo --minify", output: "public" }, app: Site,});Building from a subdirectory
Section titled “Building from a subdirectory”path is the working directory for build.command. build.output
is relative to path (default: process cwd):
export const Website = Fly.Website.StaticSite("Web", { path: "apps/web", build: { command: "npm run build", output: "dist" },});SPAs and 404 pages
Section titled “SPAs and 404 pages”A miss — a request that matches no output file — can be answered two ways, and they are mutually exclusive:
// Single-page app: misses serve index.html with a 200 so the// client-side router takes over.export const App = Fly.Website.StaticSite("App", { build: { command: "npm run build", output: "dist" }, assets: { notFoundHandling: "single-page-application" },});
// Static site: misses return a real 404 status with your error page.export const Docs = Fly.Website.StaticSite("Docs", { build: { command: "hugo --minify", output: "public" }, assets: { notFoundHandling: "404-page" },});Add environment variables
Section titled “Add environment variables”Top-level env is copied onto process.env for the build and onto
the Machine at deploy. Wrap secrets with Redacted. It is not
Worker bindings and not AWS server.environment:
export const Website = Fly.Website.StaticSite("Website", { build: { command: "hugo --minify", output: "public" }, env: { HUGO_ENV: "production", },});Generators that inline at build time pick these up. The hosted process is the generated static-file server — there is no framework SSR runtime.
Local development
Section titled “Local development”During alchemy dev, dev.command replaces the build with that
process as a long-lived child. No Fly App, Service, IP, or
certificate is created, and site.url is the local address
detected from stdout (dev.url pins it when detection fails).
dev also accepts cwd and env overrides:
export const Website = Fly.Website.StaticSite("Website", { build: { command: "hugo --minify", output: "public" }, dev: { command: "hugo server" },});Without dev.command, the site still builds and a local static
server serves outdir — still no cloud resources. Wrap the site in
Alchemy.remote() to deploy the live path even during dev:
export const Website = Fly.Website.StaticSite("Website", { build: { command: "hugo --minify", output: "public" },}).pipe(Alchemy.remote());Custom domain
Section titled “Custom domain”domain is a hostname string. Alchemy requests ACME
(Fly.Certificate) on the App and
url becomes https://{domain}. Point DNS at the App yourself —
v1 does not create records:
export const Website = Fly.Website.StaticSite("Website", { build: { command: "hugo --minify", output: "public" }, domain: "blog.example.com",});See IPs & certificates for A/AAAA records and the ACME challenge.
When to use a framework resource instead
Section titled “When to use a framework resource instead”StaticSite is the general fallback for any shell command that
produces a directory of files. Frameworks with dedicated resources
have a better path: Astro,
Foldkit, Next.js,
Nuxt, Octane,
React Router,
SolidStart,
SvelteKit,
TanStack Start,
Vite,
Vocs, and Waku. Those
resources run the framework’s own programmatic build and skip the
build.command / build.output contract. Reach for StaticSite
when there is no dedicated resource — Zola, Hugo, or any other
generator.
Where next
Section titled “Where next”- Websites on Fly — the full websites surface, including Node SSR frameworks
- StaticSite reference — every prop and attribute
- Fly on Alchemy — the Fly provider hub