Theme anatomy
A theme is four things on disk: a manifest, a billing file, an icon, and a Nuxt 4 application. The platform reads the first three; the fourth is the storefront your customer's buyers actually use.
revenexx create theme <name> writes all four. This page explains what they contain.
theme.json — the manifest
The manifest is the contract between your theme and the platform. It says this is a theme, how to build it, what it needs to function, and which blocks editors may compose pages from.
{
"$schema": "https://schemas.revenexx.com/theme.schema.json",
"kind": "theme",
"engine": "blokkli",
"name": "my-storefront",
"vendor": "my-agency",
"version": "1.0.0",
"title": "My Storefront",
"description": "A Nuxt storefront that renders a tenant's products and pages.",
"icon": "icon.svg",
"type": "public",
"site": {
"framework": "nuxt",
"adapter": "ssr",
"installCommand": "npm install",
"buildCommand": "npm run build",
"outputDirectory": ".output",
"domains": ["{{tenant_domain}}"]
},
"requires": [
{
"capability": "products.list",
"compatible": "^1.0",
"reason": "renders the product grid on the home and category pages"
}
],
"permissions": [
{ "capability": "products.list", "compatible": "^1.0" },
{ "entity": "pages", "access": ["read"] },
{ "storage": "theme-media", "access": ["read"] }
],
"blokkli": {
"blocks": [
{ "id": "hero", "title": "Hero", "description": "Full-width hero banner with headline, subline, and a call-to-action." }
],
"presets": [
{ "id": "home", "title": "Home page", "blocks": ["hero"] }
]
}
}
Point your editor at the $schema URL and you get validation and autocompletion for free.
The fields that matter
| Field | What it does |
|---|---|
kind | Always "theme". Together with engine it's the marker Cockpit reads to recognise a theme. |
engine | Which engine drives the site — see below. |
name | Lowercase, digits and hyphens. The registry id, unique within your vendor. |
vendor | The publisher. A vendor namespace is claimed by the first tenant that registers under it. |
version | Strict semver, and immutable once published — see below. |
title | The display name in Cockpit and in the Marketplace. |
type | public — any tenant may install it once the operator publishes it, and it's listed in the Marketplace. private — only the owner tenant may install it, never listed. |
icon | Path to the icon, relative to the theme root. |
site | How the platform builds and serves the front end. framework and adapter are required; the commands and output directory default per framework. |
requires[] | Capabilities the theme needs to install. A hard dependency. |
permissions[] | What the theme is granted access to, consented at install time. |
blokkli | The editor payload — the blocks and presets an editor sees. Opaque to the platform. |
requires[] is the one that surprises people. A theme that requires products.list cannot be installed on a tenant that has no products app routed: Cockpit blocks the install and names the app that provides the capability. Declare only what the theme genuinely can't run without, and put the reason in reason — a human reads it during the install.
engine: blokkli versus static
engine tells the platform what kind of site this is, and it decides whether the theme appears in the Cockpit page editor at all.
engine | What it means |
|---|---|
blokkli | A Nuxt page-builder theme. Offered in the Cockpit page editor, and may ship the blokkli payload. This is what a commerce storefront uses. |
static | A fixed Nuxt site with no editor — a landing page, a placeholder, a microsite. |
custom | A bespoke site that owns its own editing and data flow. |
Only a blokkli theme gets the visual editor. If you set engine: "static" and then wonder why Experience Studio → Pages won't open your theme, that's why.
The site block
The front end is served over the theme's own domain on the public sites entrypoint, with the tenant resolved from the request host. It is never served through the API gateway — only the theme's data calls traverse the gateway, and those go out from your server routes.
framework and adapter are required. The install command, build command, and output directory have per-framework defaults; override them here when your project differs. Full table: Build configuration.
version immutability
A published theme version can never change. The registry stores it, tenants install it, and a build that tries to re-register the same version is rejected as already registered.
The consequence for your workflow: bump version in theme.json before you publish a manifest change. Code-only changes that don't touch the manifest deploy normally — the immutability applies to the published registry version, not to every deployment.
If you see "already registered" on publish, that's the whole story: bump the version and ship again. More in Publishing a theme.
billing.json — pricing only
billing.json sits next to the manifest and carries the Marketplace commercial terms. It is deliberately separate from theme.json#type: whether a theme is public and whether it costs money are independent decisions.
{
"$schema": "https://schemas.revenexx.com/billing.schema.json",
"type": "free",
"support": { "email": "support@my-agency.com", "url": "https://my-agency.com/support" },
"categories": ["theme", "storefront"],
"available_countries": ["*"]
}
icon.svg
An SVG shown next to the theme in Cockpit and the Marketplace. The path is whatever theme.json#icon points at; the scaffold uses icon.svg at the theme root.
The Nuxt application
Everything else in the directory is a normal Nuxt 4 project — with two revenexx-specific expectations.
It extends a base layer:
export default defineNuxtConfig({
extends: ['@revenexx/cover'],
})
And it renames the root element, because the visual editor anchors to #nuxt-root:
export default defineNuxtConfig({
app: {
rootId: 'nuxt-root',
},
})
Blocks live at app/components/blokkli/<bundle>/index.vue, with snake_case bundle names. Server routes live under server/api/, and they're where every platform call happens.
Next steps
- The layer chain — which base layer to extend, and what each one gives you.
- Overriding the base layer — replacing an inherited component.
- Registering blocks — the
blokkli.blocks[]andblokkli.presets[]arrays in depth. - Publishing a theme — registry, marketplace, install, activation.
- Build your first theme — the tutorial that walks this manifest line by line.