TL;DR
- Workers Assets unifies static hosting and the Worker runtime in one deploy unit — no more juggling Pages + Pages Functions + a sidecar Worker for routes.
- Minimum
wrangler.jsonc:"assets": { "directory": "./dist", "not_found_handling": "404-page" }— thennpm run build && wrangler deploy.- Critical footgun: never use
not_found_handling: "single-page-application"on a blog — it rewrites unknown URLs toindex.htmlwith HTTP 200, breaking SEO, broken-link checks, RSS, and analytics.- Hashed build assets (
/assets/index.B7a1c4.js) cache long; non-hashed files (rss.xml,sitemap.xml,robots.txt,og-default.png) need shorterCache-Controlvia_headers— check withcurl -Iafter deploy.- Pick Workers Assets when the site will grow API routes, redirect logic, custom headers, or preview URLs; stick with Pages for a pure static site that ends at git push.
- The mental-model shift: from “static hosting with optional functions” to “programmable edge with assets attached”.
Cloudflare Pages used to be the default answer for hosting a static blog: push to Git, build, deploy, attach a domain — done.
The moment a site starts needing even a little logic — API routes, preview URLs, conditional redirects, custom headers, a touch of middleware — Pages stops being the cleanest shape. It is still doable, but it usually means pulling in a separate Worker, then juggling routes, deployments, and observability across two places.
That was the reason this blog moved to Cloudflare Workers Assets.
This post is the notes version: why the switch, the minimum configuration, and a few small things that are easy to miss on the way in.
What Workers Assets is
Historically, Workers were the strong option for compute at the edge, and Pages was the strong option for static hosting.
To serve static files from a Worker, you had to write the logic yourself — read from KV, R2, or another backend. Possible, but not natural when all you want is to host a static blog.
Workers Assets closes that gap: you can deploy a build-output directory, e.g. dist/, as static assets attached directly to a Worker.
In short:
- Pages gives you very fast, very simple static hosting.
- Workers gives you a flexible edge runtime.
- Workers Assets puts both inside one deployment unit.
For a static blog, the visible difference isn’t that HTML is served faster. The real difference is that the site can start as a static site and still grow into something more when it needs to, without switching platforms.
Why the switch
This blog started out as a static-only site. Pages was entirely fine for that.
Over time, the list grew:
- Preview URLs for draft posts.
- More flexible redirect rules.
- Custom response headers.
- A small API for secondary features.
- A single place to watch logs and errors.
- A deployment flow consistent with other Workers.
Staying on Pages would have meant Pages Functions or a separate Worker alongside. Both work. But once the site heads in the direction of runtime logic, Workers Assets is the cleaner mental model.
Instead of:
Pages static build
+ Pages Functions
+ a separate Worker for some routes
+ route config on the zone
the target shape is:
Worker
+ static assets
+ route logic
+ observability
+ custom domain
One deploy unit. One runtime model. One place to debug.
Minimum configuration
Here is the wrangler.jsonc for this blog:
{
"name": "khavan",
"compatibility_date": "2025-10-08",
"compatibility_flags": ["nodejs_compat"],
"assets": {
"directory": "./dist",
"not_found_handling": "404-page"
}
}
After building the site, deployment is two commands:
npm run build
wrangler deploy
wrangler uploads everything under dist/ as the Worker’s assets.
If the site is only static HTML/CSS/JS, the Worker script can be minimal — effectively empty. The point isn’t that there is logic on day one; it’s that when logic is needed, the runtime is already there.
What I like most
1. A single deploy unit
With Pages, static assets and Worker logic tend to feel like two parts. When something breaks, the question “is it the build, Pages, Functions, a Worker route, or the cache?” takes time to answer.
With Workers Assets, static assets and runtime logic live inside the same deployment.
That matters most when a change spans layers, for example:
- Update a static page.
- Add a new redirect.
- Add an API route.
- Change a response header.
- Tune caching.
All of them land in a single versioned deploy, easier to roll back and to reason about.
2. Routing becomes code
A static site starts simple, but rarely stays that way.
A blog tends to accumulate:
/api/*for small internal features./preview/*for draft content.- Redirects from old URLs to new ones.
- Per-language path logic.
- Specific headers for RSS, sitemap, OpenGraph images, or static assets.
- Light protection for routes that aren’t public yet.
With Workers Assets, all of that lives inside the Worker rather than a separate service.
That is the shift: you aren’t only hosting static files — you have a programmable edge layer in front of them.
3. Better observability for operations
Observability sounds oversized for a small blog. The moment something actually breaks, logs pay for themselves.
Workers expose runtime logs, invocation counts, error rates, and a troubleshooting flow most developers already recognise. Once the site grows an API route or middleware, no separate observability plumbing is needed.
A static-only blog may not need much of this. A blog with real traffic, real SEO, real redirects, and real integrations benefits from being easy to debug.
4. Custom domain stays simple
Custom domains aren’t special here.
Attach a domain to the Worker, let Cloudflare handle the certificate, and route traffic. For anyone comfortable with the Cloudflare dashboard or wrangler, the flow is straightforward.
What matters is that the domain doesn’t point at static hosting alone. It points at a Worker that can handle both static assets and the logic in front of them.
Two small mistakes to avoid
1. Wrong not_found_handling
This is the most common footgun.
Workers Assets supports several behaviours when an asset doesn’t exist. Choosing:
"not_found_handling": "single-page-application"
rewrites any unknown URL to index.html and returns HTTP 200.
That’s correct for SPAs (React, Vue) where a client-side router resolves every route.
It is the wrong choice for a static blog.
Say a visitor lands on:
/posts/a-deleted-post/
If the URL no longer exists, the correct response is 404.
Returning index.html with 200 creates several problems:
- Search engines may treat dead URLs as valid pages.
- Broken-link checkers assume every link still works.
- RSS readers and crawlers receive the wrong content.
- Users don’t see a clear 404 page.
- Analytics gets polluted because dead URLs look like real page views.
For a static blog, use:
"not_found_handling": "404-page"
That serves dist/404.html with a real HTTP 404.
One line of config, but it directly affects SEO, crawler behaviour, and the quality of operations.
2. Cache headers for static assets need a second look
Workers Assets caches well out of the box, particularly for files with a content hash in the filename.
For example:
/assets/index.B7a1c4.js
/assets/post-card.D8s9x2.png
/assets/style.F92kda.css
These are safe to cache for a long time — when content changes, the filename changes. That’s the standard pattern for most modern static site generators, Astro included.
Not every file carries a hash.
For example:
/og-default.png
/favicon.png
/robots.txt
/sitemap.xml
/rss.xml
These deserve different policies.
favicon.pngcan cache for a long time.og-default.pngmay need to update when the brand changes.robots.txtshouldn’t be cached aggressively while indexing rules are still being tuned.sitemap.xmlandrss.xmlshould reflect new content fairly quickly.
After deploying, check the actual headers:
curl -I https://example.com/og-default.png
curl -I https://example.com/rss.xml
curl -I https://example.com/sitemap.xml
If a non-hashed file ends up cached too hard, override via _headers.
For example:
/og-default.png
Cache-Control: public, max-age=3600
/rss.xml
Cache-Control: public, max-age=300
/sitemap.xml
Cache-Control: public, max-age=300
/robots.txt
Cache-Control: public, max-age=3600
The rule of thumb:
- Hashed build output: cache for a long time.
- Metadata, feeds, sitemap: cache briefly.
- Brand assets that rarely change: medium-to-long, depending on tolerance.
- When unsure: check the response header after deploy.
When to pick Workers Assets
Workers Assets is the right choice when any of these apply:
- Static site today, but likely to need API routes later.
- You want redirects and routing logic expressed as code.
- Custom headers, cache rules, or light middleware are in play.
- The deployment model should look like any other Worker.
- Static assets and edge logic belong in one place.
wrangleris already the daily driver.- You want more control than Pages, without building static hosting from scratch.
In short: if the site is static now but you know it won’t stay static, Workers Assets is the right default.
When Pages is still the better fit
Pages is still the right tool when simplicity is the priority.
For example:
- A very simple personal blog.
- A landing page.
- A small documentation site.
- A team unfamiliar with Workers.
- A flow that starts and ends with Git push.
- No need for custom runtime logic.
- No interest in thinking about Workers, routes, or
wrangler.
Pages wins on onboarding. A newcomer to Cloudflare gets there quickly. For a truly static site, Pages does its job well.
Workers Assets isn’t killing Pages. They serve different levels of need.
- Pages suits simple static hosting.
- Workers Assets suits static hosting with the option to grow via a programmable edge.
Practical trade-offs
After the switch, the day-to-day model feels tighter. There are a few trade-offs worth naming.
| Criterion | Pages | Workers Assets |
|---|---|---|
| Onboarding | Easier | Needs Workers/Wrangler |
| Static hosting | Very good | Very good |
| API/runtime logic | Supported, not the focus | Feels native |
| Code-driven routing | More limited | Stronger |
| Observability | Enough for static | Better once logic is added |
| Deployment model | Git-centric | Worker-centric |
| Good for newcomers | Very good | Slightly more technical |
For a plain blog, that difference may not justify migrating.
For a site already thinking about API routes, preview, middleware, custom cache, or redirect logic, Workers Assets opens more room.
Deploy checklist for a Workers-Assets blog
Starting over, the short list of checks:
dist/404.htmlexists.not_found_handlingis404-page.- A bad URL returns a real HTTP
404. - RSS, sitemap, and
robots.txthave sensible cache headers. - Hashed assets cache for a long time.
- Non-hashed files aren’t over-cached.
- The custom domain actually routes to the Worker.
- Old-URL → new-URL redirects work.
- Logs on the Worker are sufficient for basic debugging.
This list catches most of the small mistakes during a Pages-to-Workers-Assets migration.
Takeaway
Workers Assets isn’t a “new Pages” meant to fully replace the old one. The more useful framing: Workers Assets brings static hosting inside the Worker runtime.
For a static blog, that sounds small. Architecturally, it changes a lot.
The site is no longer locked into pure static hosting. It can start simple and grow logic over time, without switching deploy platforms.
For me, that was enough reason to make the move.
Closing
If Cloudflare Pages is in place and everything is fine, there’s no need to migrate for the sake of it.
For a new project, or a site starting to need API routes, preview URLs, redirect logic, custom headers, or better observability, Workers Assets is the starting point I would pick.
A static blog can begin as HTML, CSS, and RSS. As it grows, a programmable edge layer in front of it makes the next step much shorter than rebuilding on a different platform.