Theme Development

12 min readUpdated 27 Apr 2026

A VeloCMS theme is a TypeScript package that exports three React components (BlogLayout, PostLayout, PageLayout) plus a theme.json manifest. That's the entire contract. You don't need to understand PocketBase, multi-tenant routing, or how ISR works — just build your layout components and wire them to the manifest. VeloCMS handles the rest.

The theme.json manifest

Every theme starts with a theme.json. This is what the marketplace reads to understand your theme, validate compatibility, and generate the preview card. The full ThemeManifest interface lives in src/lib/themes/sdk/types.ts — here's a minimal example:

theme.json
{
  "$schema": "https://velocms.org/schemas/theme-manifest.json",
  "name": "velocms-theme-aurora",
  "displayName": "Aurora",
  "version": "1.0.0",
  "description": "A clean, distraction-free reading theme with subtle gradient accents.",
  "author": {
    "name": "Jane Dev",
    "email": "[email protected]",
    "url": "https://janedev.io"
  },
  "type": "layout",
  "category": "personal",
  "tags": ["minimal", "reading", "personal-blog"],
  "engines": {
    "velocms": ">=1.0.0"
  },
  "preview": {
    "thumbnail": "./preview/thumbnail.png",
    "screenshots": [
      "./preview/blog-listing.png",
      "./preview/post-detail.png"
    ]
  },
  "exports": {
    "components": {
      "BlogLayout": "./src/BlogLayout.tsx",
      "PostLayout": "./src/PostLayout.tsx",
      "PageLayout": "./src/PageLayout.tsx"
    }
  },
  "pricing": {
    "model": "free"
  }
}
json

Layout component contracts

Your three layout components receive typed props — VeloCMS passes posts, site settings, and member session data through these interfaces. You don't fetch anything yourself. The types come from src/components/themes/types.ts:

src/BlogLayout.tsx
import type { BlogLayoutProps } from "@velocms/theme-sdk";

export default function BlogLayout({
  posts,
  settings,
  siteUrl,
  searchEnabled,
  member,
  currentPage,
  totalPages,
}: BlogLayoutProps) {
  return (
    <main>
      <h1>{settings?.site_name ?? "My Blog"}</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <a href={`${siteUrl}/blog/${post.slug}`}>{post.title}</a>
          </li>
        ))}
      </ul>
    </main>
  );
}
typescript
src/PostLayout.tsx
import type { PostLayoutProps } from "@velocms/theme-sdk";

export default function PostLayout({
  post,
  settings,
  siteUrl,
  jsonLd,
  member,
  relatedPosts,
}: PostLayoutProps) {
  return (
    <article>
      <h1>{post.title}</h1>
      {/* jsonLd is a pre-built schema object — pass it to a <script> tag */}
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      <div dangerouslySetInnerHTML={{ __html: post.content_html ?? "" }} />
    </article>
  );
}
typescript

OKLCH token system

VeloCMS uses OKLCH for all color tokens. This isn't just aesthetic preference — OKLCH gives you perceptually uniform color scales, which means your brand color at 30% lightness is actually 30% as bright as your base, not the weird dark mess you get from HSL. Every theme has access to these CSS variables, defined in globals.css:

/* Core semantic tokens — always available in theme components */
:root {
  --background: oklch(0.98 0 0);
  --foreground: oklch(0.15 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.15 0 0);
  --muted: oklch(0.96 0.005 240);
  --muted-foreground: oklch(0.55 0.01 240);
  --border: oklch(0.9 0.005 240);
  --ring: oklch(0.55 0.16 264);
  --primary: oklch(0.55 0.16 264);
  --primary-foreground: oklch(0.98 0 0);
}

/* Dark mode — automatically applied via .dark class on <html> */
.dark {
  --background: oklch(0.12 0.005 240);
  --foreground: oklch(0.95 0 0);
  /* ... */
}
css

In your theme components, use Tailwind classes that reference these tokens: bg-background, text-foreground, border-border, and so on. Don't hardcode hex values — if you do, your theme won't respect the user's dark/light mode setting and it'll look broken in the preview.

Local preview with DemoFixtures

Testing your theme against real-looking data before submitting it to the marketplace is straightforward. The Theme SDK exports a DemoFixtures object with synthetic posts, site settings, and member session data. Pass it to your layout components during development:

preview/index.tsx
import { DemoFixtures } from "@velocms/theme-sdk/testing";
import BlogLayout from "../src/BlogLayout";

// Use the DemoFixtures for local development
export default function Preview() {
  return (
    <BlogLayout
      posts={DemoFixtures.posts}
      settings={DemoFixtures.settings}
      siteUrl="http://localhost:3000"
      searchEnabled={false}
      member={null}
      currentPage={1}
      totalPages={1}
    />
  );
}
typescript

The /preview/themes/[slug] route on a running VeloCMS instance does the same thing — it loads your registered theme and renders it against DemoFixtures. That's how the marketplace thumbnail is generated.

Marketplace submission

When you're ready to publish, head to /developers on VeloCMS. The submission flow asks for your theme.json manifest, a 600x400 thumbnail, at least two full-page screenshots, and a short description. Approval takes 2-5 business days — we review for accessibility, dark mode support, and mobile responsiveness before listing.

The revenue split is 80/20: you keep 80% of every sale, VeloCMS takes 20%. Free themes are listed without any revenue sharing. Payments are processed monthly via Stripe Connect, so you'll need a Stripe account when you submit.