Publishing & environments

You write content in StaticOwl as drafts. Nothing you type is on the web until you build — the build compiles your content + templates into plain HTML and deploys it to a host. This guide covers the mental model, the environments every site ships with, how to run a build (dashboard, API, and MCP), how to validate before you publish, how to check status and cancel, and how a custom domain takes over from the preview host at cutover.


The mental model

edit content (drafts)  →  publish to an environment  →  build  →  live output at a host

A build is always scoped to one environment. You build dev, you build prod — each is a separate compile + deploy to a separate host.


Environments and their preview hosts

Every site is created with three environments:

id name auto-builds on publish who can publish
dev Dev yes admin, editor, publisher
staging Staging no admin, publisher
prod Production no admin, publisher

Each environment serves at its own preview host, one per environment:

https://<slug>-<env>.preview.staticowl.com

So a site with slug nightowl gets nightowl-dev.preview.staticowl.com and nightowl-prod.preview.staticowl.com. The preview hosts are always live — no DNS or TLS setup on your side. Building an environment updates that environment's preview host and nothing else.

Tip: build dev freely while you iterate. Promote to prod only when it looks right — publish your content to prod first, then build prod. (If you build an environment with nothing published to it, the build still succeeds but warns and emits a near-empty site — so publish first.)


Running a build

Dashboard

Open Site → Builds → Build now, pick the environment, and choose a mode. When the build finishes, the panel links you to the published publicUrl.

API

# Trigger a build. With no mode, the HTTP endpoint runs a FULL build; env defaults to dev.
curl -X POST https://app.staticowl.com/api/build \
  -H "Authorization: Bearer $STATICOWL_API_KEY" \
  -H "X-Site-Id: site:mysite" \
  -H "Content-Type: application/json" \
  -d '{"mode":"full","env":"dev"}'

# Long build? Run it async — returns 202 + a statusUrl immediately, build runs in the background
curl -X POST https://app.staticowl.com/api/build \
  -H "Authorization: Bearer $STATICOWL_API_KEY" \
  -H "X-Site-Id: site:mysite" \
  -H "Content-Type: application/json" \
  -d '{"mode":"full","env":"prod","async":true}'

A successful synchronous build responds with the deploy result and every page it emitted:

{
  "ok": true,
  "envId": "dev",
  "buildId": "build:1699999999999:ab12cd",
  "artifacts": 42,
  "totalBytes": 1048576,
  "durationMs": 5123,
  "publicUrl": "https://mysite-dev.preview.staticowl.com",
  "deploy": { "ok": true, "bucket": "staticowl-sites", "filesUploaded": 42, "serveCheck": { "ok": true, "status": 200 } },
  "pages": [ { "path": "/index.html", "type": "page", "title": "Home", "bytes": 8123 } ],
  "transformErrors": [],
  "error": null
}

An async build responds 202 with { ok, buildId, envId, status: "running", statusUrl } — poll statusUrl until the status flips.

Build modes:

mode what it does
full rebuild every page. This is what the HTTP endpoint runs when you don't pass a mode.
incremental rebuild only the artifacts impacted by recent changes — much faster on a large site. Request it explicitly on the API; it's the default for the MCP build_run tool and for the auto-build that fires when you publish to dev.
auto currently a full rebuild (the label is recorded for future change-surface detection)
validate dry-run — compile in memory, return the artifact list + transform errors, deploy nothing, record no build

Pick incremental for routine updates on a large site. The HTTP default is full; pass "mode":"incremental" to rebuild only what changed. Reach for full after a template or theme change that touches every page.

MCP (AI agents)

build_run { mode: "full", env: "dev" }
build_run { mode: "full", env: "prod", async: true }

build_run returns publicUrl and (in a real build) pages[]. Related build tools:

Tool Required What it does
build_run — Trigger a build (mode, env, async)
build_status buildId Poll one build by id — running → ok/error
build_last — Status + stats of the single most recent build
build_history — List recent builds (filter with env, cap with limit)
build_impact nodeIds Dry-run blast-radius: which pages would rebuild
build_rollback buildId Revert the live site to a prior build (destructive)

Agents need a write role (admin, editor, or publisher) on the site.


Validate before you publish

