Skip to content

Static-build frameworks

The grid builds Astro, Vite, SvelteKit, and Nuxt sites from your source. You do not build them yourself. grid plug recognises the framework, writes a cloudgrid.yaml with a build: block, sends your source to the grid, and the grid runs the build and serves the result. Astro is the worked example on this page; Vite, SvelteKit, and Nuxt follow the same model with a different output directory.

The frameworks and their output directories

Section titled “The frameworks and their output directories”

grid plug detects a static-build framework from the dependency in your package.json (either dependencies or devDependencies, so a vite that sits in devDependencies, as it usually does, still counts) and picks the output directory that framework writes to:

Framework Detected from build.output
SvelteKit @sveltejs/kit build
Astro astro dist
Nuxt nuxt .output/public
Vite vite dist

All four become a single static service with a build: block. The output directory is not a guess you can skip: SvelteKit writes to build, not dist, so a hand-written yaml that copies Astro’s output: dist onto a SvelteKit project points the service at the wrong directory. That one does not fail quietly: after the build the grid checks for <output>/index.html, does not find dist/index.html, and fails the build with Your build command did not produce dist/index.html, naming the field to fix. Contrast that with the genuinely silent failure below, which has no build: block and so no such check.

The rows are checked in that order, and the first dependency present wins. SvelteKit, Astro, and Nuxt all pull in Vite, so many of these projects also list vite as a dependency. The specific framework is matched before the generic vite row, so an Astro project that also depends on vite resolves as Astro (output: dist), and a SvelteKit project that depends on vite resolves as SvelteKit (output: build), never as a bare Vite app.

Run grid plug in a bare Astro project (one that has an astro dependency and no scripts.start) and it generates:

# Generated by `grid plug` auto-init.
# One service, sourced from this directory. Edit freely.
name: my-site
services:
web:
type: static
path: /
build:
command: npm run build
output: dist
source:
path: .

No configuration is needed. type: static with a build: block is the whole model: the grid runs command and serves the output directory. source.path: . says the project is at the folder root, so your existing files are what the grid builds from.

  1. grid plug packs your source and sends it to the grid. Your build output directory is left out of that bundle automatically (see The commit-your-build anti-pattern below), so there is nothing to gitignore or stage by hand.
  2. The grid installs dependencies, then runs your build command. With a package-lock.json present it runs npm ci; without one it runs npm install. See npm only.
  3. The grid serves the output directory over HTTP at your entity root.

The build runs on the grid, on the source you just sent. The build is async: the CLI reports progress and prints the live URL when the site is up. To update the site, change your source and run grid plug again.

The model above builds from source, so there is never a reason to build locally and commit the result. Doing so is the one mistake that fails silently.

Here is the mechanism, because a vague warning will not save you from it:

  • With a build: block, the grid builds from your source and serves its output. Your build output directory is excluded from what gets sent, so a committed dist/ is not merely ignored, it never leaves your machine. The live site is always the grid’s own fresh build.
  • Without a build: block, a static service serves whatever directory you point it at, verbatim. It runs no build. If you pre-build, commit dist/, and write a static service around that folder, the grid serves the committed files exactly as they are.

That second shape is where the afternoon goes. You change your source, run grid plug, and it succeeds and prints a working URL. Because no build ran, the site is still the last dist/ you committed. If you forgot to rebuild and commit, the live site shows the previous build.

Build-time variables, the ones your framework reads while it builds (a VITE_-prefixed API URL, a feature flag baked into the output), go under build.env:

services:
web:
type: static
path: /
build:
command: npm run build
output: dist
env:
VITE_API_URL: "https://my-api.your-grid.cloudgrid.io"
COMING_SOON: "true"

Each entry is set in the environment of the build step, so process.env.VITE_API_URL resolves while npm run build runs, and the value is baked into the output.

Detection runs in a fixed order, and the first rule that matches wins:

  1. a next dependency makes it a nextjs service;
  2. a non-empty scripts.start makes it a node service, before the static-build check is ever reached;
  3. otherwise, a static-build framework dependency (SvelteKit, Astro, Nuxt, Vite) makes it a static service with a build: block.

So a project that has both an astro dependency and a start script becomes a node service, not a static site. This is deliberate: a framework like Astro or Vite is often the frontend build tooling sitting next to an Express or Fastify backend, and that backend has a start script. Classifying it as static would drop the server entirely. The cost is that it is invisible: if you expect a static site and your package.json carries a start script, you get a node service and the difference is silent. Remove the start script, or set the service type yourself, if you want the static-build path.

The build step uses npm. It runs npm ci when a package-lock.json is present and npm install when it is not. pnpm and yarn lockfiles are ignored. A repo that carries both a pnpm lockfile and an npm lockfile develops against one and builds against the other, and the two drift apart without any warning. Keep a package-lock.json committed and current, and treat it as the file the grid builds from.

Three dotenv files are always left out of what reaches the grid: .env, .env.local, and .env.*.local. These are your local machine’s files and never travel with your source.

.env.example is different: it is sent to the grid, and the grid reads it at plug time to check that the keys your app expects are accounted for. Keep it accurate and keep secrets out of it, because it is a committed, sent file and not a private one. It is reasonable to assume every dotenv file is ignored, so this is the one to remember: the example travels, the real ones do not.

By default the grid looks for a service’s code under services/<name>/. The generated yaml above sets source.path: . instead, which points the service at the project root, so a single-app repo with the framework at the top level works without moving anything. Set source.path to any relative directory when your source lives elsewhere, rather than reshaping your repo to fit the default.