Theme Development
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:
{
"$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"
}
}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:
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>
);
}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>
);
}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);
/* ... */
}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:
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}
/>
);
}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.