Skip to content

Astro

AWS.Website.Astro deploys an Astro project to AWS. It runs Astro’s programmatic build with a wrangler-free AWS Lambda adapter: server-rendered pages stream from a Lambda Function URL, prerendered pages and client assets are served from a private S3 bucket through CloudFront, routed at the edge. Your astro.config.* loads natively — there is no adapter to install into it and no CloudFormation to write.

The build integration is not bundled with alchemy. Install @alchemy.run/frontend-frameworks; the resource loads its /astro and /astro/aws exports from your project at deploy time. It is only used at build time, so a dev dependency is enough:

Terminal window
bun add -d @alchemy.run/frontend-frameworks

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 AWS Lambda 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 site as a module-level const (rather than inline in the Stack):

alchemy.run.ts
import * as AWS from "alchemy/AWS";
export const Website = AWS.Website.Astro("Website");

Yield the site from your Stack and return its URL:

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Effect from "effect/Effect";
export default Alchemy.Stack(
"MyAstroSite",
{
providers: AWS.providers(),
state: AWS.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 uploaded to S3 and served from the edge without invoking the server.

See examples/aws-website-astro for the checked-in example.

The server function’s environment is configured under server.environment — plain values, or outputs from other resources in the Stack:

alchemy.run.ts
export const Website = AWS.Website.Astro("Website", {
server: {
environment: {
GREETING: "Hello from Alchemy!",
API_BASE: api.url,
},
},
});

The values are set on the Lambda at deploy time and injected into the dev server’s process environment under alchemy dev, so server code reads the same values in both modes.

The Astro server runs in a plain Node Lambda, so server-rendered pages and endpoints read the environment from process.env:

src/pages/index.astro
---
const greeting = process.env.GREETING ?? "hello";
---
<h1>{greeting}</h1>

With astro: { output: "static" } every page is prerendered at build time and the deploy is assets-only — no Lambda is created, and the S3 + CloudFront asset layer answers every request:

export const Website = AWS.Website.Astro("Website", {
astro: { output: "static" },
errorPage: "404.html",
});

errorPage serves the built 404.html for unmatched routes; spa answers misses with the index page (200) instead.

Your astro.config.* is the primary place to configure Astro — integrations, Vite plugins, and every other option (serializable or not) work as usual:

astro.config.ts
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()],
},
});

The astro prop on the resource exposes common serializable options for deploy-specific overrides; Astro merges them over the config file (scalars override, arrays like integrations and vite.plugins concatenate after the file’s):

alchemy.run.ts
export const Website = AWS.Website.Astro("Website", {
astro: {
site: "https://preview.example.com",
},
});

Two options are managed for you regardless of the config file:

  • adapter — Alchemy injects its wrangler-free AWS Lambda adapter. Declaring an adapter in astro.config.* fails the build with an actionable error.
  • output — Alchemy defaults it to "server" (superseding a file-level output), so pages render on demand in the Lambda. Opt into a fully prerendered site with astro: { output: "static" } on the resource.

alchemy dev runs Astro’s own dev server (native HMR) instead of deploying; site.url is the local address and no AWS resources are created. Wrap the site in Alchemy.remote() to deploy the real infrastructure even during dev.

const site = yield* AWS.Website.Astro("Web", {
domain: {
name: "app.example.com",
hostedZoneId: zone.hostedZoneId,
},
});