# Serving a website with Cloudflare Workers static assets

> How to deploy a static site, an SPA, or a full Astro app on Cloudflare Workers using the static assets feature, with a Worker for the dynamic parts.

Author: Flavio Copes | Published: 2026-06-22 | Canonical: https://flaviocopes.com/cloudflare-workers-static-assets/

A Worker can do more than answer API calls. It can serve a whole website.

Cloudflare calls this **static assets**. You point a Worker at a folder of files, and Cloudflare serves them fast from the edge, for free. Your Worker code only runs when you need something dynamic.

This is how I serve real apps: static HTML, CSS, and [JavaScript](https://flaviocopes.com/javascript/) handled automatically, with a Worker behind it for the API and anything that needs logic.

## The simplest case: just files

Say you have a folder called `public` with an `index.html` and some assets. Point your config at it:

```jsonc
{
  "name": "my-site",
  "compatibility_date": "2026-06-20",
  "assets": {
    "directory": "./public"
  }
}
```

Deploy, and the whole folder is live:

```bash
npx wrangler deploy
```

No Worker code needed. Cloudflare serves the files directly. For a plain static site, that's the whole story.

## Handling URLs that don't match a file

What happens when someone visits a URL that isn't a file? You control that with `not_found_handling`.

For a normal site with custom 404s:

```jsonc
{
  "assets": {
    "directory": "./public",
    "not_found_handling": "404-page"
  }
}
```

This serves your `404.html` when nothing matches.

For a single-page app, where the client-side router handles every route, you want all unknown paths to load `index.html` instead:

```jsonc
{
  "assets": {
    "directory": "./dist",
    "not_found_handling": "single-page-application"
  }
}
```

Now [React](https://flaviocopes.com/react/) Router, or whatever you use, takes over on the client.

## Adding a Worker for dynamic parts

Most apps need some server logic, like an API. You add a `main` Worker alongside the assets, and a `binding` so your Worker can reach the files:

```jsonc
{
  "name": "my-app",
  "main": "src/index.ts",
  "compatibility_date": "2026-06-20",
  "assets": {
    "directory": "./dist",
    "binding": "ASSETS"
  }
}
```

By default, Cloudflare tries to serve a static file first, and only runs your Worker if no file matches. That's usually what you want.

But sometimes you want your Worker to run *first*, for example to check authentication before serving anything. Use `run_worker_first` with the routes that should hit the Worker:

```jsonc
{
  "assets": {
    "directory": "./dist",
    "binding": "ASSETS",
    "run_worker_first": ["/api/*"]
  }
}
```

Now `/api/*` always goes to your Worker, and everything else tries static files first.

## Deploying an Astro app

This site, and the apps I build, run on [Astro](https://flaviocopes.com/astro-introduction/). Astro has an official Cloudflare adapter that wires all of this up for you.

Install it:

```bash
npx astro add cloudflare
```

This sets the adapter so Astro builds a static client folder plus a Worker for any server-rendered routes. Then you build and deploy with Wrangler:

```bash
npx wrangler deploy
```

You get the best of both: static pages served straight from the edge, and server routes running in a Worker, all from one deploy.

## Why I like this

The old way was to pick: static host *or* a server. With Workers static assets you get both, in one place, with one deploy command.

Static files are served for free and fast. Your Worker only runs, and only costs, when there's real work to do.

The details are in the [static assets docs](https://developers.cloudflare.com/workers/static-assets/).
