Moving a static blog from Cloudflare Pages to Workers Assets

Why the switch made sense, the practical trade-offs, and a handful of small configuration details that would have saved debugging time up front.

· 8 min read · Đọc bản tiếng Việt
Migrating a static Astro blog from Cloudflare Pages to Workers Assets: minimal wrangler.jsonc with dist + 404-page handling, cache headers for non-hashed files, and unifying static hosting with the Worker runtime

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" } — then npm run build && wrangler deploy.
  • Critical footgun: never use not_found_handling: "single-page-application" on a blog — it rewrites unknown URLs to index.html with 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 shorter Cache-Control via _headers — check with curl -I after 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.png can cache for a long time.
  • og-default.png may need to update when the brand changes.
  • robots.txt shouldn’t be cached aggressively while indexing rules are still being tuned.
  • sitemap.xml and rss.xml should 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.
  • wrangler is 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.

CriterionPagesWorkers Assets
OnboardingEasierNeeds Workers/Wrangler
Static hostingVery goodVery good
API/runtime logicSupported, not the focusFeels native
Code-driven routingMore limitedStronger
ObservabilityEnough for staticBetter once logic is added
Deployment modelGit-centricWorker-centric
Good for newcomersVery goodSlightly 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.html exists.
  • not_found_handling is 404-page.
  • A bad URL returns a real HTTP 404.
  • RSS, sitemap, and robots.txt have 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.