@aravindc26/velu 0.10.0 → 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/src/build.ts CHANGED
@@ -2,20 +2,60 @@ import { readFileSync, writeFileSync, mkdirSync, copyFileSync, cpSync, existsSyn
2
2
  import { join, dirname } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
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 ENGINE_DIR = join(__dirname, "engine");
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
- pages: (string | VeluGroup)[];
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: VeluTab[];
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
- return JSON.parse(raw);
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 page of tab.pages) {
128
- const basename = pageBasename(page);
129
- const dest = `${tab.slug}/${basename}`;
130
- pageMap.push({ src: page, dest });
131
- tabPages.push(basename);
132
- trackFirstPage(dest);
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 { pageMap, metaFiles, firstPage } = buildArtifacts(config);
180
-
181
- // 4a) Write folder meta.json files (tabs/groups ordering & labels)
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(src);
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
- console.log(`📄 Generated ${pageMap.length} pages + ${metaFiles.length} navigation meta files`);
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({
@@ -221,12 +391,6 @@ function build(docsDir: string, outDir: string) {
221
391
  writeFileSync(join(outDir, "app", "velu-theme.css"), themeCss, "utf-8");
222
392
  console.log(`🎨 Generated theme: ${resolveThemeName(config.theme)}`);
223
393
 
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\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="/${firstPage}/"\n description="Begin with the first page in your configured navigation."\n />\n</Cards>\n`,
228
- "utf-8"
229
- );
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
 
@@ -55,11 +56,17 @@ function init(targetDir: string) {
55
56
  {
56
57
  tab: "Getting Started",
57
58
  slug: "getting-started",
58
- pages: ["quickstart", "installation"],
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;
@@ -3,6 +3,7 @@ import { createRequire } from 'node:module';
3
3
  import { watch } from 'node:fs';
4
4
  import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
5
5
  import { dirname, extname, join, resolve } from 'node:path';
6
+ import { normalizeConfigNavigation } from './lib/navigation-normalize.mjs';
6
7
 
7
8
  const require = createRequire(import.meta.url);
8
9
  const nextBinPath = require.resolve('next/dist/bin/next');
@@ -13,7 +14,7 @@ const contentDir = resolve('content', 'docs');
13
14
 
14
15
  function loadConfig() {
15
16
  const raw = readFileSync(join(docsDir, 'velu.json'), 'utf-8');
16
- return JSON.parse(raw);
17
+ return normalizeConfigNavigation(JSON.parse(raw));
17
18
  }
18
19
 
19
20
  function pageBasename(page) {
@@ -25,6 +26,29 @@ function pageLabelFromSlug(slug) {
25
26
  return last.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
26
27
  }
27
28
 
29
+ function isSeparator(item) {
30
+ return typeof item === 'object' && item !== null && 'separator' in item;
31
+ }
32
+
33
+ function isLink(item) {
34
+ return typeof item === 'object' && item !== null && 'href' in item && 'label' in item;
35
+ }
36
+
37
+ function isGroup(item) {
38
+ return typeof item === 'object' && item !== null && 'group' in item;
39
+ }
40
+
41
+ function metaEntry(item) {
42
+ if (typeof item === 'string') return item;
43
+ if (isSeparator(item)) return `---${item.separator}---`;
44
+ if (isLink(item)) {
45
+ return item.icon
46
+ ? `[${item.icon}][${item.label}](${item.href})`
47
+ : `[${item.label}](${item.href})`;
48
+ }
49
+ return String(item);
50
+ }
51
+
28
52
  function buildArtifacts(config) {
29
53
  const pageMap = [];
30
54
  const metaFiles = [];
@@ -51,9 +75,17 @@ function buildArtifacts(config) {
51
75
  pageMap.push({ src: item, dest });
52
76
  pages.push(basename);
53
77
  trackFirstPage(dest);
54
- } else {
78
+ } else if (isGroup(item)) {
55
79
  addGroup(item, groupDir);
56
- pages.push(item.slug);
80
+ pages.push(item.hidden ? `!${item.slug}` : item.slug);
81
+ } else if (isSeparator(item)) {
82
+ pages.push(`---${item.separator}---`);
83
+ } else if (isLink(item)) {
84
+ pages.push(
85
+ item.icon
86
+ ? `[${item.icon}][${item.label}](${item.href})`
87
+ : `[${item.label}](${item.href})`
88
+ );
57
89
  }
58
90
  }
59
91
 
@@ -64,6 +96,7 @@ function buildArtifacts(config) {
64
96
  };
65
97
 
66
98
  if (group.icon) groupMeta.icon = group.icon;
99
+ if (group.description) groupMeta.description = group.description;
67
100
 
68
101
  metaFiles.push({ dir: groupDir, data: groupMeta });
69
102
  }
@@ -73,15 +106,19 @@ function buildArtifacts(config) {
73
106
 
74
107
  for (const group of tab.groups || []) {
75
108
  addGroup(group, tab.slug);
76
- tabPages.push(group.slug);
109
+ tabPages.push(group.hidden ? `!${group.slug}` : group.slug);
77
110
  }
78
111
 
79
- for (const page of tab.pages || []) {
80
- const basename = pageBasename(page);
81
- const dest = `${tab.slug}/${basename}`;
82
- pageMap.push({ src: page, dest });
83
- tabPages.push(basename);
84
- trackFirstPage(dest);
112
+ for (const item of tab.pages || []) {
113
+ if (typeof item === 'string') {
114
+ const basename = pageBasename(item);
115
+ const dest = `${tab.slug}/${basename}`;
116
+ pageMap.push({ src: item, dest });
117
+ tabPages.push(basename);
118
+ trackFirstPage(dest);
119
+ } else {
120
+ tabPages.push(metaEntry(item));
121
+ }
85
122
  }
86
123
 
87
124
  const tabMeta = {
@@ -133,23 +170,95 @@ function writeIndexPage(firstPage) {
133
170
  );
134
171
  }
135
172
 
173
+ function writeLangContent(langCode, artifacts, isDefault, useLangFolders = false) {
174
+ const storagePrefix = useLangFolders ? langCode : (isDefault ? '' : langCode);
175
+ const urlPrefix = isDefault ? '' : langCode;
176
+
177
+ // Write meta files (prefixed for non-default)
178
+ const metaFiles = storagePrefix
179
+ ? artifacts.metaFiles.map((meta) => ({
180
+ dir: meta.dir ? `${storagePrefix}/${meta.dir}` : storagePrefix,
181
+ data: { ...meta.data },
182
+ }))
183
+ : artifacts.metaFiles;
184
+ writeMetaFiles(metaFiles);
185
+
186
+ // Copy pages using explicit source paths from velu.json
187
+ for (const { src, dest } of artifacts.pageMap) {
188
+ const srcPath = join(docsDir, `${src}.md`);
189
+ if (!existsSync(srcPath)) {
190
+ console.warn(` \x1b[33m⚠\x1b[0m Missing page source: ${src}.md (language: ${langCode})`);
191
+ continue;
192
+ }
193
+ const destPath = join(contentDir, storagePrefix ? `${storagePrefix}/${dest}.mdx` : `${dest}.mdx`);
194
+ processPage(srcPath, destPath, src);
195
+ }
196
+
197
+ // Index page
198
+ const href = urlPrefix ? `/${urlPrefix}/${artifacts.firstPage}/` : `/${artifacts.firstPage}/`;
199
+ const indexPath = storagePrefix ? join(contentDir, storagePrefix, 'index.mdx') : join(contentDir, 'index.mdx');
200
+ writeFileSync(
201
+ indexPath,
202
+ `---\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`,
203
+ 'utf-8'
204
+ );
205
+ }
206
+
136
207
  function rebuildFromConfig() {
137
208
  const config = loadConfig();
138
- const artifacts = buildArtifacts(config);
209
+ const navLanguages = config.navigation?.languages;
210
+ const simpleLanguages = config.languages || [];
139
211
 
140
212
  rmSync(contentDir, { recursive: true, force: true });
141
213
  mkdirSync(contentDir, { recursive: true });
142
214
 
143
- writeMetaFiles(artifacts.metaFiles);
215
+ // ── Mode 1: Per-language navigation (Mintlify-style) ──────────────
216
+ if (navLanguages && navLanguages.length > 0) {
217
+ const rootPages = [];
144
218
 
145
- for (const { src, dest } of artifacts.pageMap) {
146
- const srcPath = join(docsDir, `${src}.md`);
147
- if (!existsSync(srcPath)) continue;
148
- const destPath = join(contentDir, `${dest}.mdx`);
149
- processPage(srcPath, destPath, src);
219
+ for (let i = 0; i < navLanguages.length; i++) {
220
+ const langEntry = navLanguages[i];
221
+ const langCode = langEntry.language;
222
+ const isDefault = i === 0;
223
+
224
+ // Build artifacts using this language's own tabs
225
+ const langConfig = { ...config, navigation: { ...config.navigation, tabs: langEntry.tabs } };
226
+ const artifacts = buildArtifacts(langConfig);
227
+
228
+ writeLangContent(langCode, artifacts, isDefault, true);
229
+ rootPages.push(`!${langCode}`);
230
+ }
231
+
232
+ // Write root meta with default tabs + hidden language folders
233
+ writeFileSync(
234
+ join(contentDir, 'meta.json'),
235
+ JSON.stringify({ pages: rootPages }, null, 2) + '\n',
236
+ 'utf-8'
237
+ );
238
+
239
+ // Return the default language's page map for file watching
240
+ const defaultConfig = { ...config, navigation: { ...config.navigation, tabs: navLanguages[0].tabs } };
241
+ return buildArtifacts(defaultConfig).pageMap;
242
+ }
243
+
244
+ // ── Mode 2: Simple multi-lang (same nav, content in docs/<lang>/) ─
245
+ const artifacts = buildArtifacts(config);
246
+
247
+ const useLangFolders = simpleLanguages.length > 1;
248
+ writeLangContent(simpleLanguages[0] || 'en', artifacts, true, useLangFolders);
249
+
250
+ if (simpleLanguages.length > 1) {
251
+ const rootMetaPath = join(contentDir, 'meta.json');
252
+ const rootPages = [`!${simpleLanguages[0] || 'en'}`];
253
+
254
+ for (const lang of simpleLanguages.slice(1)) {
255
+ writeLangContent(lang, artifacts, false, true);
256
+ rootPages.push(`!${lang}`);
257
+ }
258
+
259
+ writeFileSync(rootMetaPath, JSON.stringify({ pages: rootPages }, null, 2) + '\n', 'utf-8');
150
260
  }
151
261
 
152
- writeIndexPage(artifacts.firstPage);
153
262
  return artifacts.pageMap;
154
263
  }
155
264