How theme upload works today
OpeNext CMS has a single dashboard upload path for full themes: Dashboard → Themes (/dashboard/theme-engine). Upload a Theme Engine .zip package containing theme.json, theme.ts, and React/TSX templates. The CMS validates the package, registers it in the database as inactive, and makes it available in Dashboard → Themes. Upload alone never changes the live site.
| Step | What happens |
|---|---|
| 1. Upload ZIP | Validates package and registers theme (inactive until you activate) |
| 2. npm install | Only if you add npm packages to CMS root package.json manually or via uploaded plugins that declare dependencies |
| 3. Restart CMS | Required after installing new root dependencies |
| 4. Activate | Sets active theme slug in Theme Engine settings; public pages render through Theme Engine |
| 5. Preview | Optional live preview in the dashboard before activation |
After upload, click Activate in Dashboard → Themes. Requires Owner or Admin role. If the slug already exists, pass replace=true on the upload API or remove the old theme first.
- Always upload through Dashboard → Themes — do not copy files manually onto the server
- API: POST /api/theme-engine/themes/upload (multipart .zip)
- Activate: POST /api/theme-engine/themes/[slug]/activate
- Theme assets are served at /theme-engine-assets/{slug}/…
- Duplicate slug returns 409 unless replace=true
- Legacy upload routes (/api/themes/upload, /api/themes/import, /api/theme-system/*) are removed
- Built-in fallback theme theme-engine-default is installed automatically when needed
Theme Builder vs Theme Engine upload
These are different tools for different jobs:
| Tool | Purpose | Upload? |
|---|---|---|
| Theme Engine (Dashboard → Themes) | Full React/TSX site themes | Yes — .zip upload |
| Theme Builder (Dashboard → Themes → Create / edit) | Token colors, typography, variants | No file upload — edit in UI |
Use the Theme Builder to duplicate a system theme and tweak design tokens in the dashboard. Use Theme Engine upload when you ship a complete TSX theme package with custom templates, partials, and editable blocks.
Package structure
Every upload must include theme.json at the package root (or inside one wrapper folder). The CMS reads metadata first — missing or invalid JSON fails the upload. Unlike plugins, themes use a single folder per slug (not double-nested).
aura-coffee/ ← or zip one wrapper folder
├── theme.json Required — metadata only
├── theme.ts Required — registration entry
├── templates/
│ ├── Index.tsx Required (index fallback)
│ ├── Home.tsx Optional — homepage
│ └── Page.tsx Optional — inner pages
├── partials/
│ ├── Header.tsx Optional
│ └── Footer.tsx Optional
├── blocks/ Required for editable homepage sections
│ └── Hero/
│ ├── Renderer.tsx
│ └── schema.ts
└── assets/
├── style.css Recommended
├── theme.js Optional client script
└── gsap.min.js Browser libs ship here — not node_modules/- Zip so theme.json is at the root, or zip one wrapper folder — both work
- Do not include node_modules, .env, or bundled npm packages in the ZIP
- Browser libraries (GSAP, Three.js) belong in assets/ — the CMS serves them via /theme-engine-assets/{slug}/
- Use forward-slash relative paths; @/ aliases are not supported in theme files
- Only theme.ts may import @cms/theme for defineTheme registration
Shared npm dependencies (themes + plugins)
Uploaded themes and plugins share the CMS root node_modules/. Themes normally ship browser libraries as static files under assets/. Backend plugins declare npm packages in manifest.json → dependencies — the CMS merges them into root package.json on upload.
Typical theme workflow
1. Upload theme ZIP in Dashboard → Themes
2. Activate the theme
3. Public pages render through the Theme Engine
Plugin with npm deps (when using backend plugins)
1. Upload plugin ZIP → CMS updates root package.json
2. npm install at CMS project root
3. Restart CMS → Enable pluginSee Plugin Upload for the full shared-dependencies workflow when your deployment uses plugins that require npm packages at the CMS root.
theme.json (metadata)
{
"name": "My Theme",
"slug": "my-theme",
"author": "Your Company",
"version": "1.0.0",
"description": "A production website theme.",
"compatibleCmsVersion": "^2.0.0",
"entry": "theme.ts",
"supportedFeatures": ["block-editor"],
"editableSections": [
{
"id": "hero",
"label": "Hero",
"blockType": "my-hero",
"defaults": { "title": "Welcome" }
}
]
}Either "entry": "theme.ts" or legacy manifest fields (templates.index, blocks[]) must resolve to a valid index template. New themes should use theme.ts registration. The slug must be unique and match the folder name you use when zipping.
theme.ts registration
import { defineTheme } from '@cms/theme';
export default defineTheme((theme) => {
theme.registerTemplate('index', './templates/Index');
theme.registerTemplate('home', './templates/Home');
theme.registerPartial('header', './partials/Header');
theme.registerPartial('footer', './partials/Footer');
theme.registerBlock({
type: 'my-hero',
rendererPath: './blocks/Hero/Renderer',
schemaPath: './blocks/Hero/schema',
label: 'Hero',
});
theme.registerStyle('./assets/style.css');
});At upload, theme.ts is validated (AST + path checks) but not executed. It runs at render time when the theme is active. Templates, blocks, and assets can also be auto-discovered from conventional folders.
Templates and rendering
templates/Index.tsx — every template must default-export a component:
export default function Index({ theme }: { theme: any }) {
const site = theme.site();
const blocks = theme.blocks();
return (
<main className="my-theme">
{theme.header()}
<h1>{site.name}</h1>
{blocks}
{theme.footer()}
</main>
);
}Template resolution order:
- Homepage: home → page → index
- Normal page: page → index
- Blog post: post → page → index
- index is always required as the final fallback
Theme component API available in templates and partials:
- theme.site() — site name, URL, description
- theme.page() — current page metadata
- theme.menu(key) / theme.navigation(key) — menu items
- theme.settings() — theme setting values
- theme.blocks() — rendered CMS or theme-declared blocks
- theme.asset(path) — URL for an uploaded theme asset
- theme.header() / theme.footer() — resolved partials or built-in fallback
Active theme assets are served at /theme-engine-assets/{slug}/…. Public pages render through the Theme Engine pipeline (themeManager.renderPage()).
Editable blocks
To make homepage sections editable in the block editor, register blocks in theme.ts and list them in editableSections. Each block folder needs a renderer and schema.
// blocks/Hero/schema.ts
const schema = {
fields: [
{ key: 'title', label: 'Title', type: 'text', default: 'Welcome' },
{ key: 'description', label: 'Description', type: 'textarea' },
{ key: 'image', label: 'Background', type: 'image' },
{ key: 'accent', label: 'Accent', type: 'color', default: '#2563eb' },
{ key: 'showButton', label: 'Show button', type: 'boolean', default: true },
{
key: 'alignment',
label: 'Alignment',
type: 'select',
default: 'left',
options: ['left', 'center']
}
]
};
export default schema;Supported schema field types: text, textarea, color, image, number, boolean, url, select, list. Templates must call theme.blocks() to render editable sections.
Upload from the dashboard
- 1. Log in as Owner or Admin
- 2. Open Dashboard → Themes
- 3. Click Upload and select your .zip file
- 4. If theme.json lists dependencies, run npm install at CMS root and restart
- 5. Click Activate — public pages switch to your TSX templates
- If the slug already exists, confirm replace or pick a new slug in theme.json
# PowerShell — create an upload-ready ZIP
Compress-Archive -Path .\aura-coffee\* -DestinationPath .\aura-coffee.zip -Force
# Or zip one wrapper folder
Compress-Archive -Path .\aura-coffee -DestinationPath .\aura-coffee.zip -ForcePre-upload checklist:
- theme.json present with valid JSON, name, slug, and version
- theme.ts registers at least templates/Index
- templates/Index.tsx default-exports a React component
- No node_modules, .env, .php, .sh, or executables
- Paths use forward slashes, relative to theme root
Upload API
Authenticated dashboard endpoint (same flow as the UI):
POST /api/theme-engine/themes/upload
Content-Type: multipart/form-data
file: <theme.zip>
replace: true Optional — upgrade existing slug in placeSuccess response (200):
{
"data": {
"slug": "aura-coffee",
"name": "Aura Coffee",
"version": "1.0.0",
"status": "installed",
"installLog": [ ... ]
},
"error": null
}Activate after upload:
POST /api/theme-engine/themes/aura-coffee/activate
Content-Type: application/json
{ "applyToEditor": false }Other routes: GET /api/theme-engine/themes, DELETE /api/theme-engine/themes/[slug], POST /api/theme-engine/themes/[slug]/preview. See the API Reference.
Runtime loading sequence
Upload ZIP
→ Validate theme.json, paths, theme.ts, and allowed file types
→ Register theme (inactive until activate)
Activate (Dashboard → Themes)
→ Set as the active theme for the live site
Render (request time)
→ Execute theme.ts → compile templates → resolve template (home → page → index)
Fallback
→ If the active theme fails, CMS retries with the built-in default themeSize and security limits
| Rule | Theme Engine ZIP |
|---|---|
| Max upload size | 60 MB |
| Max uncompressed | 50 MB |
| Max files | 2,000 |
| Max single file | 10 MB |
| Allowed | .tsx, .css, .js, fonts, images (.png, .jpg, .svg, .webp, .avif) |
| .json | Root theme.json only |
| .ts | theme.ts, blocks/**/schema.ts, editor/**, hooks/** only |
| Blocked | .html, .exe, .php, .sh, .bat, node_modules/, undeclared npm imports |
Common errors
- 400 — invalid JSON, missing theme.json, bad paths, blocked file type, theme.ts validation failed
- 403 — user is not Owner or Admin
- 409 — duplicate theme slug (replace existing or change slug in theme.json)
- 413 — ZIP or uncompressed content exceeds size limits
- Blank public page — verify templates/Index.tsx default-exports a component
- Editable sections missing — register blocks in theme.ts and call theme.blocks() in templates
- Assets 404 — use theme.asset('assets/...') so URLs resolve through /theme-engine-assets/your-slug/
- Default theme missing — restart the CMS; the built-in fallback theme is restored automatically
- Template resolution failed — ensure templates/Index.tsx exists and theme.ts registers index template
Related guides
For in-dashboard token editing (no ZIP upload), see the Theme Builder. For dashboard plugins and shared npm dependencies, see Plugin Upload.
Full package specification in the CMS repository: docs/theme-upload-format.md on GitHub →