Astro
Cloudflare.Website.Astro deploys an Astro
project as a Cloudflare Worker. It runs Astro’s programmatic build
with a wrangler-free Cloudflare adapter: server-rendered pages
execute in the Worker, prerendered pages and client assets deploy as
static assets. Your astro.config.* loads natively — there is no
adapter to install into it and no Wrangler file to write.
Install
Section titled “Install”The build integration is not bundled with alchemy. Install
@alchemy.run/frontend-frameworks; the resource loads its /astro
export from your project at deploy time. It is only used at build time,
so a dev dependency is enough:
bun add -d @alchemy.run/frontend-frameworksnpm install -D @alchemy.run/frontend-frameworkspnpm add -D @alchemy.run/frontend-frameworksyarn add -D @alchemy.run/frontend-frameworksConfigure Astro
Section titled “Configure Astro”Your astro.config.* file loads natively — integrations, Vite
plugins, and any other non-serializable options work exactly as they
do outside Alchemy. Alchemy merges a programmatic config over it:
the Cloudflare adapter is injected for you (don’t declare an
adapter in your config — that fails the build with an actionable
error), and options passed via the astro prop override the file’s —
see Astro configuration below for the
details.
Declare the Website
Section titled “Declare the Website”Declare the site as a module-level const (rather than inline in the Stack) and derive the typed shape of its bindings from it:
import * as Cloudflare from "alchemy/Cloudflare";
export const Website = Cloudflare.Website.Astro("Website");
export type WebsiteEnv = Cloudflare.InferEnv<typeof Website>;WebsiteEnv is the typed shape of the Worker’s bindings, derived
from the class — you’ll import it into your Astro code in
Read bindings in server code.
Add it to the Stack
Section titled “Add it to the Stack”Yield the class from your Stack and return its URL:
import * as Alchemy from "alchemy";import * as Effect from "effect/Effect";
export default Alchemy.Stack( "MyAstroSite", { providers: Cloudflare.providers(), state: Cloudflare.state(), }, Effect.gen(function* () { const site = yield* Website; return { url: site.url }; }),);Pages are server-rendered by default. Pages that
export const prerender = true are served as static assets. Astro’s
server runtime is built against Node APIs, so the nodejs_compat
compatibility flag is added automatically — you don’t need to pass
it.
During alchemy dev, Astro’s dev server runs with SSR executing in
workerd, so server code sees the same runtime and the same real
bindings it will have in production.
See examples/cloudflare-website-astro for the checked-in example.
Add bindings
Section titled “Add bindings”The resource returns a plain Worker, so env accepts the full
binding vocabulary — KV namespaces, R2 buckets, Durable Objects,
secrets:
import * as Config from "effect/Config";
export const Cache = Cloudflare.KV.Namespace("Cache");
export const Website = Cloudflare.Website.Astro("Website", { env: { CACHE: Cache, API_KEY: Config.redacted("API_KEY"), },});Cache is a description, not a deploy — Alchemy provisions the real
namespace because the Website binds it. Config.redacted reads
API_KEY from your environment at deploy time and binds it as a
Worker secret — see Secrets & env.
Read bindings in server code
Section titled “Read bindings in server code”Astro code reads bindings via Astro.locals.runtime.env (or
import { env } from "cloudflare:workers"). Type the locals once by
augmenting App.Locals with the inferred env type:
import type { WebsiteEnv } from "../alchemy.run.ts";
declare global { namespace App { interface Locals { runtime: { env: WebsiteEnv }; } }}
export {};Every server-rendered page and endpoint now gets fully typed bindings:
---const cached = await Astro.locals.runtime.env.CACHE.get("greeting");---
<h1>{cached ?? "hello"}</h1>Astro.locals.runtime.env.CACHE is typed as a KV namespace and
.API_KEY as a string — renaming a binding in alchemy.run.ts is a
type error in your pages.
Sessions
Section titled “Sessions”Astro’s session API is backed by a KV namespace. One is provisioned
and bound as SESSION automatically, so Astro.session works with
zero configuration.
Bind your own namespace under that name to use it instead:
export const Sessions = Cloudflare.KV.Namespace("Sessions");
export const Website = Cloudflare.Website.Astro("Website", { env: { SESSION: Sessions, },});Or opt out of session provisioning entirely:
export const Website = Cloudflare.Website.Astro("Website", { sessionKVBindingName: false,});Fully static sites
Section titled “Fully static sites”With astro: { output: "static" } every page is prerendered at
build time and the deploy is assets-only — no server bundle is
uploaded, and Cloudflare’s asset layer answers every request:
export const Website = Cloudflare.Website.Astro("Website", { astro: { output: "static" }, assets: { notFoundHandling: "404-page" },});notFoundHandling: "404-page" serves the built 404.html for
unmatched routes. Session provisioning is skipped for declared-static
sites, since no Worker code runs at request time.
Astro configuration
Section titled “Astro configuration”Your astro.config.* is the primary place to configure Astro —
integrations, Vite plugins, and every other option (serializable or
not) work as usual:
import { defineConfig } from "astro/config";import react from "@astrojs/react";import tailwindcss from "@tailwindcss/vite";
export default defineConfig({ site: "https://blog.example.com", integrations: [react()], vite: { plugins: [tailwindcss()], },});A config file can’t consume Alchemy Outputs or vary by stage. For
those values the astro prop is a deploy-time override bag — a
serializable subset (site, base, output, srcDir, publicDir,
outDir, trailingSlash) merged over the config file, so a value
set here wins:
import { Stack } from "alchemy/Stack";
export const Website = Cloudflare.Website.Astro( "Website", Stack.useSync(({ stage }) => ({ astro: { site: stage === "prod" ? "https://blog.example.com" : `https://${stage}.example.com`, }, })),);Use config to load an alternate config file (relative to
rootDir) instead of astro’s own astro.config.* discovery:
export const Website = Cloudflare.Website.Astro("Website", { config: "astro.deploy.config.ts",});Two options are managed for you regardless of the config file:
adapter— Alchemy injects its wrangler-free Cloudflare adapter. Declaring an adapter inastro.config.*fails the build with an actionable error.output— Alchemy defaults it to"server"(superseding a file-leveloutput), so pages render on demand in the Worker. Opt into a fully prerendered site withastro: { output: "static" }on the resource.