Theme anatomy

The four files a theme ships — theme.json, billing.json, icon.svg and the Nuxt app — what the manifest declares, and why a published version can never change.

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.

theme.json
{
  "$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

FieldWhat it does
kindAlways "theme". Together with engine it's the marker Cockpit reads to recognise a theme.
engineWhich engine drives the site — see below.
nameLowercase, digits and hyphens. The registry id, unique within your vendor.
vendorThe publisher. A vendor namespace is claimed by the first tenant that registers under it.
versionStrict semver, and immutable once published — see below.
titleThe display name in Cockpit and in the Marketplace.
typepublic — 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.
iconPath to the icon, relative to the theme root.
siteHow 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.
blokkliThe 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.

engineWhat it means
blokkliA Nuxt page-builder theme. Offered in the Cockpit page editor, and may ship the blokkli payload. This is what a commerce storefront uses.
staticA fixed Nuxt site with no editor — a landing page, a placeholder, a microsite.
customA 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.

billing.json
{
  "$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:

nuxt.config.ts
export default defineNuxtConfig({
  extends: ['@revenexx/cover'],
})

And it renames the root element, because the visual editor anchors to #nuxt-root:

nuxt.config.ts
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

Was this page helpful?