Methodology

Cloudflare Workers Static Assets Deployment Recipe — wrangler config, assets binding, single-command deploy

Copy-pasteable recipe for deploying an Astro 6 site to Cloudflare Workers Static Assets. The wrangler.toml configuration, the assets binding, the build-and-deploy command chain, the cache headers, and the post-deploy verification checklist.

Your deploy target should be the most boring part of the entire build. One config file, one command, assets served from the edge nearest every visitor — and here is the exact wiring, copy-pasteable.

This is the deploy half of the AI-native stack from AI-Native Website in 4 Weeks: Astro 6 builds a static dist/, and Cloudflare Workers Static Assets serves it from the edge with a thin Worker in front for the handful of edge-side concerns a pure static host cannot cover. Static Assets shipped in 2024 and replaced Cloudflare Pages for new builds — one runtime, one deploy, one config file, no seam between "static host" and "edge compute." Everything below assumes your Astro build already produces a clean dist/; this is the Cloudflare side.

The wrangler.toml

About thirty lines. The canonical pattern:

name = "nbm-site-v2"
main = "src/index.ts"
compatibility_date = "2026-05-15"
compatibility_flags = ["nodejs_compat"]
account_id = "your-cloudflare-account-id"
workers_dev = false

[assets]
directory = "./dist"
binding = "ASSETS"
not_found_handling = "404-page"
html_handling = "auto-trailing-slash"

[[routes]]
pattern = "nobrainermedia.com/*"
custom_domain = true
zone_name = "nobrainermedia.com"

[[routes]]
pattern = "www.nobrainermedia.com/*"
custom_domain = true
zone_name = "nobrainermedia.com"

[observability]
enabled = true
head_sampling_rate = 1.0

[vars]
ENVIRONMENT = "production"

Six choices earn naming:

  • The [assets] block is the Static Assets binding. directory points at the Astro output; binding = "ASSETS" is what the Worker references (env.ASSETS.fetch(request)); not_found_handling = "404-page" serves dist/404.html on misses; html_handling = "auto-trailing-slash" resolves both /about and /about/ to the same canonical content.
  • compatibility_date pins the runtime to a tested Cloudflare release — do not auto-track latest.
  • main declares the Worker entry point. Pure static sites can omit it, but most production sites want at least one edge-side concern, so the entry point is standard.
  • [[routes]] binds the Worker to your custom domains — two here, because the site canonicalizes www → non-www at the edge.
  • workers_dev = false disables the default *.workers.dev URL so it never leaks into search results.
  • [observability] turns on the per-request log surface for Logpush.

The Worker entry point

About thirty lines of TypeScript handling what the pure Static Assets surface cannot:

type Env = {
  ASSETS: Fetcher
  ENVIRONMENT: string
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url)

    // Canonicalize www → non-www
    if (url.hostname === 'www.nobrainermedia.com') {
      url.hostname = 'nobrainermedia.com'
      return Response.redirect(url.toString(), 301)
    }

    // Delegate everything else to the Static Assets binding
    return env.ASSETS.fetch(request)
  },
}

Three patterns: www canonicalization at the Worker boundary (a 301 in code scales without the per-domain page-rule quota); the binding handles every 200 (the Worker is a thin shim — insert A/B, geo-routing, or consent-aware analytics before the final env.ASSETS.fetch); and environment-typed bindings (the Env type catches binding-name typos at compile time, not deploy time).

The _headers and _redirects files

Two convention files in public/ carry into dist/ and configure the runtime. dist/_headers sets cache-control and security headers per pattern:

/*
  Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: geolocation=(), microphone=(), camera=()

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

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

Fingerprinted assets (/_astro/…) get a one-year immutable cache — the filename hash invalidates it on change; HTML gets a 5-minute must-revalidate cache, balancing freshness against edge efficiency; security headers (HSTS, nosniff, Referrer-Policy, Permissions-Policy) apply globally. dist/_redirects carries the redirect map from the Legacy CMS Migration Playbook, emitted at build time from the URL inventory, never hand-edited:

/old-services/ /services/ 301
/blog/category/seo/ /tag/seo/ 301
/wp-admin/* /404 410

Want this stack deployed correctly the first time? Talk to the team that runs it. →

The deploy — one shot from CI

Install, build, deploy:

# .github/workflows/deploy.yml
name: Deploy Production
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
      - uses: actions/setup-node@v4
        with:
          node-version: '24'
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm build
      - uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          command: deploy

pnpm build produces dist/; wrangler deploy uploads it and updates the Worker. End-to-end is 30–90 seconds depending on bundle size. Scope the CLOUDFLARE_API_TOKEN to deploy-only (Workers Scripts: Edit, Workers Routes: Edit, Account: Read) — never a full account token. Rollback is one command: wrangler rollback flips the active pointer to the retained previous deploy.

Post-deploy verification

Four checks before you call it done:

  • Canonical serving. curl -I https://nobrainermedia.com/ returns 200 with the expected headers; the www form returns 301 to canonical.
  • 304 on repeat. Capture the etag, re-request with if-none-match — a 304 Not Modified confirms the edge honors conditional requests.
  • Edge propagation. The Cloudflare request map should show every region on the new version within five minutes; slower means a Cloudflare-side incident.
  • 404 handling. A missing path returns 404 with your dist/404.html body — a default Cloudflare error page means not_found_handling is misconfigured.

The four failure modes

  • Misconfigured [assets] binding. directory must point at ./dist, not ./public. Verify Astro's outDir matches.
  • Stale compatibility_date. Pinning more than ~6 months back risks runtime regressions on date-gated changes. Refresh after each tested Cloudflare release.
  • Custom-domain DNS lag. The first deploy against a new domain takes 30–120 seconds to propagate; let custom_domain = true provision DNS, wait it out, verify with dig.
  • Redirect loops. A /about/ → /about rule plus auto-trailing-slash can loop infinitely. Pick one canonical form and reserve _redirects for legacy URLs, not canonical-form enforcement.

Closing

Workers Static Assets is the deploy target that lets the AI-native stack live up to its claims — sub-1.5-second LCP from the nearest edge, zero monthly cost at most service-business volumes, one-command deploy from CI. The recipe is short because the technology is small. The boring stack that prints does not need elaborate deploy plumbing; it needs the plumbing configured once, correctly, and never deviated from. Variations should be justified at code review.

Ready to ship the Astro + Workers deploy in week 4 of the cluster build? Book a 30-minute call →

Share X LinkedIn
Build it yourself?

Get the kit, not just the theory.

We'll send the build checklist behind this post — and the next pillar when it ships. One email, no drip sequence. Unsubscribe in one click.

Want this built for you?

Book a discovery call. We'll walk your numbers.

20 minutes. Tell us what's broken, hear what we'd ship in the next 90 days. No pitch deck.