Build your first theme
A theme controls how site-runtime renders a website. Themes depend only on @zcmsorg/theme-sdk and do not access the API or database directly.
Before you start
Section titled “Before you start”Install Node.js 22+, pnpm 10+, and the zcms CLI:
npm install -g @zcmsorg/cliStep 1: Scaffold the project
Section titled “Step 1: Scaffold the project”zcms init ./corporate --kind theme --id com.acme.theme.corporateUse a reverse-DNS id under a domain you control. init asks for anything you leave out, and refuses to write into a directory that already holds something.
You get a theme that builds, typechecks, packs and signs with nothing changed:
corporate/├── theme.json # the manifest├── package.json├── build.mjs # esbuild -> dist/index.mjs + dist/theme.css├── tsconfig.json├── src/│ ├── index.tsx # Layout, templates, blocks, SEO│ ├── theme.css│ └── locales/en.json└── .gitignore # ignores *.pem — your signing key lives here latercd corporatepnpm installpnpm buildThe rest of this page explains what the generated files contain, so you can change them with confidence.
Step 2: Declare theme capabilities
Section titled “Step 2: Declare theme capabilities”Create theme.json at the package root. The required fields are id, name, version, author, engine, templates, menuLocations and settingsSchema. For a theme entry must be dist/index.mjs — see the warning above — and styles points at the stylesheet the theme ships.
{ "id": "com.example.theme.corporate", "name": "Corporate", "version": "0.1.0", "description": "Responsive corporate theme.", "author": { "name": "Example Studio", "url": "https://example.com" }, "engine": ">=0.1.0", "entry": "dist/index.mjs", "styles": "dist/theme.css", "templates": ["home", "page", "post", "archive", "notFound", "error"], "menuLocations": [ { "key": "primary", "name": "Primary menu" }, { "key": "footer", "name": "Footer menu" } ], "settingsSchema": { "type": "object", "properties": { "primaryColor": { "type": "string", "title": "Primary colour", "format": "color", "default": "" } } }, "media": { "screenshots": ["screenshots/home.png"] }}Optional fields include seo, media, demo and optionalCapabilities. Keep template names, menu keys and setting keys stable across updates because sites can continue referencing them.
Step 3: Build templates and blocks
Section titled “Step 3: Build templates and blocks”Implement the home, page, post, archive, error, and fallback templates with @zcmsorg/theme-sdk types. A theme does not fetch Pages or Blogs itself: site-runtime passes a matched item as content, a listing as archive, and shared site data through ctx.
Start with Render pages and blog posts for working examples of content.data, content.blocks, pagination, menus, and localized URLs. If the theme ships sample data, follow Provide demo content. If it supports optional plugin features, continue with Integrate a theme with plugins.
- Render only data supplied by
site-runtime. - Escape untrusted content according to its field type.
- Provide an accessible heading structure, keyboard navigation and visible focus states.
- Define responsive behavior for narrow and wide viewports.
- Provide a safe fallback when optional content, media or menu data is missing.
Step 4: Define settings
Section titled “Step 4: Define settings”Define theme settings with JSON Schema. Admin generates the settings form from this schema, so a theme can add configuration without changing admin-web.
Keep settings backward compatible. Add defaults for new fields, do not silently change the meaning of an existing field, and document any required migration.
Step 5: Add assets and translations
Section titled “Step 5: Add assets and translations”- Include only assets used by the theme.
- Optimize images and fonts before packaging.
- Use translation keys instead of hard-coded UI text.
- Provide a default message for every key.
- Keep user content separate from theme translation messages.
Step 6: Preview locally
Section titled “Step 6: Preview locally”Build the theme and load the local development build on a test site. Preview at least:
- Home, listing, detail and 404 pages.
- Empty, minimal and long-content states.
- Every supported locale.
- Desktop and mobile layouts.
- Light and dark brand assets where supported.
- Metadata, Open Graph, robots policy and JSON-LD output.
Switch away from the theme and back again to confirm that activation does not delete content or settings.
Step 7: Test and package
Section titled “Step 7: Test and package”Run typecheck, lint, unit tests, accessibility checks and visual regression tests. Then:
-
Update the version and changelog.
-
Build from a clean checkout with the committed lockfile.
-
Generate the publisher key pair once if needed:
Terminal window zcms keygen --out ./keys -
Copy
theme.json,distand required assets into a clean release directory, then pack it:Terminal window zcms pack ./build/package --kind theme \--key ./keys/publisher-private.pem \--pub ./keys/publisher-public.pem \--out ./release/corporate-0.1.0.zcms -
Verify the publisher signature:
Terminal window zcms verify ./release/corporate-0.1.0.zcms -
Record the checksum printed by
zcms pack.
Marketplace runs the static scanner after upload and adds its co-signature during intake. Continue with Publish a package.