Skip to content

Vite

Source: src/Cloudflare/Website/Vite.ts

A Cloudflare Worker deployed from a Vite project.

Vite uses the Cloudflare Vite plugin to build both the server bundle and client assets in a single vite build invocation — no manual main entrypoint, build command, output directory, or Wrangler configuration required.

Input files are content-hashed (respecting .gitignore by default) so unchanged projects skip the build and deploy entirely.

For a pure static site (no SSR), a single call is all you need. Vite builds the project and Alchemy deploys the output as a Cloudflare Worker with static assets.

const site = yield* Cloudflare.Website.Vite("Website");

SSR frameworks like TanStack Start or SolidStart work with a single call — the nodejs_compat compatibility flag is enabled by default so the server bundle can use Node.js APIs.

TanStack Start

const app = yield* Cloudflare.Website.Vite("TanStackStart");

SolidStart with worker-first routing

const app = yield* Cloudflare.Website.Vite("SolidStart", {
assets: { runWorkerFirst: true },
});

React Router

React Router’s server build (virtual:react-router/server-build) is a build manifest with no default export, so it cannot be deployed as the Worker entry directly. Point main at a module that wraps it with createRequestHandler (React Router’s Cloudflare template ships this as workers/app.ts):

const app = yield* Cloudflare.Website.Vite("ReactRouter", {
main: "workers/app.ts",
});

Frameworks that emit more than one server environment (e.g. React Server Components, which split into an rsc environment and an ssr environment) need viteEnvironments to declare which environment produces the deployed Worker entry and which additional server environments to bundle alongside it. The client environment is always deployed as static assets.

const app = yield* Cloudflare.Website.Vite("ReactRouterRSC", {
viteEnvironments: {
entry: "rsc",
children: ["ssr"],
},
});

By default the deployed Worker entry is the server bundle the framework produces. When the Worker must export more than the framework’s fetch handler — Durable Object classes, additional handlers — point main at your own module that wraps the framework handler and re-exports the extras. main takes precedence over any entry configured in the Vite config.

const app = yield* Cloudflare.Website.Vite("App", {
main: "worker/index.ts",
viteEnvironments: {
entry: "rsc",
children: ["ssr"],
},
});

For SPAs (React, Vue, etc.), configure asset handling so all routes fall back to index.html.

Vue SPA

const app = yield* Cloudflare.Website.Vite("Vue", {
assets: {
htmlHandling: "auto-trailing-slash",
notFoundHandling: "single-page-application",
},
});

Foldkit

Foldkit apps are client-only Vite projects, so a single call deploys them — the Foldkit Vite plugin in the app’s own vite.config.ts composes with the injected Cloudflare plugin. Enable single-page-application not-found handling so deep links boot the app:

const app = yield* Cloudflare.Website.Vite("Foldkit", {
assets: {
notFoundHandling: "single-page-application",
},
});

Cloudflare.Website.Foldkit is the same thing with that default already applied.

Octane SPA

A client-only OctaneJS app (no octane.config.ts routes) is a plain Vite SPA — the octane() compiler plugin in the app’s own vite.config.ts composes with the injected Cloudflare plugin:

const app = yield* Cloudflare.Website.Vite("Octane", {
assets: {
notFoundHandling: "single-page-application",
},
});

Fullstack Octane apps (routes + SSR in octane.config.ts) run their own two-pass build through Octane’s Cloudflare adapter — deploy those with Cloudflare.Website.Octane instead.

Serving on a Zone Route with a Path Prefix

Section titled “Serving on a Zone Route with a Path Prefix”

Cloudflare matches static assets against the full request pathname, so a site attached to a route like example.com/docs* only serves assets whose uploaded paths carry the /docs prefix. Set Vite’s base in your vite.config.ts — the emitted HTML references its assets under the prefix, and Alchemy keys the uploaded asset manifest with the same resolved base so the two always agree.

vite.config.ts

import { defineConfig } from "vite";
export default defineConfig({
base: "/docs/",
});

alchemy.run.ts

const docs = yield* Cloudflare.Website.Vite("Docs", {
routes: [{ pattern: "example.com/docs*", zoneName: "example.com" }],
});

By default, every non-gitignored file is hashed to decide whether a rebuild is needed. Use memo to narrow the scope when your project has large directories that don’t affect the build output.

Narrowing the memo scope

const site = yield* Cloudflare.Website.Vite("Docs", {
memo: {
include: ["src/**", "content/**", "package.json"],
},
});

Rebuilding when a sibling workspace package changes

The default scope only hashes files under the project root (plus the nearest lockfile), so edits to a sibling workspace package the app imports do not retrigger the build on their own. Add the sibling’s sources with a ../ include glob — and keep lockfile: true, since providing include otherwise drops the lockfile from the hash:

const site = yield* Cloudflare.Website.Vite("Web", {
rootDir: "apps/web",
memo: {
include: ["**\/*", "../../packages/env/src/**"],
lockfile: true,
},
});

Calling Vite with no arguments returns a constructor you can extend to declare the Worker as a named class. The class is both an Effect you can yield* to deploy and a type you can reference elsewhere — useful when other resources need to bind to this Worker.

class Website extends Cloudflare.Website.Vite<Website>()("Website") {}
const site = yield* Website;