validate mode is the cheap sanity check. It runs the full compile in memory and returns the artifact list plus any template/transform errors, but deploys nothing and records no build — so it's the fast way to catch a broken template before spending a real build slot.

curl -X POST https://app.staticowl.com/api/build \
  -H "Authorization: Bearer $STATICOWL_API_KEY" \
  -H "X-Site-Id: site:mysite" \
  -H "Content-Type: application/json" \
  -d '{"mode":"validate","env":"dev"}'
build_run { mode: "validate", env: "dev" }

The response is distinct from a real build — note validate: true, a null buildId, and a per-artifact list:

{
  "ok": true,
  "validate": true,
  "buildId": null,
  "envId": "dev",
  "artifactCount": 42,
  "artifacts": [ { "path": "/index.html", "type": "page", "id": "page:home", "title": "Home", "bytes": 8123 } ],
  "totalBytes": 1048576,
  "durationMs": 3200,
  "transformErrors": [],
  "note": "Validate mode — nothing deployed, no BuildEvent recorded."
}

If transformErrors is non-empty, fix your templates before running the real build. (?dryRun=1 on the query string is an equivalent trigger.)


Checking status and cancelling

Status

Every real build gets a buildId. Poll it:

curl https://app.staticowl.com/api/build/status/build:1699999999999:ab12cd \
  -H "Authorization: Bearer $STATICOWL_API_KEY" -H "X-Site-Id: site:mysite"
build_status { buildId: "build:1699999999999:ab12cd" }

The status endpoint returns status (running | ok | error | cancelled) along with phase, artifacts, bytes, durationMs, publicUrl, and error. Keep polling while status is running. To see the latest build or a list, use GET /api/build/last and GET /api/build/history?env=prod&limit=20.

Cancelling

Only one full/incremental build can run per site + environment at a time — a second POST /api/build for the same pair bounces with 409 build_in_progress and points you at the running build. If a build gets stuck, release its lock:

# Either verb works — both hit the same handler
curl -X DELETE https://app.staticowl.com/api/build/build:1699999999999:ab12cd \
  -H "Authorization: Bearer $STATICOWL_API_KEY" -H "X-Site-Id: site:mysite"

curl -X POST https://app.staticowl.com/api/build/build:1699999999999:ab12cd/cancel \
  -H "Authorization: Bearer $STATICOWL_API_KEY" -H "X-Site-Id: site:mysite"

Cancel releases the lock (so a fresh build can start) and marks the build cancelled. It does not kill compile work already in flight — that finishes on the box on its own. It's a way to unstick the queue, not an abort button.


Custom domains

Preview hosts are enough to launch and share. When you want your own domain, that lives in the site's publishConfig:

field meaning
customDomain your domain (e.g. www.example.com) — drives publicUrl
distributionId the CloudFront distribution serving your domain. Set it to your distribution's id; leave it unset to use the shared preview distribution.
bucket your own S3 bucket (unset = the shared StaticOwl bucket)
bucketRegion region of your own bucket
prefix key prefix inside the bucket (defaults to the site slug)

Who gets one: custom domains are a paid feature — Starter, Pro, and Business. The Free plan is limited to one site on a preview subdomain, with no custom domain — attempting to attach one returns a plan-gate error.

How cutover works: once your domain is configured, StaticOwl provisions its CloudFront distribution and attaches the edge redirect/rewrite function to it. From that point a build sets publicUrl to https://<customDomain> instead of the preview host, and your redirects fire on the live domain automatically — the redirect table is keyed on the custom-domain host the moment the domain is set, so a rule you create applies to both the preview hosts and the live domain. No per-redirect action is needed at cutover.


Deploy targets

Where a build's artifacts go is a separate, operator-level choice controlled by STATICOWL_DEPLOY_TARGET: plain static paths on S3 (static-paths, the default "host anywhere" mode), a managed atomic-release CDN (manifest-pointer), a mirror to a Git repo you own (github), or both. Most sites never need to touch this — the default serves the preview hosts and custom domains described above.

See Deploy targets for the full comparison and the env vars each mode needs.


Gotchas & tips


See also

Related docs

Resolved at build time via {% similar %} — cosine similarity over embeddings, not tag overlap. Zero arguments.

HTTP API
71% match
Accessibility — the WCAG build check & daily sweep
69% match
Deploy targets — where your published site goes
67% match