p4ni.

Tutorial

Deploy an Astro Site to Cloudflare Workers (2026 Guide)

· Updated · 6 min read

On this page

For years, “deploy Astro to Cloudflare” meant Cloudflare Pages. That advice is now stale: Cloudflare has shifted its focus to Workers with static assets, recommends Workers for new projects, and is no longer bringing new features to Pages. The good news is that deploying a static Astro site to Workers is arguably simpler than Pages ever was — one config file, one command, and your site is on Cloudflare’s edge.

This is the exact setup I use for this blog and for my commercial Astro themes. It covers:

  1. Why Workers (and what “static assets” means)
  2. The minimal wrangler.jsonc for a static Astro site
  3. Deploying with wrangler deploy
  4. Attaching a custom domain
  5. Gotchas: 404 pages, trailing slashes, and when you actually need the Cloudflare adapter

Why Workers instead of Pages?

A quick decision table before we start:

You are building…Use
A new static Astro siteWorkers (this guide)
A new Astro site with SSR routesWorkers + @astrojs/cloudflare (see the last gotcha below)
An existing site already on PagesPages still works; migrate when convenient
A site that needs Cron Triggers, Queue consumers, or gradual deploymentsWorkers (Pages never got parity here)

“Static assets” is the Workers feature that made Pages redundant: a Worker can now ship a directory of files (your dist/) that Cloudflare serves directly from its CDN. For a fully static site you don’t write or pay for any Worker code at all — requests served from static assets are free and unlimited on every plan, including Free. The ceiling is on file count rather than traffic: 20,000 files on Free, 100,000 on paid, 25 MiB each. A blog reaches that only if it generates something per page — OG images, say — and even then not soon.

Step 0: An Astro site

Any static Astro project works. If you’re starting fresh:

pnpm create astro@latest my-site -- --template minimal
cd my-site

Astro is static by default — no adapter needed. pnpm build outputs plain HTML/CSS/JS to dist/. That’s our deployable artifact.

Step 1: Install Wrangler

Wrangler is Cloudflare’s CLI. Keep it as a dev dependency so your deploys are reproducible:

pnpm add -D wrangler

Step 2: Write wrangler.jsonc

Create wrangler.jsonc in your project root. This is the entire configuration for a static site:

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "my-site",
  "compatibility_date": "2026-07-27",
  "assets": {
    "directory": "./dist",
    "not_found_handling": "404-page"
  }
}

Three things worth understanding:

  • name becomes your Worker’s name and its free preview URL: my-site.<your-subdomain>.workers.dev.
  • assets.directory points at Astro’s build output. There is no main field — with no Worker script, Cloudflare serves the directory directly.
  • not_found_handling: "404-page" serves your dist/404.html (Astro builds one from src/pages/404.astro) with a proper 404 status. If you were deploying an SPA instead, you’d use "single-page-application" here.

Step 3: Deploy

Authenticate once, then deploy:

pnpm exec wrangler login
pnpm build
pnpm exec wrangler deploy

Wrangler uploads dist/, and a few seconds later your site is live at https://my-site.<your-subdomain>.workers.dev. Add a script to package.json so a deploy is always a fresh build:

{
  "scripts": {
    "deploy": "astro build && wrangler deploy"
  }
}

From now on, shipping is pnpm run deploy. Note the run: pnpm reserves the bare pnpm deploy for its own workspace command, so it will not execute your script. If typing run every time annoys you, name the script something pnpm doesn’t own — ship works.

Step 4: Custom domain

If your domain’s DNS is already on Cloudflare (i.e. the zone lives in the same account), you don’t need to touch the dashboard. Declare the domain in wrangler.jsonc:

{
  // …everything from before…
  "routes": [
    {
      "pattern": "blog.example.com",
      "custom_domain": true
    }
  ]
}

On the next wrangler deploy, Cloudflare creates the DNS record and provisions the TLS certificate automatically. The site you’re reading was attached to astro.p4ni.com exactly this way — one config block, one deploy.

Tell Astro its production URL, or your canonical URLs, sitemaps, and RSS links come out relative:

// astro.config.mjs
export default defineConfig({
  site: 'https://blog.example.com',
});

site also unlocks something worth doing while you’re here. Because Workers static assets serve any file in dist/, you can generate a per-page OG image at build time and ship it as a plain PNG — no serverless function, no runtime cost. I wrote up the full setup in Auto-Generate Open Graph Images in Astro with Satori. The same absolute URL site gives you is also what the JSON-LD on each page needs, so it’s worth setting before you write either.

Migrating a site that’s already on Pages

Nothing about your Astro source changes — Pages and Workers static assets serve the same dist/. What moves is the configuration:

  • Build output. Pages had pages_build_output_dir; Workers uses assets.directory. Same path, different key.
  • 404 behavior. Pages detected this for you. Workers wants it stated: not_found_handling, as above.
  • Environment variables. These don’t come along. Redeclare them in [vars] or push them with wrangler secret put. If you build in CI, note that Workers Builds keeps its own set of build-time variables rather than inheriting the Pages ones.
  • _headers and _redirects. Unchanged — see the gotcha below.

Two things to know before you cut over. Workers only serves custom domains whose nameservers Cloudflare manages, which is stricter than Pages — if you were pointing an externally-hosted domain at a Pages project, that setup doesn’t carry over. And treat the domain move as a cutover rather than a parallel run: deploy the Worker, verify it on its workers.dev URL, then move the hostname.

One local-dev detail that trips people up mid-migration: wrangler dev serves on port 8787, not the 8788 you were used to with wrangler pages dev.

Gotchas worth knowing

Trailing slashes. By default, static assets use “auto” HTML handling: /about/ serves about/index.html and /about 307-redirects to /about/. That matches Astro’s default directory-style output, so things just work — but keep your internal links consistent (I set trailingSlash: 'always' in astro.config.mjs so dev and prod behave identically).

_headers and _redirects. The Pages-style _headers and _redirects files work with Workers static assets too. Drop them in public/ and Astro copies them into dist/. Handy for cache headers on fonts or redirecting old URLs after a migration.

Preview before deploying. wrangler dev serves the built site locally under the same asset-serving rules as production — useful for checking redirect/404 behavior that astro preview approximates slightly differently.

When you do need the adapter. Everything above assumes a fully prerendered site. The moment you need SSR — personalization, form handling, an API route hitting D1 — add @astrojs/cloudflare. Your static pages still ship as free static assets; only server-rendered routes invoke the Worker. That hybrid setup is how my directory theme Almanac works: static browsing pages, with D1-backed search, submissions, and an admin panel on the Worker side. If you’re weighing a build like that, I compared the Astro directory themes worth using, free and paid; the data layer is the choice that decides whether you need any of this Worker-side machinery at all.

Recap

pnpm add -D wrangler                 # 1. CLI
# 2. wrangler.jsonc with assets.directory = ./dist
pnpm exec wrangler login             # 3. authenticate once
pnpm run deploy                      # 4. astro build && wrangler deploy

Static Astro on Workers gives you free, unmetered hosting on Cloudflare’s edge with a config file short enough to memorize — and a clean upgrade path to D1 and SSR when your site outgrows “just static.”