OpeNext CMS · Developer Docs

Theme Upload

Upload full site themes as Theme Engine ZIP packages. Upload first, run npm install for shared dependencies, then activate to go live.

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.

StepWhat happens
1. Upload ZIPValidates package and registers theme (inactive until you activate)
2. npm installOnly if you add npm packages to CMS root package.json manually or via uploaded plugins that declare dependencies
3. Restart CMSRequired after installing new root dependencies
4. ActivateSets active theme slug in Theme Engine settings; public pages render through Theme Engine
5. PreviewOptional 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:

ToolPurposeUpload?
Theme Engine (Dashboard → Themes)Full React/TSX site themesYes — .zip upload
Theme Builder (Dashboard → Themes → Create / edit)Token colors, typography, variantsNo 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.jsondependencies — 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 plugin

See 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 -Force

Pre-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 place

Success 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 theme

Size and security limits

RuleTheme Engine ZIP
Max upload size60 MB
Max uncompressed50 MB
Max files2,000
Max single file10 MB
Allowed.tsx, .css, .js, fonts, images (.png, .jpg, .svg, .webp, .avif)
.jsonRoot theme.json only
.tstheme.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 →