Skip to main content

Build your own theme

Publishing

What kotao theme push actually does — semver, the build pipeline, the dispatch namespace, and how to apply a theme to a site.

kotao theme push is the round-trip from your laptop to a live site. Here’s what happens.

The wire

When you run kotao theme push:

  1. Read projecttheme.json + kotao.theme.toml are validated. Slugs must match; theme.json.version must be valid semver.
  2. Resolve workspace — from --workspace, KOTAO_WORKSPACE, the toml, an interactive picker, or your only workspace.
  3. Ensure theme existsGET /sites/themes checks whether your slug already has a registry entry. If not, POST /sites/themes creates one (slug + display name from theme.json).
  4. Create version rowPOST /sites/themes/{tid}/versions with the semver. Returns a presigned R2 PUT URL valid for 10 minutes and the new version_id.
  5. Pack the source — a deterministic gzipped tarball of your project (50 MB max). Default-ignored: node_modules/, dist/, .git/, .env*, *.log. Add a .kotaoignore for anything else.
  6. PUT to R2 — directly from your laptop. The CLI doesn’t proxy the upload through api-service.
  7. CommitPOST /sites/themes/versions/{vid}/commit. Verifies the R2 object exists and advances the row from awaiting_uploadqueued.
  8. Poll — the CLI polls GET /sites/themes/versions/{vid} every 2 seconds. Status transitions appear inline: queuedbuildingpublishingready (or failed).
  9. Render result — on ready, prints the install hint. On failed, fetches the build log and prints the last 50 lines.

The whole flow typically takes 30-90 seconds for a small theme. Most of that is the build itself.

Semver

theme.json.version is the canonical version. Bump it before every push. The api-service enforces per-theme uniqueness — re-pushing the same version is rejected with a 409.

Conventions:

  • Patch (0.1.0 → 0.1.1) — bug fix, no visible change to authors using the theme.
  • Minor (0.1.0 → 0.2.0) — new sections, new fields, additive change.
  • Major (0.1.0 → 1.0.0) — breaking change. Authors using earlier versions still get the old build; switching them is opt-in via the editor.

There’s no enforcement on what counts as breaking; this is convention only.

The build

The sites-theme-builder service drains your queued row and runs your source through a sandboxed Kubernetes Job in the sites-builds namespace:

  • No service-account token — the build pod can’t talk to the cluster API
  • NetworkPolicy egress-allowlisted — only DNS + npm + R2 are reachable; everything else is blocked
  • 5-minute deadlineactiveDeadlineSeconds; the pod is killed if the build takes longer
  • Resource caps — CPU + memory limits so a runaway build can’t take down the node

Inside the pod: bun install then astro build. Output: a Worker bundle (your theme baked into the runtime), a manifest (theme.json plus build metadata), and the build log. All three uploaded back to R2 via short-lived presigned PUT URLs.

If any of install, build, scan, or WfP upload fails, the row goes failed with an error_summary and your log is preserved for 24 hours.

After ready

The build’s output is a Cloudflare Worker script, named theme-{slug}-{semver-with-dashes}, uploaded to the Kotao WfP dispatch namespace. The storefront dispatcher resolves your theme’s slug to that script name on every render.

Apply your theme to a site (today this is a curl until the editor UI in 4D lands):

curl -X PATCH https://api.kotao.com/v1/workspaces/<workspace>/sites/<site_id> \
     -H 'Authorization: Bearer <session>' \
     -H 'Content-Type: application/json' \
     -d '{"theme_id":"my-theme"}'

The dispatcher picks up the new theme on the site’s next render. No deploy step on your end; no cache to flush.

When the build fails

Common failures and their fixes:

install_failed

bun install returned non-zero. Usually a missing dep, a typo in package.json, or a network blip on the npm registry. The log shows the exact error — fix in your project, push a bumped version.

astro_build_failed

Your theme has a TypeScript error or a runtime error in a section that fires during SSR. The log tail tells you which file. Reproduce locally with astro build from inside the runtime.

scan_rejected

The output bundle failed the publish-time scan. Causes:

  • bundle_too_large — bundle exceeds the size cap. Trim deps or move large assets to R2.
  • invalid_worker_module — Astro didn’t emit a valid Worker module. Re-check astro.config.ts uses the @astrojs/cloudflare adapter.
  • manifest_invalidtheme.json doesn’t validate against the SDK contract. Compare against the scaffolded version.
  • section_coverage_incomplete — your theme declares it implements section type X but no renderer is wired in src/theme.ts. Add it.

wfp_upload_failed

The build succeeded but pushing to WfP failed. Almost always a transient Cloudflare API blip — re-run kotao theme push (bump semver patch; same code, fresh version).