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:
- Why Workers (and what “static assets” means)
- The minimal
wrangler.jsoncfor a static Astro site - Deploying with
wrangler deploy - Attaching a custom domain
- 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 site | Workers (this guide) |
| A new Astro site with SSR routes | Workers + @astrojs/cloudflare (see the last gotcha below) |
| An existing site already on Pages | Pages still works; migrate when convenient |
| A site that needs Cron Triggers, Queue consumers, or gradual deployments | Workers (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:
namebecomes your Worker’s name and its free preview URL:my-site.<your-subdomain>.workers.dev.assets.directorypoints at Astro’s build output. There is nomainfield — with no Worker script, Cloudflare serves the directory directly.not_found_handling: "404-page"serves yourdist/404.html(Astro builds one fromsrc/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 usesassets.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 withwrangler 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. _headersand_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.”