2 min read

Deploying Astro to Cloudflare Pages without surprises

The exact build settings, the Node version trap, and the five things that break on first deploy.

Abstract network diagram on a dark background

Astro outputs a plain dist/ folder, which is exactly what Cloudflare Pages wants. In theory the whole deployment is three fields. In practice there are a handful of things that trip people up on the first try.

The three fields

In the Cloudflare Pages setup wizard:

Field Value
Framework preset Astro
Build command npm run build
Build output directory dist

Leave the install command as npm install. Don’t add npm ci unless you’ve committed a lockfile — and you should commit the lockfile.

The Node version trap

This is the one that fails most often. Cloudflare’s default build image ships an older Node, and modern Astro requires Node 20+. The symptom is a build log that dies with a syntax error deep inside a dependency, which looks nothing like a version problem.

Fix it by adding an environment variable to the Pages project:

NODE_VERSION = 22

You can set it under Settings → Environment variables for both production and preview builds. A .nvmrc in the repo is a good second line of defence for local development.

Set site before you deploy

Astro needs an absolute origin to generate sitemap.xml and canonical URLs. If site is missing, the build warns and your sitemap ends up with relative URLs that search engines ignore.

// astro.config.mjs
export default defineConfig({
  site: 'https://your-domain.com',
  output: 'static',
});

Change this when you attach a custom domain, then redeploy. Stale canonical URLs are a slow, quiet SEO problem.

Static output means no adapter

If you see docs telling you to install @astrojs/cloudflare, check whether you actually need SSR. For a blog you don’t. output: 'static' needs no adapter at all, builds faster, and can be served from anywhere.

Add SSR only when you have a concrete reason — authenticated pages, per-request personalisation, a form that writes somewhere.

Cache headers

Cloudflare already caches static assets aggressively. If you want control, drop a _headers file into public/:

/assets/*
  Cache-Control: public, max-age=31536000, immutable

/*.html
  Cache-Control: public, max-age=0, must-revalidate

Long cache for hashed or versioned assets, no cache for HTML so edits show up immediately.

Preview deployments

Every branch and pull request gets its own URL automatically. Use it. It costs nothing and it means you never merge a broken build to find out about it from a reader.

astrocloudflaredeployment