Skip to content

Vocs

Fly.Website.Vocs deploys a Vocs documentation project to Fly. Vocs prerenders static HTML; the node target drops server modules and Alchemy hosts a tiny static-file server on a Fly Service — one App plus a shared IPv4 so https://{app}.fly.dev answers. Extensionless pages work (/aboutabout/index.html). Your vocs.config.* loads natively — there is no adapter to install.

The build integration is not bundled with alchemy. Install @alchemy.run/frontend-frameworks; the resource loads /vocs/node from your project at deploy time. It is only used at build time, so a dev dependency is enough:

Terminal window
bun add -d @alchemy.run/frontend-frameworks

Your Vocs project already needs vocs installed.

Your vocs.config.* file loads natively — title, sidebar, and any other Vocs options work exactly as they do outside Alchemy:

vocs.config.ts
import { defineConfig } from "vocs/config";
export default defineConfig({
title: "Docs",
sidebar: [
{ text: "Home", link: "/" },
{ text: "Guide", link: "/guide" },
],
});

If vocs.config.* sets outDir, pass the same value on the resource (default "dist") so generated output stays outside the rebuild hash:

export const Website = Fly.Website.Vocs("Website", {
rootDir: "./docs",
outDir: "build",
});

Declare the site as a module-level const (rather than inline in the Stack):

alchemy.run.ts
import * as Fly from "alchemy/Fly";
export const Website = Fly.Website.Vocs("Website", {
rootDir: "./docs",
});

Omit app to create a Fly.App under this site’s namespace. Pass an existing Fly.App when the site should live on an App you already declared.

Yield the site from your Stack and return its URL:

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Effect from "effect/Effect";
export default Alchemy.Stack(
"MyVocsSite",
{
providers: Fly.providers(),
state: Alchemy.localState(),
},
Effect.gen(function* () {
const site = yield* Website;
return { url: site.url };
}),
);

The live URL is https://{app}.fly.dev. The hosted server listens on port 3000 and answers GET /health. site.app, site.service, site.ip, and site.certificate are set on deploy.

Top-level env is copied onto process.env before build and alchemy dev, and onto the Machine at deploy:

alchemy.run.ts
export const Website = Fly.Website.Vocs("Website", {
rootDir: "./docs",
env: {
DOCS_TITLE: "Hello from Alchemy!",
},
});

These are process environment variables, not Worker bindings.

Vocs prerenders at build time, so page content that reads process.env is baked into the HTML. The same values are set on the hosted Node process; there is no request-time Vocs server.

const title = process.env.DOCS_TITLE ?? "Docs";

alchemy dev runs Vocs’ own dev server (native HMR) instead of deploying; site.url is the local address and no Fly resources are created (app, service, ip, and certificate are undefined). Wrap the site in Alchemy.remote() to deploy the live App and Service even during dev:

export const Website = Fly.Website.Vocs("Website", {
rootDir: "./docs",
}).pipe(Alchemy.remote());

domain is a hostname string. Alchemy requests ACME (Fly.Certificate) on the App; url becomes https://{domain}. v1 expects DNS for that hostname to already point at the App:

export const Website = Fly.Website.Vocs("Website", {
rootDir: "./docs",
domain: "docs.example.com",
});