Deploying
Ship to Node, Cloudflare Pages, Netlify, or Vercel — and how the contact form's server action works on otherwise-static hosts.
Where you deploy depends on one question: does the site need server compute?
- The docs template is fully static (
output: 'static', no adapter, no server code).astro buildemitsdist/— drop it on any static host. - The starter ships a contact form built on Astro Actions, which run
server-side. It pins the
@astrojs/nodeadapter so that endpoint has somewhere to run. To deploy elsewhere, you swap the adapter to match the host.
Static hosts (docs, or starter without the form)
If there’s no server code, the build is portable:
pnpm build # → dist/ Point Cloudflare Pages, Netlify, GitHub Pages, or any CDN at dist/. Build
command pnpm build, output directory dist. Done.
Node
The starter’s default. Build, then run the standalone server entry:
pnpm build
node ./dist/server/entry.mjs Put it behind a reverse proxy (Caddy, nginx) or in a container. Set the email env vars (see Contact form & email) in the runtime environment.
Netlify / Vercel
To deploy the starter (with its server action) to one of these, swap the adapter. The change is two lines plus the dependency:
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify'; // or @astrojs/vercel
export default defineConfig({
output: 'static',
adapter: netlify(),
// …the rest is unchanged
}); pnpm add @astrojs/netlify # or @astrojs/vercel The adapter turns the action endpoint into a serverless / edge function at
build time. The static pages still deploy to the CDN; the function handles the
form POST. The same applies to Cloudflare Workers with
@astrojs/cloudflare — but not to Cloudflare Pages; see below.
Cloudflare Pages
Don’t reach for @astrojs/cloudflare here — the current adapter emits a
Worker build that Pages can’t run. The pattern that works (and the one
astro-ignite’s own marketing site ships with) is a fully static build plus a
hand-written Pages Function:
- Keep
output: 'static'and no adapter.pnpm buildemits plaindist/; Pages serves it from the CDN. - Write the form handler as a Pages Function at
functions/api/contact.ts— anonRequestPostthat validates the fields, checks the honeypot, and talks to your email provider’s HTTP API, reading secrets from the function’senvbinding (set them as encrypted variables on the Pages project). - Repoint the contact
<form>at/api/contactand drop the Astro Action. Cloudflare deploys thefunctions/directory alongsidedist/automatically.
For a complete, copyable handler, see
apps/site/functions/api/contact.ts
in the astro-ignite repo — that file is exactly this pattern in production.
The contact form on a purely static host
If you want a static-only deploy (no functions at all), the server action can’t run. Two clean options:
- Use the host’s function layer directly. On Netlify and Vercel the adapter compiles the action into a serverless function — no extra work. On Cloudflare Pages, write the small Pages Function by hand (see above). Either way the form stays server-side, owned by your code.
- Hand the form off. Repoint the
<form>at a third-party form endpoint (Formspree, Web3Forms, your own webhook) and drop the action. You lose the typed Zod validation and the honeypot, but the deploy is pure static.
Before you ship
- Set
siteConfig.urlto your production origin — it drives canonical URLs, the sitemap, OG tags, androbots.txt. - Set the email + analytics env vars in the host’s dashboard, not in the repo.
- Re-run
pnpm buildlocally first; it surfaces schema and type errors the host would otherwise fail on.