Deployment

Gustwind sites can be deployed to any static host. The most difficult part is building the site and you can either push this problem to a CI provider or build at the host itself. The preferred deployment path is now the Node.js build.

The general approach is to first set up a build script to install Node.js dependencies and run Gustwind like this:

build.sh

#!/usr/bin/env bash

npm ci
npm run build:release

Also make the file executable with chmod +x and point your CI environment to use the file when building. Usually there's a field for that in their user interface somewhere.

The benefit of using a simple script like this is that it lets you control versions of both Node.js and Gustwind. Gustwind is still a moving target, so pinning dependency versions in your lockfile is recommended. npm run build remains useful for a faster local build loop, but deployments should use npm run build:release so Pagefind search assets are generated.

Netlify🔗

To configure Netlify, set up a file as follows:

netlify.toml

[functions]
  included_files = ["node_modules/sharp/**/*", "node_modules/@img/sharp-linux-x64/**/*", "netlify/functions/layouts/**", "site/meta.json"]

[build]
  base    = ""
  publish = "build"
  command = "npm ci && DEBUG=1 node ./gustwind-node/cli.ts --build --output ./build"

The related build command would install dependencies and run the Node build. Alternatively you can leverage build.sh as above.

Vercel🔗

For Vercel, point to build.sh through their user interface.

Cloudflare Workers🔗

For Worker deployments, Gustwind now exposes a Cloudflare adapter through gustwind/workers/cloudflare. It wraps the edge-compatible render path into a Worker fetch() handler and can optionally serve built assets from the standard ASSETS binding.

import { createCloudflareWorker } from "gustwind/workers/cloudflare";
import { plugin as metaPlugin } from "gustwind/plugins/meta";
import { plugin as scriptPlugin } from "gustwind/plugins/script";
import { plugin as edgeRendererPlugin } from "gustwind/plugins/htmlisp-edge-renderer";
import { plugin as edgeRouterPlugin } from "gustwind/routers/edge-router";
import scriptAssets from "./build/.gustwind/script-assets.json" with { type: "json" };

export default createCloudflareWorker({
  initialPlugins: [
    [edgeRouterPlugin, {
      routes: {
        "/": {
          layout: "Home",
          context: { headline: "Hello from Gustwind" },
        },
      },
    }],
    [metaPlugin, { meta: { title: "Cloudflare Worker" } }],
    [scriptPlugin, { scriptAssets }],
    [edgeRendererPlugin, {
      components: {
        Home: "<html><body><h1 &children=\"meta.title\"></h1><p &children=\"context.headline\"></p></body></html>",
      },
      componentUtilities: {},
      globalUtilities: { init: () => ({}) },
    }],
  ],
});

If you are also publishing a static build/ directory, keep Wrangler assets enabled so CSS, JS, images, and other emitted files are served directly by the ASSETS binding while page requests fall through to Gustwind rendering.

Dynamic edge-rendered pages can use a stable stylesheet URL by setting stableCssPath on the Tailwind plugin, for example "/assets/site.css". Static pages still receive the hashed production stylesheet link, and Gustwind writes the stable copy for custom edge handlers.

The script plugin writes .gustwind/script-assets.json during the Node build. Pass that manifest back to the Worker script plugin as scriptAssets so route-level scripts entries resolve to the same hashed module URLs that the static build uses.

For custom Worker bundles, generateCloudflareManifest(...) from the Node API can generate a module containing explicit .html and .server.ts component imports. This keeps Wrangler/esbuild from guessing extensionless imports.

If you need a custom Pages Function or Worker handler, import initRender from gustwind/workers/cloudflare instead of the root gustwind entry. The Cloudflare entry only depends on edge-safe runtime code, so it avoids pulling Node build tooling into the Worker bundle.

import { initRender } from "gustwind/workers/cloudflare";

const render = await initRender(initialPlugins);

export default {
  async fetch(request) {
    const { markup } = await render(new URL(request.url).pathname, {});

    return new Response(markup, {
      headers: { "content-type": "text/html; charset=UTF-8" },
    });
  },
};

GitHub Pages🔗

For GitHub Pages, it's a good idea to follow Pagic documentation. You can point to the build script within GitHub YAML configuration.