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].sluginkotao.theme.toml. Becomes part of the published WfP script name (theme-{id}-{version}).version— semver. The build pipeline enforces uniqueness per theme;kotao theme pushwon’t let you overwrite an already-published version.sdkVersion— the SDK major range you target. The publish-time scan rejects a theme whosepackage.jsonresolves an SDK version outside this range.sections— which section types this theme renders. Must cover every type listed inSECTION_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— duplicatestheme.json.id. Two sources sokotao theme pushcan fail fast without parsing the JSON.workspace— optional default for--workspace. Overridable by the--workspace=<slug>flag orKOTAO_WORKSPACEenv 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.