create-velocms-theme 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +69 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +43 -0
- package/dist/prompts.d.ts +17 -0
- package/dist/prompts.js +73 -0
- package/dist/scaffold.d.ts +2 -0
- package/dist/scaffold.js +370 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VeloCMS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# create-velocms-theme
|
|
2
|
+
|
|
3
|
+
Scaffold a new VeloCMS theme project ready for npm publish.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx create-velocms-theme my-theme
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or install globally:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g create-velocms-theme
|
|
15
|
+
create-velocms-theme my-theme
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Prompts
|
|
19
|
+
|
|
20
|
+
The CLI will ask you:
|
|
21
|
+
|
|
22
|
+
1. **Theme name** — human-readable display name (e.g. "Minimal Portfolio")
|
|
23
|
+
2. **Theme slug** — kebab-case package suffix (e.g. "minimal-portfolio")
|
|
24
|
+
3. **Theme type** — `layout` | `niche` | `pack`
|
|
25
|
+
4. **Niche category** — (only if type = niche) photographer | podcaster | restaurant | ...
|
|
26
|
+
5. **Author name**
|
|
27
|
+
6. **Author email**
|
|
28
|
+
7. **License** — default: MIT
|
|
29
|
+
8. **Short description**
|
|
30
|
+
|
|
31
|
+
## Non-interactive mode
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx create-velocms-theme my-theme --non-interactive
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Uses sensible defaults for CI environments.
|
|
38
|
+
|
|
39
|
+
## Generated scaffold
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
my-theme/
|
|
43
|
+
├── package.json # velocms-theme-my-theme, peerDeps: react@19, next@16
|
|
44
|
+
├── theme.manifest.json # ThemeManifest (spec §4)
|
|
45
|
+
├── tsconfig.json
|
|
46
|
+
├── tsconfig.build.json
|
|
47
|
+
├── README.md
|
|
48
|
+
├── .gitignore
|
|
49
|
+
├── preview/ # Place thumbnail.png + screenshots here
|
|
50
|
+
└── src/
|
|
51
|
+
├── index.ts # Re-exports BlogIndex, PostLayout
|
|
52
|
+
├── index.test.ts # Smoke test (vitest) — extend as your theme grows
|
|
53
|
+
├── BlogIndex.tsx # Blog listing layout (TODO markers)
|
|
54
|
+
├── PostLayout.tsx # Single post layout (TODO markers)
|
|
55
|
+
├── theme.config.ts # Tailwind preset overrides
|
|
56
|
+
└── styles.css # Global CSS tokens
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Building
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm run build # compiles src/ → dist/
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Submitting to marketplace
|
|
66
|
+
|
|
67
|
+
1. `npm run build`
|
|
68
|
+
2. Verify `theme.manifest.json` is complete
|
|
69
|
+
3. `npx velocms-cli theme submit` (coming Q3 2026)
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { collectPrompts } from "./prompts.js";
|
|
4
|
+
import { scaffoldTheme } from "./scaffold.js";
|
|
5
|
+
async function main() {
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const nonInteractive = args.includes("--non-interactive");
|
|
8
|
+
const nameArg = args.find((a) => !a.startsWith("-"));
|
|
9
|
+
console.log("\nVeloCMS Theme Scaffolder\n");
|
|
10
|
+
let cfg;
|
|
11
|
+
try {
|
|
12
|
+
cfg = await collectPrompts(nameArg, nonInteractive);
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
console.error("Failed to collect prompts:", err);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const outputDir = join(process.cwd(), cfg.slug);
|
|
19
|
+
console.log(`\nScaffolding theme "${cfg.name}" (${cfg.type}) into ./${cfg.slug}/`);
|
|
20
|
+
try {
|
|
21
|
+
scaffoldTheme(outputDir, cfg);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (err instanceof Error) {
|
|
25
|
+
console.error(`Error: ${err.message}`);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
console.error("Unknown error:", err);
|
|
29
|
+
}
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
console.log(`\nDone! Theme scaffold created at ./${cfg.slug}/`);
|
|
33
|
+
console.log(` cd ${cfg.slug}`);
|
|
34
|
+
console.log(" npm install");
|
|
35
|
+
console.log(" npm run dev");
|
|
36
|
+
console.log("\nWhen ready to submit:");
|
|
37
|
+
console.log(" npm run build");
|
|
38
|
+
console.log(" npx velocms-cli theme submit (coming soon)\n");
|
|
39
|
+
}
|
|
40
|
+
main().catch((err) => {
|
|
41
|
+
console.error(err);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export interface ThemeConfig {
|
|
3
|
+
name: string;
|
|
4
|
+
slug: string;
|
|
5
|
+
type: "layout" | "niche" | "pack";
|
|
6
|
+
authorName: string;
|
|
7
|
+
authorEmail: string;
|
|
8
|
+
license: string;
|
|
9
|
+
description: string;
|
|
10
|
+
niche?: string;
|
|
11
|
+
}
|
|
12
|
+
declare const VALID_TYPES: readonly ["layout", "niche", "pack"];
|
|
13
|
+
declare const NICHE_CATEGORIES: readonly ["photographer", "podcaster", "restaurant", "creator", "writer", "agency", "developer", "indie-maker"];
|
|
14
|
+
export type ThemeType = (typeof VALID_TYPES)[number];
|
|
15
|
+
export type NicheCategory = (typeof NICHE_CATEGORIES)[number];
|
|
16
|
+
export declare function collectPrompts(nameArg?: string, nonInteractive?: boolean): Promise<ThemeConfig>;
|
|
17
|
+
export {};
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
const VALID_TYPES = ["layout", "niche", "pack"];
|
|
4
|
+
const NICHE_CATEGORIES = [
|
|
5
|
+
"photographer",
|
|
6
|
+
"podcaster",
|
|
7
|
+
"restaurant",
|
|
8
|
+
"creator",
|
|
9
|
+
"writer",
|
|
10
|
+
"agency",
|
|
11
|
+
"developer",
|
|
12
|
+
"indie-maker",
|
|
13
|
+
];
|
|
14
|
+
function ask(question, defaultValue) {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
17
|
+
const query = defaultValue ? `${question} (${defaultValue}): ` : `${question}: `;
|
|
18
|
+
rl.question(query, (answer) => {
|
|
19
|
+
rl.close();
|
|
20
|
+
resolve(answer.trim() || defaultValue || "");
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function toSlug(str) {
|
|
25
|
+
return str
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
28
|
+
.replace(/^-+|-+$/g, "");
|
|
29
|
+
}
|
|
30
|
+
export async function collectPrompts(nameArg, nonInteractive = false) {
|
|
31
|
+
let name = nameArg ?? "";
|
|
32
|
+
if (!name && !nonInteractive) {
|
|
33
|
+
name = await ask("Theme name", "my-theme");
|
|
34
|
+
}
|
|
35
|
+
if (!name) {
|
|
36
|
+
name = "my-theme";
|
|
37
|
+
}
|
|
38
|
+
const defaultSlug = toSlug(name);
|
|
39
|
+
const slug = nonInteractive
|
|
40
|
+
? defaultSlug
|
|
41
|
+
: await ask("Theme slug (kebab-case)", defaultSlug);
|
|
42
|
+
let type = "layout";
|
|
43
|
+
if (!nonInteractive) {
|
|
44
|
+
const typeAnswer = await ask("Theme type — layout | niche | pack", "layout");
|
|
45
|
+
if (VALID_TYPES.includes(typeAnswer)) {
|
|
46
|
+
type = typeAnswer;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
console.log(`Unknown type "${typeAnswer}", defaulting to "layout"`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
let niche;
|
|
53
|
+
if (type === "niche") {
|
|
54
|
+
if (!nonInteractive) {
|
|
55
|
+
const nicheAnswer = await ask(`Niche category — ${NICHE_CATEGORIES.join(" | ")}`, "photographer");
|
|
56
|
+
niche = nicheAnswer || "photographer";
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
niche = "photographer";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const authorName = nonInteractive
|
|
63
|
+
? "Theme Author"
|
|
64
|
+
: await ask("Author name", "Theme Author");
|
|
65
|
+
const authorEmail = nonInteractive
|
|
66
|
+
? "author@example.com"
|
|
67
|
+
: await ask("Author email", "author@example.com");
|
|
68
|
+
const license = nonInteractive ? "MIT" : await ask("License", "MIT");
|
|
69
|
+
const description = nonInteractive
|
|
70
|
+
? `A VeloCMS ${type} theme`
|
|
71
|
+
: await ask("Short description", `A VeloCMS ${type} theme`);
|
|
72
|
+
return { name, slug, type, authorName, authorEmail, license, description, niche };
|
|
73
|
+
}
|
package/dist/scaffold.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
function packageJsonContent(cfg) {
|
|
4
|
+
return JSON.stringify({
|
|
5
|
+
name: `velocms-theme-${cfg.slug}`,
|
|
6
|
+
version: "1.0.0",
|
|
7
|
+
description: cfg.description,
|
|
8
|
+
type: "module",
|
|
9
|
+
main: "./dist/index.js",
|
|
10
|
+
types: "./dist/index.d.ts",
|
|
11
|
+
scripts: {
|
|
12
|
+
build: "tsc -p tsconfig.build.json",
|
|
13
|
+
typecheck: "tsc --noEmit",
|
|
14
|
+
test: "vitest run",
|
|
15
|
+
dev: "tsc --watch",
|
|
16
|
+
},
|
|
17
|
+
keywords: ["velocms", "theme", cfg.type, ...(cfg.niche ? [cfg.niche] : [])],
|
|
18
|
+
author: {
|
|
19
|
+
name: cfg.authorName,
|
|
20
|
+
email: cfg.authorEmail,
|
|
21
|
+
},
|
|
22
|
+
license: cfg.license,
|
|
23
|
+
peerDependencies: {
|
|
24
|
+
react: "^19.0.0",
|
|
25
|
+
next: "^16.0.0",
|
|
26
|
+
},
|
|
27
|
+
devDependencies: {
|
|
28
|
+
typescript: "^5.0.0",
|
|
29
|
+
"@types/react": "^19.0.0",
|
|
30
|
+
vitest: "^4.0.0",
|
|
31
|
+
},
|
|
32
|
+
publishConfig: {
|
|
33
|
+
access: "public",
|
|
34
|
+
},
|
|
35
|
+
}, null, 2);
|
|
36
|
+
}
|
|
37
|
+
function manifestJsonContent(cfg) {
|
|
38
|
+
return JSON.stringify({
|
|
39
|
+
$schema: "https://velocms.org/schemas/theme-v1.json",
|
|
40
|
+
name: `velocms-theme-${cfg.slug}`,
|
|
41
|
+
displayName: cfg.name,
|
|
42
|
+
version: "1.0.0",
|
|
43
|
+
description: cfg.description,
|
|
44
|
+
author: {
|
|
45
|
+
name: cfg.authorName,
|
|
46
|
+
email: cfg.authorEmail,
|
|
47
|
+
},
|
|
48
|
+
type: cfg.type,
|
|
49
|
+
category: cfg.niche ?? "general",
|
|
50
|
+
tags: [cfg.type, ...(cfg.niche ? [cfg.niche] : [])],
|
|
51
|
+
engines: {
|
|
52
|
+
velocms: ">=1.0.0",
|
|
53
|
+
},
|
|
54
|
+
preview: {
|
|
55
|
+
thumbnail: "./preview/thumbnail.png",
|
|
56
|
+
screenshots: ["./preview/screenshot-1.png"],
|
|
57
|
+
},
|
|
58
|
+
exports: {
|
|
59
|
+
components: {
|
|
60
|
+
BlogLayout: "./dist/BlogIndex.js",
|
|
61
|
+
PostLayout: "./dist/PostLayout.js",
|
|
62
|
+
PageLayout: "./dist/PostLayout.js",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
pricing: {
|
|
66
|
+
model: "free",
|
|
67
|
+
},
|
|
68
|
+
}, null, 2);
|
|
69
|
+
}
|
|
70
|
+
function tsconfigContent() {
|
|
71
|
+
return JSON.stringify({
|
|
72
|
+
compilerOptions: {
|
|
73
|
+
target: "ES2020",
|
|
74
|
+
module: "ESNext",
|
|
75
|
+
moduleResolution: "bundler",
|
|
76
|
+
jsx: "react-jsx",
|
|
77
|
+
strict: true,
|
|
78
|
+
esModuleInterop: true,
|
|
79
|
+
skipLibCheck: true,
|
|
80
|
+
outDir: "./dist",
|
|
81
|
+
rootDir: "./src",
|
|
82
|
+
declaration: true,
|
|
83
|
+
declarationDir: "./dist",
|
|
84
|
+
},
|
|
85
|
+
include: ["src"],
|
|
86
|
+
exclude: ["node_modules", "dist"],
|
|
87
|
+
}, null, 2);
|
|
88
|
+
}
|
|
89
|
+
function tsconfigBuildContent() {
|
|
90
|
+
return JSON.stringify({
|
|
91
|
+
extends: "./tsconfig.json",
|
|
92
|
+
compilerOptions: {
|
|
93
|
+
declaration: true,
|
|
94
|
+
sourceMap: false,
|
|
95
|
+
removeComments: true,
|
|
96
|
+
},
|
|
97
|
+
exclude: ["**/*.test.ts", "**/*.test.tsx"],
|
|
98
|
+
}, null, 2);
|
|
99
|
+
}
|
|
100
|
+
function indexTestTsContent(cfg) {
|
|
101
|
+
return `/**
|
|
102
|
+
* Smoke test for ${cfg.name} — verifies the theme entry point loads and
|
|
103
|
+
* exports the constants VeloCMS reads at install/registration time.
|
|
104
|
+
*
|
|
105
|
+
* Replace/extend with real component tests as your theme grows.
|
|
106
|
+
*/
|
|
107
|
+
|
|
108
|
+
import { describe, it, expect } from "vitest";
|
|
109
|
+
import { THEME_SLUG, THEME_VERSION, BlogIndex, PostLayout } from "./index";
|
|
110
|
+
|
|
111
|
+
describe("${cfg.name} theme entry point", () => {
|
|
112
|
+
it("exports the expected theme metadata", () => {
|
|
113
|
+
expect(THEME_SLUG).toBe("${cfg.slug}");
|
|
114
|
+
expect(THEME_VERSION).toBe("1.0.0");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("exports BlogIndex and PostLayout components", () => {
|
|
118
|
+
expect(typeof BlogIndex).toBe("function");
|
|
119
|
+
expect(typeof PostLayout).toBe("function");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
function indexTsContent(cfg) {
|
|
125
|
+
const name = cfg.name;
|
|
126
|
+
const slug = cfg.slug;
|
|
127
|
+
return `/**
|
|
128
|
+
* ${name} — VeloCMS Theme
|
|
129
|
+
*
|
|
130
|
+
* Entry point. Re-exports all theme components.
|
|
131
|
+
* VeloCMS loads these via the manifest "exports.components" paths.
|
|
132
|
+
*/
|
|
133
|
+
|
|
134
|
+
export { BlogIndex } from "./BlogIndex";
|
|
135
|
+
export { PostLayout } from "./PostLayout";
|
|
136
|
+
|
|
137
|
+
// Theme metadata (convenience re-export for integrations)
|
|
138
|
+
export const THEME_SLUG = "${slug}";
|
|
139
|
+
export const THEME_VERSION = "1.0.0";
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
function blogIndexContent(cfg) {
|
|
143
|
+
return `/**
|
|
144
|
+
* BlogIndex — ${cfg.name}
|
|
145
|
+
*
|
|
146
|
+
* TODO: Replace with your actual blog listing layout.
|
|
147
|
+
* This minimal scaffold renders a list of post titles.
|
|
148
|
+
*
|
|
149
|
+
* Props injected by VeloCMS at render time:
|
|
150
|
+
* - posts: array of post objects
|
|
151
|
+
* - settings: tenant site settings
|
|
152
|
+
* - tenant: tenant context (id, slug, blogName, etc.)
|
|
153
|
+
*/
|
|
154
|
+
|
|
155
|
+
import React from "react";
|
|
156
|
+
|
|
157
|
+
interface Post {
|
|
158
|
+
id: string;
|
|
159
|
+
title: string;
|
|
160
|
+
slug: string;
|
|
161
|
+
excerpt?: string;
|
|
162
|
+
published_at?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
interface BlogIndexProps {
|
|
166
|
+
posts: Post[];
|
|
167
|
+
settings?: { blog_name?: string; description?: string };
|
|
168
|
+
tenant?: { blogName?: string };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function BlogIndex({ posts, settings, tenant }: BlogIndexProps) {
|
|
172
|
+
const blogName = tenant?.blogName ?? settings?.blog_name ?? "Blog";
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<main>
|
|
176
|
+
<header>
|
|
177
|
+
<h1>{blogName}</h1>
|
|
178
|
+
{settings?.description && <p>{settings.description}</p>}
|
|
179
|
+
</header>
|
|
180
|
+
|
|
181
|
+
{/* TODO: Add your custom layout, hero section, filter pills, etc. */}
|
|
182
|
+
<section>
|
|
183
|
+
{posts.map((post) => (
|
|
184
|
+
<article key={post.id}>
|
|
185
|
+
<h2>
|
|
186
|
+
<a href={\`/blog/\${post.slug}\`}>{post.title}</a>
|
|
187
|
+
</h2>
|
|
188
|
+
{post.excerpt && <p>{post.excerpt}</p>}
|
|
189
|
+
</article>
|
|
190
|
+
))}
|
|
191
|
+
</section>
|
|
192
|
+
</main>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
`;
|
|
196
|
+
}
|
|
197
|
+
function postLayoutContent(cfg) {
|
|
198
|
+
return `/**
|
|
199
|
+
* PostLayout — ${cfg.name}
|
|
200
|
+
*
|
|
201
|
+
* TODO: Replace with your actual single-post layout.
|
|
202
|
+
*
|
|
203
|
+
* Props injected by VeloCMS at render time:
|
|
204
|
+
* - post: the current post object
|
|
205
|
+
* - settings: tenant site settings
|
|
206
|
+
* - content: pre-rendered HTML string of the post body
|
|
207
|
+
*/
|
|
208
|
+
|
|
209
|
+
import React from "react";
|
|
210
|
+
|
|
211
|
+
interface Post {
|
|
212
|
+
id: string;
|
|
213
|
+
title: string;
|
|
214
|
+
slug: string;
|
|
215
|
+
author_name?: string;
|
|
216
|
+
published_at?: string;
|
|
217
|
+
featured_image?: string;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
interface PostLayoutProps {
|
|
221
|
+
post: Post;
|
|
222
|
+
content: string;
|
|
223
|
+
settings?: { blog_name?: string };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function PostLayout({ post, content, settings }: PostLayoutProps) {
|
|
227
|
+
const blogName = settings?.blog_name ?? "Blog";
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<main>
|
|
231
|
+
<nav>
|
|
232
|
+
<a href="/blog">{blogName}</a>
|
|
233
|
+
</nav>
|
|
234
|
+
|
|
235
|
+
<article>
|
|
236
|
+
<header>
|
|
237
|
+
<h1>{post.title}</h1>
|
|
238
|
+
{post.author_name && (
|
|
239
|
+
<p>
|
|
240
|
+
By {post.author_name}
|
|
241
|
+
{post.published_at && (
|
|
242
|
+
<>
|
|
243
|
+
{" · "}
|
|
244
|
+
<time dateTime={post.published_at}>
|
|
245
|
+
{new Date(post.published_at).toLocaleDateString()}
|
|
246
|
+
</time>
|
|
247
|
+
</>
|
|
248
|
+
)}
|
|
249
|
+
</p>
|
|
250
|
+
)}
|
|
251
|
+
</header>
|
|
252
|
+
|
|
253
|
+
{/* TODO: Style the post body to match your theme */}
|
|
254
|
+
{/* eslint-disable-next-line react/no-danger */}
|
|
255
|
+
<div dangerouslySetInnerHTML={{ __html: content }} />
|
|
256
|
+
</article>
|
|
257
|
+
</main>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
262
|
+
function themeConfigContent(cfg) {
|
|
263
|
+
return `/**
|
|
264
|
+
* theme.config.ts — ${cfg.name}
|
|
265
|
+
*
|
|
266
|
+
* Tailwind CSS preset overrides applied when this theme is active.
|
|
267
|
+
* Extend the defaults object with your color palette, fonts, etc.
|
|
268
|
+
*
|
|
269
|
+
* See: https://tailwindcss.com/docs/configuration#presets
|
|
270
|
+
*/
|
|
271
|
+
|
|
272
|
+
const themeConfig = {
|
|
273
|
+
theme: {
|
|
274
|
+
extend: {
|
|
275
|
+
colors: {
|
|
276
|
+
// TODO: Add your palette
|
|
277
|
+
// primary: "oklch(60% 0.20 240)",
|
|
278
|
+
},
|
|
279
|
+
fontFamily: {
|
|
280
|
+
// TODO: Add your font stack
|
|
281
|
+
// sans: ["Inter", "ui-sans-serif", "system-ui"],
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
export default themeConfig;
|
|
288
|
+
`;
|
|
289
|
+
}
|
|
290
|
+
function stylesCssContent(cfg) {
|
|
291
|
+
return `/**
|
|
292
|
+
* styles.css — ${cfg.name}
|
|
293
|
+
*
|
|
294
|
+
* Global CSS for this theme. Loaded alongside Tailwind utilities.
|
|
295
|
+
* Keep it minimal — prefer Tailwind classes in components.
|
|
296
|
+
*
|
|
297
|
+
* CSS custom properties prefixed with --theme- to avoid collisions.
|
|
298
|
+
*/
|
|
299
|
+
|
|
300
|
+
:root {
|
|
301
|
+
/* TODO: Replace with your theme tokens */
|
|
302
|
+
--theme-font-heading: system-ui, sans-serif;
|
|
303
|
+
--theme-font-body: system-ui, sans-serif;
|
|
304
|
+
--theme-color-bg: #ffffff;
|
|
305
|
+
--theme-color-text: #1a1a1a;
|
|
306
|
+
--theme-color-accent: oklch(60% 0.20 240);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* TODO: Add your global base styles */
|
|
310
|
+
`;
|
|
311
|
+
}
|
|
312
|
+
function readmeContent(cfg) {
|
|
313
|
+
return `# ${cfg.name}
|
|
314
|
+
|
|
315
|
+
A VeloCMS ${cfg.type} theme${cfg.niche ? ` for ${cfg.niche}s` : ""}.
|
|
316
|
+
|
|
317
|
+
## Getting started
|
|
318
|
+
|
|
319
|
+
\`\`\`bash
|
|
320
|
+
npm install
|
|
321
|
+
npm run dev
|
|
322
|
+
\`\`\`
|
|
323
|
+
|
|
324
|
+
## Build
|
|
325
|
+
|
|
326
|
+
\`\`\`bash
|
|
327
|
+
npm run build
|
|
328
|
+
\`\`\`
|
|
329
|
+
|
|
330
|
+
Produces \`dist/\` ready for publishing to the VeloCMS marketplace.
|
|
331
|
+
|
|
332
|
+
## Publish to marketplace
|
|
333
|
+
|
|
334
|
+
1. Build: \`npm run build\`
|
|
335
|
+
2. Validate manifest: ensure \`theme.manifest.json\` is complete
|
|
336
|
+
3. Submit: \`npx velocms-cli theme submit\` (coming soon)
|
|
337
|
+
|
|
338
|
+
## License
|
|
339
|
+
|
|
340
|
+
${cfg.license}
|
|
341
|
+
`;
|
|
342
|
+
}
|
|
343
|
+
function gitignoreContent() {
|
|
344
|
+
return `node_modules/
|
|
345
|
+
dist/
|
|
346
|
+
*.tsbuildinfo
|
|
347
|
+
.env
|
|
348
|
+
.env.local
|
|
349
|
+
`;
|
|
350
|
+
}
|
|
351
|
+
export function scaffoldTheme(outputDir, cfg) {
|
|
352
|
+
if (existsSync(outputDir)) {
|
|
353
|
+
throw new Error(`Directory already exists: ${outputDir}`);
|
|
354
|
+
}
|
|
355
|
+
mkdirSync(outputDir, { recursive: true });
|
|
356
|
+
mkdirSync(join(outputDir, "src"), { recursive: true });
|
|
357
|
+
mkdirSync(join(outputDir, "preview"), { recursive: true });
|
|
358
|
+
writeFileSync(join(outputDir, "package.json"), packageJsonContent(cfg));
|
|
359
|
+
writeFileSync(join(outputDir, "theme.manifest.json"), manifestJsonContent(cfg));
|
|
360
|
+
writeFileSync(join(outputDir, "tsconfig.json"), tsconfigContent());
|
|
361
|
+
writeFileSync(join(outputDir, "tsconfig.build.json"), tsconfigBuildContent());
|
|
362
|
+
writeFileSync(join(outputDir, "README.md"), readmeContent(cfg));
|
|
363
|
+
writeFileSync(join(outputDir, ".gitignore"), gitignoreContent());
|
|
364
|
+
writeFileSync(join(outputDir, "src", "index.ts"), indexTsContent(cfg));
|
|
365
|
+
writeFileSync(join(outputDir, "src", "index.test.ts"), indexTestTsContent(cfg));
|
|
366
|
+
writeFileSync(join(outputDir, "src", "BlogIndex.tsx"), blogIndexContent(cfg));
|
|
367
|
+
writeFileSync(join(outputDir, "src", "PostLayout.tsx"), postLayoutContent(cfg));
|
|
368
|
+
writeFileSync(join(outputDir, "src", "theme.config.ts"), themeConfigContent(cfg));
|
|
369
|
+
writeFileSync(join(outputDir, "src", "styles.css"), stylesCssContent(cfg));
|
|
370
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-velocms-theme",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Scaffolding tool for VeloCMS theme development",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-velocms-theme": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.build.json",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"dev": "node dist/index.js",
|
|
19
|
+
"prepublishOnly": "npm run build && npm run typecheck"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"velocms",
|
|
23
|
+
"cms",
|
|
24
|
+
"theme",
|
|
25
|
+
"cli",
|
|
26
|
+
"scaffold",
|
|
27
|
+
"scaffolding",
|
|
28
|
+
"generator",
|
|
29
|
+
"create-velocms-theme"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"author": {
|
|
33
|
+
"name": "VeloCMS",
|
|
34
|
+
"url": "https://velocms.org"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/VeloCMS/create-velocms-theme.git"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://velocms.org/developers",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/VeloCMS/create-velocms-theme/issues"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"typescript": "^5.0.0"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
}
|
|
50
|
+
}
|