Skip to content
Nap Joseph Calub

Migrating off Deno Deploy

Oct 11, 2026

This is the actual plan I worked from when the Deno news landed, kept honest including the parts that didn't work. It was written with an AI agent (ZCode / GLM), I made the calls, and it shipped the same day.

Context

On October 9, 2026 Deno announced the team is joining Cloudflare. Deno Deploy shuts down in roughly six months; the Deno runtime gets about a year of maintenance, then development ends.

This site was built Deno-first: Fresh 2 SSR, scheduled to deploy on Deno Deploy. The question was where to land instead.

The audit that shaped everything

Before choosing, we audited what the site actually depended on:

  • The built server default-exports { fetch } — exactly the Cloudflare Workers module shape.
  • Application code contained exactly two Deno-specific API calls — both filesystem reads loading markdown content at boot.
  • Everything else was framework internals.

Two calls away from portable. That set the plan.

Path one: the official recipe (abandoned)

Fresh documents a Cloudflare Workers deploy via @cloudflare/vite-plugin in the Vite config. We tried it, in every plugin ordering. All of them fail inside Fresh's own CSS sub-build, and Fresh's JSR page disclaims that Workers compatibility is unvalidated.

We could have debugged the plugin interop — and still carried runtime Deno.* risk on workerd. Instead we asked the better question.

Path two: does this site need a server at all?

The site is read-only. Twenty-odd routes, no forms, no sessions, no per-request data. Every page can be baked at build time:

  1. Bundle content — a generator inlines content/ markdown into the build, removing the last filesystem reads.
  2. Prerender — a script boots the built Fresh server, renders every page plus the sitemap and 404 into dist-site/, mirrors the client assets, and verifies every referenced URL resolves to a real file.
  3. A 15-line worker (web-standard APIs only) handles what static files cannot: 301s for legacy /posts/* URLs. Everything else defers to static assets.

What shipped

  • 0514e87 — content bundled at build time, no runtime FS reads
  • dfb56f6 — prerender pipeline, worker, wrangler config
  • d4901f0 — CI deploys to Cloudflare Workers with wrangler

Verified locally through real workerd before pushing: every page 200, branded 404 with a real 404 status, legacy URLs 301 correctly, islands and fonts loading.

What I'd do differently

Nothing structural. Two learnings worth keeping:

  • The asset verifier in the prerender script paid for itself on day one — it caught a nav link to a route that didn't exist yet.
  • Build hashes differ between local and CI environments; always verify against the URLs the live pages reference, never a stale local build.

Production now runs with zero server runtime. The Deno runtime's eventual end-of-life cannot touch the live site — Deno is only used to build, and the deployed artifact is plain files plus a 15-line worker.