Build your first Theme

Scaffold a Theme from the sample, understand its manifest, run it locally, deploy a first version, and activate it on a domain.

This tutorial takes you from nothing to a live Theme. You'll start from the sample Theme, learn what the manifest declares, run it locally, and ship a first deployment you can open in a browser.

This page is for frontend developers building a Storefront's front end. You need Node.js 22+, npm, and a revenexx account with a tenant. If you want to deploy from your terminal, install and sign in to the revenexx CLI first.

1. Scaffold from the sample Theme

The fastest start is the sample Theme — a minimal, runnable Nuxt SSR site with a valid manifest. Copy it into a new project directory and install dependencies.

Scaffold
# Start from the sample Theme as your own repository
npx degit revenexx/sample-theme my-theme
cd my-theme
npm install

The sample is deliberately small: a single page, a manifest, and a billing file. You'll add blocks and styling on top of it in the next tutorials.

2. Understand theme.json

theme.json is the manifest. It tells the platform that this site is a Theme, what it needs to run, and which blocks editors can compose pages with. Open it.

theme.json
{
  "$schema": "https://schemas.revenexx.com/theme.schema.json",
  "kind": "theme",
  "engine": "blokkli",
  "name": "my-theme",
  "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 storefront 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." },
      { "id": "product-grid", "title": "Product Grid", "description": "Renders products from the products.list capability.", "requiresCapability": "products.list" },
      { "id": "rich-text", "title": "Rich Text", "description": "A WYSIWYG text block for editorial content." }
    ],
    "presets": [
      { "id": "home", "title": "Home page", "blocks": ["hero", "product-grid", "rich-text"] }
    ]
  }
}

The fields that matter:

FieldWhat it does
kind: "theme" + engine: "blokkli"Marks the site as a Blökkli Theme so Cockpit lists it in the Theme marketplace.
name, vendor, versionIdentity. version is strict semver — a published version is immutable, so bump it before you re-publish.
siteHow the platform builds and runs the site: Nuxt, server-side rendering, npm install + npm run build, output in .output.
requires[]Capabilities the Theme needs to install. A Theme that requires products.list can't be installed on a tenant that has no products app routed — Cockpit blocks the install and points the customer at the app that provides it.
permissions[]What the Theme is granted access to, consented at install time.
blokkli.blocks[]The blocks editors can place on pages. Each id maps to a block component you write.
blokkli.presets[]Starter page layouts an editor can pick from.

billing.json sits next to it and carries pricing only. The sample ships a free Theme:

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": ["*"]
}

3. Run it locally

A Theme is a Nuxt app, so the dev loop is the standard one.

Dev server
npm run dev

Open the printed URL. The sample page server-renders and shows the request context it received. The build config is plain Nuxt SSR — ssr: true with the node server output that the platform runs in production:

nuxt.config.ts
export default defineNuxtConfig({
  compatibilityDate: '2025-01-01',
  // Server-side rendering, served by the Nuxt server in production.
  ssr: true,
  devtools: { enabled: false },
})

To check the production output before you ship, build and preview it the same way the platform will run it:

Build and preview
npm run build      # → .output
npm run preview    # serves .output/server/index.mjs locally

4. Deploy a first version

You have two ways to ship a Theme. Pick one now; Deploy your storefront covers both in depth.

Git-connected (recommended). Push your Theme repository to your Git provider and connect it to a Storefront. Every push to your production branch builds (npm install && npm run build) and deploys automatically. Full walkthrough: Deploy from Git.

From the revenexx CLI. Deploy straight from your terminal — useful for a first push or for CI. Full walkthrough: Deploy from CLI.

Deploy from the CLI
revenexx login --token <your-token> --tenant <your-tenant-slug>   # if you haven't already
# then create a site deployment — see the Deploy from CLI guide

Either path produces a deployment with a preview URL on the generated *.sites.revenexx.io domain. Open it to confirm the Theme runs in production.

5. Activate it on a domain

A successful deployment lists your Theme in the Cockpit Theme marketplace. To put it on a real domain:

  1. In Cockpit → Experience Studio → Marketplace, install your Theme on the tenant. It then appears under Installed Themes.
  2. Activate it for the domain you want it to serve.

From then on, requests to that domain are served by your Theme, with the tenant resolved per request.

Troubleshooting

  • Install is blocked on the tenant. The Theme requires a capability (e.g. products.list) the tenant doesn't have routed yet. Install the app that provides it — Cockpit names it in the error — then retry.
  • "Already registered" when you re-publish. A published Theme version is immutable. Bump version in theme.json and ship again.
  • Build runs locally but fails on deploy. The platform runs npm install (not a workspace install), so every dependency must resolve from npm. Commit your lockfile and avoid workspace-only links.

What's next

Was this page helpful?