@aravindc26/velu 0.9.1 → 0.11.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/package.json +1 -1
- package/schema/velu.schema.json +717 -19
- package/src/build.ts +210 -46
- package/src/cli.ts +66 -3
- package/src/engine/_server.mjs +129 -20
- package/src/engine/app/(docs)/[[...slug]]/layout.tsx +87 -0
- package/src/engine/app/(docs)/[[...slug]]/page.tsx +86 -6
- package/src/engine/app/(docs)/layout.tsx +1 -13
- package/src/engine/app/global.css +346 -1
- package/src/engine/app/layout.tsx +3 -7
- package/src/engine/app/search.css +20 -0
- package/src/engine/components/lang-switcher.tsx +95 -0
- package/src/engine/components/product-switcher.tsx +78 -0
- package/src/engine/components/providers.tsx +26 -0
- package/src/engine/components/search.tsx +66 -3
- package/src/engine/components/sidebar-links.tsx +51 -0
- package/src/engine/components/theme-toggle.tsx +39 -0
- package/src/engine/components/version-switcher.tsx +89 -0
- package/src/engine/lib/layout.shared.ts +28 -6
- package/src/engine/lib/navigation-normalize.mjs +456 -0
- package/src/engine/lib/navigation-normalize.ts +488 -0
- package/src/engine/lib/source.ts +14 -0
- package/src/engine/lib/velu.ts +267 -3
- package/src/engine/next.config.mjs +2 -2
- package/src/engine/src/lib/velu.ts +86 -13
- package/src/navigation-normalize.ts +488 -0
- package/src/themes.ts +66 -257
- package/src/validate.ts +116 -18
package/src/build.ts
CHANGED
|
@@ -1,21 +1,61 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync, copyFileSync, cpSync, existsSync, rmSync } from "node:fs";
|
|
2
2
|
import { join, dirname } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { generateThemeCss, type VeluColors, type VeluStyling } from "./themes.js";
|
|
4
|
+
import { generateThemeCss, resolveThemeName, type VeluColors, type VeluStyling } from "./themes.js";
|
|
5
|
+
import { normalizeConfigNavigation } from "./navigation-normalize.js";
|
|
5
6
|
|
|
6
7
|
// ── Engine directory (shipped with the CLI package) ──────────────────────────
|
|
7
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
9
|
const __dirname = dirname(__filename);
|
|
9
|
-
const
|
|
10
|
+
const PACKAGED_ENGINE_DIR = join(__dirname, "engine");
|
|
11
|
+
const DEV_ENGINE_DIR = join(__dirname, "..", "src", "engine");
|
|
12
|
+
const ENGINE_DIR = existsSync(DEV_ENGINE_DIR) ? DEV_ENGINE_DIR : PACKAGED_ENGINE_DIR;
|
|
10
13
|
|
|
11
14
|
// ── Types (used only by build.ts for page copying) ─────────────────────────────
|
|
12
15
|
|
|
16
|
+
interface VeluSeparator {
|
|
17
|
+
separator: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface VeluLink {
|
|
21
|
+
href: string;
|
|
22
|
+
label: string;
|
|
23
|
+
icon?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface VeluAnchor {
|
|
27
|
+
anchor: string;
|
|
28
|
+
href?: string;
|
|
29
|
+
icon?: string;
|
|
30
|
+
color?: {
|
|
31
|
+
light: string;
|
|
32
|
+
dark: string;
|
|
33
|
+
};
|
|
34
|
+
tabs?: VeluTab[];
|
|
35
|
+
hidden?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface VeluGlobalTab {
|
|
39
|
+
tab: string;
|
|
40
|
+
href: string;
|
|
41
|
+
icon?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
13
44
|
interface VeluGroup {
|
|
14
45
|
group: string;
|
|
15
46
|
slug: string;
|
|
16
47
|
icon?: string;
|
|
17
48
|
expanded?: boolean;
|
|
18
|
-
|
|
49
|
+
description?: string;
|
|
50
|
+
hidden?: boolean;
|
|
51
|
+
pages: (string | VeluGroup | VeluSeparator | VeluLink)[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface VeluMenuItem {
|
|
55
|
+
item: string;
|
|
56
|
+
icon?: string;
|
|
57
|
+
groups?: VeluGroup[];
|
|
58
|
+
pages?: (string | VeluSeparator | VeluLink)[];
|
|
19
59
|
}
|
|
20
60
|
|
|
21
61
|
interface VeluTab {
|
|
@@ -23,8 +63,26 @@ interface VeluTab {
|
|
|
23
63
|
slug: string;
|
|
24
64
|
icon?: string;
|
|
25
65
|
href?: string;
|
|
26
|
-
pages?: string[];
|
|
66
|
+
pages?: (string | VeluSeparator | VeluLink)[];
|
|
27
67
|
groups?: VeluGroup[];
|
|
68
|
+
menu?: VeluMenuItem[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface VeluLanguageNav {
|
|
72
|
+
language: string;
|
|
73
|
+
tabs: VeluTab[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface VeluProductNav {
|
|
77
|
+
product: string;
|
|
78
|
+
icon?: string;
|
|
79
|
+
tabs?: VeluTab[];
|
|
80
|
+
pages?: (string | VeluSeparator | VeluLink)[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface VeluVersionNav {
|
|
84
|
+
version: string;
|
|
85
|
+
tabs: VeluTab[];
|
|
28
86
|
}
|
|
29
87
|
|
|
30
88
|
interface VeluConfig {
|
|
@@ -33,16 +91,38 @@ interface VeluConfig {
|
|
|
33
91
|
colors?: VeluColors;
|
|
34
92
|
appearance?: "system" | "light" | "dark";
|
|
35
93
|
styling?: VeluStyling;
|
|
94
|
+
languages?: string[];
|
|
36
95
|
navigation: {
|
|
37
|
-
tabs
|
|
96
|
+
tabs?: VeluTab[];
|
|
97
|
+
languages?: VeluLanguageNav[];
|
|
98
|
+
products?: VeluProductNav[];
|
|
99
|
+
versions?: VeluVersionNav[];
|
|
100
|
+
anchors?: VeluAnchor[];
|
|
101
|
+
global?: {
|
|
102
|
+
anchors?: VeluAnchor[];
|
|
103
|
+
tabs?: VeluGlobalTab[];
|
|
104
|
+
};
|
|
38
105
|
};
|
|
39
106
|
}
|
|
40
107
|
|
|
108
|
+
function isSeparator(item: unknown): item is VeluSeparator {
|
|
109
|
+
return typeof item === "object" && item !== null && "separator" in item;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isLink(item: unknown): item is VeluLink {
|
|
113
|
+
return typeof item === "object" && item !== null && "href" in item && "label" in item;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isGroup(item: unknown): item is VeluGroup {
|
|
117
|
+
return typeof item === "object" && item !== null && "group" in item;
|
|
118
|
+
}
|
|
119
|
+
|
|
41
120
|
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
42
121
|
|
|
43
122
|
function loadConfig(docsDir: string): VeluConfig {
|
|
44
123
|
const raw = readFileSync(join(docsDir, "velu.json"), "utf-8");
|
|
45
|
-
|
|
124
|
+
const parsed = JSON.parse(raw) as VeluConfig;
|
|
125
|
+
return normalizeConfigNavigation(parsed);
|
|
46
126
|
}
|
|
47
127
|
|
|
48
128
|
function pageLabelFromSlug(slug: string): string {
|
|
@@ -73,7 +153,7 @@ interface BuildArtifacts {
|
|
|
73
153
|
function buildArtifacts(config: VeluConfig): BuildArtifacts {
|
|
74
154
|
const pageMap: PageMapping[] = [];
|
|
75
155
|
const metaFiles: MetaFile[] = [];
|
|
76
|
-
const rootTabs = config.navigation.tabs.filter((tab) => !tab.href);
|
|
156
|
+
const rootTabs = (config.navigation.tabs || []).filter((tab) => !tab.href);
|
|
77
157
|
const rootPages = rootTabs.map((tab) => tab.slug);
|
|
78
158
|
let firstPage = "quickstart";
|
|
79
159
|
let hasFirstPage = false;
|
|
@@ -85,6 +165,17 @@ function buildArtifacts(config: VeluConfig): BuildArtifacts {
|
|
|
85
165
|
}
|
|
86
166
|
}
|
|
87
167
|
|
|
168
|
+
function metaEntry(item: string | VeluSeparator | VeluLink): string {
|
|
169
|
+
if (typeof item === "string") return item;
|
|
170
|
+
if (isSeparator(item)) return `---${item.separator}---`;
|
|
171
|
+
if (isLink(item)) {
|
|
172
|
+
return item.icon
|
|
173
|
+
? `[${item.icon}][${item.label}](${item.href})`
|
|
174
|
+
: `[${item.label}](${item.href})`;
|
|
175
|
+
}
|
|
176
|
+
return String(item);
|
|
177
|
+
}
|
|
178
|
+
|
|
88
179
|
function addGroup(group: VeluGroup, parentDir: string) {
|
|
89
180
|
const groupDir = `${parentDir}/${group.slug}`;
|
|
90
181
|
const pages: string[] = [];
|
|
@@ -96,9 +187,17 @@ function buildArtifacts(config: VeluConfig): BuildArtifacts {
|
|
|
96
187
|
pageMap.push({ src: item, dest });
|
|
97
188
|
pages.push(basename);
|
|
98
189
|
trackFirstPage(dest);
|
|
99
|
-
} else {
|
|
190
|
+
} else if (isGroup(item)) {
|
|
100
191
|
addGroup(item, groupDir);
|
|
101
|
-
pages.push(item.slug);
|
|
192
|
+
pages.push(item.hidden ? `!${item.slug}` : item.slug);
|
|
193
|
+
} else if (isSeparator(item)) {
|
|
194
|
+
pages.push(`---${item.separator}---`);
|
|
195
|
+
} else if (isLink(item)) {
|
|
196
|
+
pages.push(
|
|
197
|
+
item.icon
|
|
198
|
+
? `[${item.icon}][${item.label}](${item.href})`
|
|
199
|
+
: `[${item.label}](${item.href})`
|
|
200
|
+
);
|
|
102
201
|
}
|
|
103
202
|
}
|
|
104
203
|
|
|
@@ -109,6 +208,7 @@ function buildArtifacts(config: VeluConfig): BuildArtifacts {
|
|
|
109
208
|
};
|
|
110
209
|
|
|
111
210
|
if (group.icon) groupMeta.icon = group.icon;
|
|
211
|
+
if (group.description) groupMeta.description = group.description;
|
|
112
212
|
|
|
113
213
|
metaFiles.push({ dir: groupDir, data: groupMeta });
|
|
114
214
|
}
|
|
@@ -119,17 +219,21 @@ function buildArtifacts(config: VeluConfig): BuildArtifacts {
|
|
|
119
219
|
if (tab.groups) {
|
|
120
220
|
for (const group of tab.groups) {
|
|
121
221
|
addGroup(group, tab.slug);
|
|
122
|
-
tabPages.push(group.slug);
|
|
222
|
+
tabPages.push(group.hidden ? `!${group.slug}` : group.slug);
|
|
123
223
|
}
|
|
124
224
|
}
|
|
125
225
|
|
|
126
226
|
if (tab.pages) {
|
|
127
|
-
for (const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
227
|
+
for (const item of tab.pages) {
|
|
228
|
+
if (typeof item === "string") {
|
|
229
|
+
const basename = pageBasename(item);
|
|
230
|
+
const dest = `${tab.slug}/${basename}`;
|
|
231
|
+
pageMap.push({ src: item, dest });
|
|
232
|
+
tabPages.push(basename);
|
|
233
|
+
trackFirstPage(dest);
|
|
234
|
+
} else {
|
|
235
|
+
tabPages.push(metaEntry(item));
|
|
236
|
+
}
|
|
133
237
|
}
|
|
134
238
|
}
|
|
135
239
|
|
|
@@ -176,40 +280,106 @@ function build(docsDir: string, outDir: string) {
|
|
|
176
280
|
console.log("📋 Copied velu.json");
|
|
177
281
|
|
|
178
282
|
// ── 4. Build content + metadata artifacts ────────────────────────────────
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
for (const meta of metaFiles) {
|
|
183
|
-
const metaPath = join(outDir, "content", "docs", meta.dir, "meta.json");
|
|
184
|
-
mkdirSync(dirname(metaPath), { recursive: true });
|
|
185
|
-
writeFileSync(metaPath, JSON.stringify(meta.data, null, 2) + "\n", "utf-8");
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// 4b) Copy all referenced .md files (slug-based destinations)
|
|
189
|
-
for (const { src, dest } of pageMap) {
|
|
190
|
-
const srcPath = join(docsDir, `${src}.md`);
|
|
191
|
-
const destPath = join(outDir, "content", "docs", `${dest}.mdx`);
|
|
192
|
-
|
|
193
|
-
if (!existsSync(srcPath)) {
|
|
194
|
-
console.warn(`⚠️ Missing: ${srcPath}`);
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
283
|
+
const contentDir = join(outDir, "content", "docs");
|
|
284
|
+
const navLanguages = config.navigation.languages;
|
|
285
|
+
const simpleLanguages = config.languages || [];
|
|
197
286
|
|
|
287
|
+
function processPage(srcPath: string, destPath: string, slug: string) {
|
|
198
288
|
mkdirSync(dirname(destPath), { recursive: true });
|
|
199
|
-
|
|
200
289
|
let content = readFileSync(srcPath, "utf-8");
|
|
201
290
|
if (!content.startsWith("---")) {
|
|
202
291
|
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
203
|
-
const title = titleMatch ? titleMatch[1] : pageLabelFromSlug(
|
|
292
|
+
const title = titleMatch ? titleMatch[1] : pageLabelFromSlug(slug);
|
|
204
293
|
if (titleMatch) {
|
|
205
294
|
content = content.replace(/^#\s+.+$/m, "").trimStart();
|
|
206
295
|
}
|
|
207
296
|
content = `---\ntitle: "${title}"\n---\n\n${content}`;
|
|
208
297
|
}
|
|
209
|
-
|
|
210
298
|
writeFileSync(destPath, content, "utf-8");
|
|
211
299
|
}
|
|
212
|
-
|
|
300
|
+
|
|
301
|
+
function writeLangContent(
|
|
302
|
+
langCode: string,
|
|
303
|
+
artifacts: BuildArtifacts,
|
|
304
|
+
isDefault: boolean,
|
|
305
|
+
useLangFolders = false
|
|
306
|
+
) {
|
|
307
|
+
const storagePrefix = useLangFolders ? langCode : (isDefault ? "" : langCode);
|
|
308
|
+
const urlPrefix = isDefault ? "" : langCode;
|
|
309
|
+
|
|
310
|
+
// Write meta files
|
|
311
|
+
const metas = storagePrefix
|
|
312
|
+
? artifacts.metaFiles.map((m) => ({ dir: m.dir ? `${storagePrefix}/${m.dir}` : storagePrefix, data: { ...m.data } }))
|
|
313
|
+
: artifacts.metaFiles;
|
|
314
|
+
for (const meta of metas) {
|
|
315
|
+
const metaPath = join(contentDir, meta.dir, "meta.json");
|
|
316
|
+
mkdirSync(dirname(metaPath), { recursive: true });
|
|
317
|
+
writeFileSync(metaPath, JSON.stringify(meta.data, null, 2) + "\n", "utf-8");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Copy pages using explicit source paths from velu.json
|
|
321
|
+
for (const { src, dest } of artifacts.pageMap) {
|
|
322
|
+
const srcPath = join(docsDir, `${src}.md`);
|
|
323
|
+
if (!existsSync(srcPath)) {
|
|
324
|
+
console.warn(`⚠️ Missing page source: ${src}.md (language: ${langCode})`);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const destPath = join(contentDir, storagePrefix ? `${storagePrefix}/${dest}.mdx` : `${dest}.mdx`);
|
|
328
|
+
processPage(srcPath, destPath, src);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Index page
|
|
332
|
+
const href = urlPrefix ? `/${urlPrefix}/${artifacts.firstPage}/` : `/${artifacts.firstPage}/`;
|
|
333
|
+
const indexPath = storagePrefix ? join(contentDir, storagePrefix, "index.mdx") : join(contentDir, "index.mdx");
|
|
334
|
+
writeFileSync(
|
|
335
|
+
indexPath,
|
|
336
|
+
`---\ntitle: "Overview"\ndescription: Documentation powered by Velu\n---\n\nimport { Card, Cards } from "fumadocs-ui/components/card"\nimport { Callout } from "fumadocs-ui/components/callout"\n\n<Callout type="info">\n Welcome to your documentation site.\n</Callout>\n\n## Start here\n\n<Cards>\n <Card\n title="Read the docs"\n href="${href}"\n description="Begin with the first page in your configured navigation."\n />\n</Cards>\n`,
|
|
337
|
+
"utf-8"
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let totalPages = 0;
|
|
342
|
+
let totalMeta = 0;
|
|
343
|
+
|
|
344
|
+
if (navLanguages && navLanguages.length > 0) {
|
|
345
|
+
// ── Mode 1: Per-language navigation (Mintlify-style) ──────────────
|
|
346
|
+
const rootPages: string[] = [];
|
|
347
|
+
|
|
348
|
+
for (let i = 0; i < navLanguages.length; i++) {
|
|
349
|
+
const langEntry = navLanguages[i];
|
|
350
|
+
const isDefault = i === 0;
|
|
351
|
+
const langConfig = { ...config, navigation: { ...config.navigation, tabs: langEntry.tabs } } as VeluConfig;
|
|
352
|
+
const artifacts = buildArtifacts(langConfig);
|
|
353
|
+
writeLangContent(langEntry.language, artifacts, isDefault, true);
|
|
354
|
+
totalPages += artifacts.pageMap.length;
|
|
355
|
+
totalMeta += artifacts.metaFiles.length;
|
|
356
|
+
rootPages.push(`!${langEntry.language}`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const rootMetaPath = join(contentDir, "meta.json");
|
|
360
|
+
writeFileSync(rootMetaPath, JSON.stringify({ pages: rootPages }, null, 2) + "\n", "utf-8");
|
|
361
|
+
} else {
|
|
362
|
+
// ── Mode 2: Simple (single-lang or same-nav multi-lang) ───────────
|
|
363
|
+
const artifacts = buildArtifacts(config);
|
|
364
|
+
const useLangFolders = simpleLanguages.length > 1;
|
|
365
|
+
writeLangContent(simpleLanguages[0] || "en", artifacts, true, useLangFolders);
|
|
366
|
+
totalPages += artifacts.pageMap.length;
|
|
367
|
+
totalMeta += artifacts.metaFiles.length;
|
|
368
|
+
|
|
369
|
+
if (simpleLanguages.length > 1) {
|
|
370
|
+
const rootMetaPath = join(contentDir, "meta.json");
|
|
371
|
+
const rootPages = [`!${simpleLanguages[0] || "en"}`];
|
|
372
|
+
for (const lang of simpleLanguages.slice(1)) {
|
|
373
|
+
writeLangContent(lang, artifacts, false, true);
|
|
374
|
+
rootPages.push(`!${lang}`);
|
|
375
|
+
totalPages += artifacts.pageMap.length;
|
|
376
|
+
totalMeta += artifacts.metaFiles.length;
|
|
377
|
+
}
|
|
378
|
+
writeFileSync(rootMetaPath, JSON.stringify({ pages: rootPages }, null, 2) + "\n", "utf-8");
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
console.log(`📄 Generated ${totalPages} pages + ${totalMeta} navigation meta files`);
|
|
213
383
|
|
|
214
384
|
// ── 5. Generate theme CSS (dynamic — depends on user config) ─────────────
|
|
215
385
|
const themeCss = generateThemeCss({
|
|
@@ -219,14 +389,8 @@ function build(docsDir: string, outDir: string) {
|
|
|
219
389
|
styling: config.styling,
|
|
220
390
|
});
|
|
221
391
|
writeFileSync(join(outDir, "app", "velu-theme.css"), themeCss, "utf-8");
|
|
222
|
-
console.log(`🎨 Generated theme: ${config.theme
|
|
223
|
-
|
|
224
|
-
// ── 6. Generate index.mdx (dynamic — references first page) ──────────────
|
|
225
|
-
writeFileSync(
|
|
226
|
-
join(outDir, "content", "docs", "index.mdx"),
|
|
227
|
-
`---\ntitle: "Overview"\ndescription: Documentation powered by Velu + Fumadocs\n---\n\nimport { Card, Cards } from "fumadocs-ui/components/card"\nimport { Callout } from "fumadocs-ui/components/callout"\n\n<Callout type="info">\n This site is powered by Velu + Fumadocs.\n</Callout>\n\n## Start here\n\n<Cards>\n <Card\n title="Read the docs"\n href="/${firstPage}/"\n description="Begin with the first page in your configured navigation."\n />\n</Cards>\n`,
|
|
228
|
-
"utf-8"
|
|
229
|
-
);
|
|
392
|
+
console.log(`🎨 Generated theme: ${resolveThemeName(config.theme)}`);
|
|
393
|
+
|
|
230
394
|
|
|
231
395
|
// ── 7. Generate minimal package.json (type: module, no local deps) ───────
|
|
232
396
|
const sitePkg = {
|
package/src/cli.ts
CHANGED
|
@@ -29,12 +29,13 @@ function printHelp() {
|
|
|
29
29
|
velu lint Validate velu.json and check referenced pages
|
|
30
30
|
velu run [--port N] Build site and start dev server (default: 4321)
|
|
31
31
|
velu build Build a deployable static site (SSG)
|
|
32
|
+
velu paths Output all navigation paths and their source files as JSON
|
|
32
33
|
|
|
33
34
|
Options:
|
|
34
35
|
--port <number> Port for the dev server (default: 4321)
|
|
35
36
|
--help Show this help message
|
|
36
37
|
|
|
37
|
-
Run lint/run/build from a directory containing velu.json.
|
|
38
|
+
Run lint/run/build/paths from a directory containing velu.json.
|
|
38
39
|
`);
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -49,17 +50,23 @@ function init(targetDir: string) {
|
|
|
49
50
|
// velu.json
|
|
50
51
|
const config = {
|
|
51
52
|
$schema: "https://raw.githubusercontent.com/aravindc26/velu/main/schema/velu.schema.json",
|
|
52
|
-
theme: "
|
|
53
|
+
theme: "neutral" as const,
|
|
53
54
|
navigation: {
|
|
54
55
|
tabs: [
|
|
55
56
|
{
|
|
56
57
|
tab: "Getting Started",
|
|
57
58
|
slug: "getting-started",
|
|
58
|
-
pages: [
|
|
59
|
+
pages: [
|
|
60
|
+
"quickstart",
|
|
61
|
+
"installation",
|
|
62
|
+
{ separator: "Resources" },
|
|
63
|
+
{ label: "Velu Website", href: "https://getvelu.com" },
|
|
64
|
+
],
|
|
59
65
|
groups: [
|
|
60
66
|
{
|
|
61
67
|
group: "Guides",
|
|
62
68
|
slug: "guides",
|
|
69
|
+
description: "Step-by-step guides to configure and deploy your docs.",
|
|
63
70
|
pages: ["guides/configuration", "guides/deployment"],
|
|
64
71
|
},
|
|
65
72
|
],
|
|
@@ -70,6 +77,13 @@ function init(targetDir: string) {
|
|
|
70
77
|
pages: ["api-reference/overview", "api-reference/authentication"],
|
|
71
78
|
},
|
|
72
79
|
],
|
|
80
|
+
anchors: [
|
|
81
|
+
{
|
|
82
|
+
anchor: "GitHub",
|
|
83
|
+
href: "https://github.com/aravindc26/velu",
|
|
84
|
+
icon: "Github",
|
|
85
|
+
},
|
|
86
|
+
],
|
|
73
87
|
},
|
|
74
88
|
};
|
|
75
89
|
writeFileSync(join(targetDir, "velu.json"), JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
@@ -122,6 +136,51 @@ async function lint(docsDir: string) {
|
|
|
122
136
|
}
|
|
123
137
|
}
|
|
124
138
|
|
|
139
|
+
// ── paths ───────────────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
interface PathEntry {
|
|
142
|
+
path: string;
|
|
143
|
+
file: string | null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function paths(docsDir: string) {
|
|
147
|
+
const { collectPages } = await import("./validate.js");
|
|
148
|
+
const { normalizeConfigNavigation } = await import("./navigation-normalize.js");
|
|
149
|
+
const { readFileSync, existsSync } = await import("node:fs");
|
|
150
|
+
const { join } = await import("node:path");
|
|
151
|
+
|
|
152
|
+
const configPath = join(docsDir, "velu.json");
|
|
153
|
+
if (!existsSync(configPath)) {
|
|
154
|
+
console.error("❌ velu.json not found.");
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
159
|
+
const config = normalizeConfigNavigation(raw);
|
|
160
|
+
const pages = collectPages(config);
|
|
161
|
+
|
|
162
|
+
const entries: PathEntry[] = pages.map((pagePath) => {
|
|
163
|
+
// Check for .mdx first, then .md
|
|
164
|
+
const mdxPath = join(docsDir, `${pagePath}.mdx`);
|
|
165
|
+
const mdPath = join(docsDir, `${pagePath}.md`);
|
|
166
|
+
|
|
167
|
+
if (existsSync(mdxPath)) {
|
|
168
|
+
return { path: pagePath, file: `${pagePath}.mdx` };
|
|
169
|
+
}
|
|
170
|
+
if (existsSync(mdPath)) {
|
|
171
|
+
return { path: pagePath, file: `${pagePath}.md` };
|
|
172
|
+
}
|
|
173
|
+
return { path: pagePath, file: null };
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const output = {
|
|
177
|
+
paths: entries,
|
|
178
|
+
count: entries.length,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
console.log(JSON.stringify(output, null, 2));
|
|
182
|
+
}
|
|
183
|
+
|
|
125
184
|
// ── build ────────────────────────────────────────────────────────────────────────
|
|
126
185
|
|
|
127
186
|
async function generateProject(docsDir: string): Promise<string> {
|
|
@@ -201,6 +260,10 @@ switch (command) {
|
|
|
201
260
|
await lint(docsDir);
|
|
202
261
|
break;
|
|
203
262
|
|
|
263
|
+
case "paths":
|
|
264
|
+
await paths(docsDir);
|
|
265
|
+
break;
|
|
266
|
+
|
|
204
267
|
case "build":
|
|
205
268
|
await buildSite(docsDir);
|
|
206
269
|
break;
|