Cloudflare Edge Worker

Introduction

ReeWeb is static-first: bun ssg renders every page to HTML during SSG, and the output in dist/ can be served by any static host with no runtime (see Deployment Overview). Cloudflare adds one optional capability on top of that static output - a small edge worker that runs on each request and can rewrite the already-rendered HTML before it reaches the browser.

This is not runtime server-side rendering. Templates are still compiled to static HTML during static site generation; the worker only post-processes that finished HTML at the edge. You keep the performance and simplicity of a static site while getting a narrow, per-request hook for things the generator cannot know - the visitor's edge location, an A/B bucket, a request header, and similar.

The template ships a ready-to-use worker at cf-worker.ts in the project root, wired but commented out in wrangler.jsonc so a fresh site deploys as pure static assets until you opt in.

How It Fits Together

Cloudflare serves the built site from the assets store and, when the worker is enabled, runs the worker in front of it:

Request → cf-worker.ts → env.ASSETS.fetch(req) → dist/**/index.html
                │
                └─ HTMLRewriter streams the response, editing marked elements

The wiring lives in wrangler.jsonc:

{
    "name": "your-site",
    "compatibility_date": "2026-06-29",
    "main": "./cf-worker.ts",          // enable the worker (uncomment to opt in)
    "assets": {
        "binding": "ASSETS",             // the worker fetches static files via env.ASSETS
        "directory": "./dist",
        "not_found_handling": "404-page",
        "run_worker_first": true         // run the worker on every request, not just misses
    }
}

Two keys make the pattern work:

  • binding: "ASSETS" gives the worker a Fetcher for the static site, so env.ASSETS.fetch(req) returns the pre-built page.
  • run_worker_first: true routes every request through the worker (by default the worker only runs when an asset is missing). This is what lets the worker touch normal page HTML. Because it runs on every request, keep it cheap - see Keep It Cheap.

Without main set, the same dist/ deploys as plain static assets and the worker never runs - the site behaves exactly as described in the overview.

The Shipped Worker

cf-worker.ts is intentionally tiny. It fetches the static asset, ignores anything that is not HTML, and - only when Cloudflare reports an edge location - streams the HTML through HTMLRewriter to fill in elements marked data-cf-edge:

export default {
    async fetch(req: Request, env: { ASSETS: Fetcher; }): Promise<Response> {
        const res = await env.ASSETS.fetch(req);

        if (!res.headers.get("content-type")?.includes("text/html")) return res;

        const colo = (req as any).cf?.colo as string | undefined;

        if (!colo) return res;

        return new HTMLRewriter().on("[data-cf-edge]", {
            element(el) {
                el.setInnerContent(colo);
                el.removeAttribute("data-cf-template");
                el.removeAttribute("data-cf-edge");
            },
        }).transform(res);
    },
};

HTMLRewriter is Cloudflare's streaming HTML parser - it edits the response as bytes flow through, so there is no buffering and effectively no added latency.

The data-cf-edge Convention

To surface an edge-computed value in a page, mark an element in your .ree template with data-cf-edge. Put a static-generation placeholder inside it so the page is complete even when the worker is not running:

<span data-cf-edge>{_ location_edge}</span>

At static generation time the element renders with its translated placeholder text. When the worker is enabled and the request carries a Cloudflare edge location, HTMLRewriter replaces the contents with the datacentre code (for example FRA) and strips the marker attributes so they never reach the browser.

Graceful degradation is built in. If there is no cf.colo on the request - a non-Cloudflare host, or the worker disabled - the worker returns the asset untouched and the placeholder simply stays. The page always works; the edge value is an enhancement, not a dependency.

Extending the Worker

The same shape covers most per-request needs. Add more HTMLRewriter handlers, branch on request data, or set response headers - all while keeping the static-first model. Typical additions:

  • Geo or locale hints - read req.cf?.country and adjust a banner or a suggested language.
  • A/B buckets - hash a cookie or header, inject a variant class, keep the two variants in the same static HTML.
  • Security or cache headers - add headers to the asset response before returning it.
  • Edge redirects beyond the static _redirects file - when a redirect depends on request state rather than a fixed path map.

Anything that must be decided per request and cannot be baked in during static site generation belongs here. Anything that can be baked in should stay in the SSG script - that is what keeps the site fast.

Keep It Cheap

Because run_worker_first: true runs the worker on every request, treat it as a hot path:

  • Stream, do not buffer. Use HTMLRewriter to edit the response as it passes through. Reading the whole body into memory defeats the streaming model and adds latency.
  • Bail early on non-HTML. The shipped worker returns immediately for assets that are not text/html, so CSS, JS, images, and fonts pass through with no work.
  • Do no I/O you do not need. Keep the worker to request-local logic; reaching out to other services on every page load reintroduces the origin round-trips a static site avoids.

Deploying

With the worker enabled in wrangler.jsonc, deploy with Wrangler (you need to be authenticated, of course):

bun ssg
wrangler deploy

Wrangler uploads the dist/ assets and the worker together. To go back to pure static hosting, comment out main (and run_worker_first) in wrangler.jsonc and redeploy - the worker is removed and the same dist/ is served directly. See Cloudflare Pages for the assets-only path and Git-based builds.