Skip to main content

Build your own theme

Theme anatomy

What every file in a scaffolded Kotao theme does — and why.

A scaffolded theme looks like this:

my-theme/
├── theme.json            ← runtime contract
├── kotao.theme.toml      ← CLI metadata
├── package.json          ← npm deps (SDK + section-schemas + astro)
├── astro.config.ts       ← Cloudflare adapter
├── src/
│   ├── theme.ts          ← defineTheme() entry — wires sections together
│   ├── sections/
│   │   └── hero/Section.astro
│   ├── layouts/
│   │   └── Page.astro
│   └── styles/theme.css
├── README.md
└── .gitignore

theme.json

The runtime contract. Read at build time by the server-side scan and at render time by the storefront. Required fields:

{
  "id": "my-theme",
  "displayName": "My Theme",
  "version": "0.1.0",
  "sdkVersion": "^0.1.0",
  "sections": ["hero", "text_with_image"]
}
  • id — the theme slug. Must match [theme].slug in kotao.theme.toml. Becomes part of the published WfP script name (theme-{id}-{version}).
  • version — semver. The build pipeline enforces uniqueness per theme; kotao theme push won’t let you overwrite an already-published version.
  • sdkVersion — the SDK major range you target. The publish-time scan rejects a theme whose package.json resolves an SDK version outside this range.
  • sections — which section types this theme renders. Must cover every type listed in SECTION_REGISTRY (see section schemas); defineTheme() throws at startup if any are missing.

kotao.theme.toml

CLI-only metadata — never read by the runtime.

[theme]
slug = "my-theme"
workspace = "acme"
  • slug — duplicates theme.json.id. Two sources so kotao theme push can fail fast without parsing the JSON.
  • workspace — optional default for --workspace. Overridable by the --workspace=<slug> flag or KOTAO_WORKSPACE env var.

package.json

A normal npm package manifest pinning two deps:

{
  "dependencies": {
    "@kotao/sites-section-schemas": "^0.1.0",
    "@kotao/sites-theme-sdk": "^0.1.0",
    "astro": "^6.3.0"
  }
}

Pin majors, not exact versions — minor SDK updates ship section additions and helpers you’ll want without breaking your theme.

src/theme.ts

The entry point. Every theme must export a Theme object as the default export from this file:

import { defineTheme } from "@kotao/sites-theme-sdk";
import Hero from "./sections/hero/Section.astro";

export default defineTheme({
  id: "my-theme",
  displayName: "My Theme",
  sections: {
    hero: Hero,
  },
  tokenManifest: [{ name: "color-primary", type: "color", default: "#111" }],
});

defineTheme() validates at construction time that every section type in SECTION_REGISTRY has a renderer here. A missing renderer throws a clear error at startup rather than silently 500-ing on a page that needed it.

src/sections/<type>/Section.astro

One per section type your theme implements. Each receives a typed data prop:

---
import type { SectionRendererProps } from "@kotao/sites-theme-sdk";
import { sectionMarkerAttrs } from "@kotao/sites-theme-sdk";

interface Props extends SectionRendererProps<"hero"> {}
const { data, sectionId } = Astro.props;
---

<section {...sectionMarkerAttrs(sectionId)}>
  <h1>{data.heading}</h1>
  {data.subheading && <p>{data.subheading}</p>}
</section>

sectionMarkerAttrs(sectionId) stamps data-section-id so the editor can highlight the right block when an author clicks inside the live preview.

src/layouts/Page.astro

The shell every page renders into. Imports your global stylesheet, sets the document head, drops a <slot /> where the section list goes.

src/styles/theme.css

Site-wide styles + tokens. Sections own their own scoped styles inline; this file holds the small set of resets and CSS variables every page needs.

.kotaoignore (optional)

Same syntax as .gitignore. Excluded from the tarball that kotao theme push uploads. Default-ignored: node_modules/, dist/, .git/, .env*, *.log. Add anything else your project generates that doesn’t need to ship.