@aravindc26/velu 0.11.6 → 0.11.10
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 +383 -122
- package/src/build.ts +679 -551
- package/src/cli.ts +91 -21
- package/src/engine/app/(docs)/[...slug]/layout.tsx +155 -13
- package/src/engine/app/(docs)/[...slug]/page.tsx +77 -9
- package/src/engine/app/copy-page.css +17 -1
- package/src/engine/app/global.css +111 -5
- package/src/engine/app/layout.tsx +8 -1
- package/src/engine/app/search.css +4 -0
- package/src/engine/components/banner.tsx +80 -0
- package/src/engine/components/copy-page.tsx +162 -35
- package/src/engine/components/dropdown-switcher.tsx +142 -0
- package/src/engine/components/header-tab-link.tsx +43 -0
- package/src/engine/components/search.tsx +136 -49
- package/src/engine/lib/layout.shared.ts +68 -68
- package/src/engine/lib/velu.ts +297 -0
- package/src/validate.ts +32 -1
package/src/engine/lib/velu.ts
CHANGED
|
@@ -107,12 +107,37 @@ interface VeluSeoConfig {
|
|
|
107
107
|
indexing?: 'navigable' | 'all' | string;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
interface VeluMetadataConfig {
|
|
111
|
+
timestamp?: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
110
114
|
interface VeluThemeAsset {
|
|
111
115
|
light?: string;
|
|
112
116
|
dark?: string;
|
|
113
117
|
href?: string;
|
|
114
118
|
}
|
|
115
119
|
|
|
120
|
+
interface VeluFooterConfig {
|
|
121
|
+
socials?: Record<string, unknown> | VeluFooterSocialInput[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface VeluFooterSocialInput {
|
|
125
|
+
type?: string;
|
|
126
|
+
label?: string;
|
|
127
|
+
href?: string;
|
|
128
|
+
url?: string;
|
|
129
|
+
icon?: string;
|
|
130
|
+
iconType?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface VeluFooterSocialLink {
|
|
134
|
+
key: string;
|
|
135
|
+
label: string;
|
|
136
|
+
href: string;
|
|
137
|
+
icon: string;
|
|
138
|
+
iconType?: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
116
141
|
export interface VeluProductOption {
|
|
117
142
|
product: string;
|
|
118
143
|
slug: string;
|
|
@@ -123,6 +148,16 @@ export interface VeluProductOption {
|
|
|
123
148
|
defaultPath: string;
|
|
124
149
|
}
|
|
125
150
|
|
|
151
|
+
export interface VeluDropdownOption {
|
|
152
|
+
dropdown: string;
|
|
153
|
+
slug: string;
|
|
154
|
+
description?: string;
|
|
155
|
+
icon?: string;
|
|
156
|
+
iconType?: string;
|
|
157
|
+
tabSlugs: string[];
|
|
158
|
+
defaultPath: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
126
161
|
export interface VeluVersionOption {
|
|
127
162
|
version: string;
|
|
128
163
|
slug: string;
|
|
@@ -131,8 +166,16 @@ export interface VeluVersionOption {
|
|
|
131
166
|
defaultPath: string;
|
|
132
167
|
}
|
|
133
168
|
|
|
169
|
+
interface VeluContextualCustomOption {
|
|
170
|
+
title: string;
|
|
171
|
+
description?: string;
|
|
172
|
+
icon?: string;
|
|
173
|
+
href: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
134
176
|
interface VeluConfig {
|
|
135
177
|
name?: string;
|
|
178
|
+
description?: string;
|
|
136
179
|
title?: string;
|
|
137
180
|
favicon?: string | VeluThemeAsset;
|
|
138
181
|
logo?: string | VeluThemeAsset;
|
|
@@ -150,6 +193,13 @@ interface VeluConfig {
|
|
|
150
193
|
asyncapi?: string | string[] | Record<string, unknown>;
|
|
151
194
|
api?: VeluApiConfig;
|
|
152
195
|
seo?: VeluSeoConfig;
|
|
196
|
+
metadata?: VeluMetadataConfig;
|
|
197
|
+
footer?: VeluFooterConfig;
|
|
198
|
+
footerSocials?: Record<string, unknown> | VeluFooterSocialInput[];
|
|
199
|
+
banner?: { content?: string; dismissible?: boolean };
|
|
200
|
+
contextual?: {
|
|
201
|
+
options?: Array<string | VeluContextualCustomOption>;
|
|
202
|
+
};
|
|
153
203
|
navigation: {
|
|
154
204
|
tabs?: VeluTab[];
|
|
155
205
|
languages?: VeluLanguageNav[];
|
|
@@ -243,6 +293,41 @@ function findFirstPageInTab(tab: VeluTab): string | undefined {
|
|
|
243
293
|
return undefined;
|
|
244
294
|
}
|
|
245
295
|
|
|
296
|
+
function findFirstRouteInGroup(group: VeluGroup, tabSlug: string, parentGroupSlugs: string[] = []): string | undefined {
|
|
297
|
+
const nextGroupSlugs = group.slug ? [...parentGroupSlugs, group.slug] : parentGroupSlugs;
|
|
298
|
+
|
|
299
|
+
for (const item of group.pages) {
|
|
300
|
+
if (typeof item === 'string') {
|
|
301
|
+
return `/${[tabSlug, ...nextGroupSlugs, pageBasename(item)].filter(Boolean).join('/')}`;
|
|
302
|
+
}
|
|
303
|
+
if (isGroup(item)) {
|
|
304
|
+
const nested = findFirstRouteInGroup(item, tabSlug, nextGroupSlugs);
|
|
305
|
+
if (nested) return nested;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function resolveFirstRouteInTab(tab: VeluTab): string | undefined {
|
|
312
|
+
if (!tab.slug) return undefined;
|
|
313
|
+
|
|
314
|
+
if (tab.pages) {
|
|
315
|
+
for (const item of tab.pages) {
|
|
316
|
+
if (typeof item === 'string') return `/${tab.slug}/${pageBasename(item)}`;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (tab.groups) {
|
|
321
|
+
for (const group of tab.groups) {
|
|
322
|
+
const nested = findFirstRouteInGroup(group, tab.slug);
|
|
323
|
+
if (nested) return nested;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (typeof tab.href === 'string' && tab.href.trim().length > 0) return tab.href.trim();
|
|
328
|
+
return `/${tab.slug}`;
|
|
329
|
+
}
|
|
330
|
+
|
|
246
331
|
function parseVersionParts(version: string): number[] {
|
|
247
332
|
const parts = version.match(/\d+/g);
|
|
248
333
|
return parts ? parts.map((n) => Number(n)) : [];
|
|
@@ -258,6 +343,109 @@ function compareVersionParts(a: number[], b: number[]): number {
|
|
|
258
343
|
return 0;
|
|
259
344
|
}
|
|
260
345
|
|
|
346
|
+
const FOOTER_SOCIAL_PRESETS: Record<string, { label: string; icon: string }> = {
|
|
347
|
+
x: { label: 'X', icon: 'x-twitter' },
|
|
348
|
+
twitter: { label: 'X', icon: 'x-twitter' },
|
|
349
|
+
github: { label: 'GitHub', icon: 'github' },
|
|
350
|
+
gitlab: { label: 'GitLab', icon: 'gitlab' },
|
|
351
|
+
linkedin: { label: 'LinkedIn', icon: 'linkedin' },
|
|
352
|
+
discord: { label: 'Discord', icon: 'discord' },
|
|
353
|
+
youtube: { label: 'YouTube', icon: 'youtube' },
|
|
354
|
+
facebook: { label: 'Facebook', icon: 'facebook' },
|
|
355
|
+
instagram: { label: 'Instagram', icon: 'instagram' },
|
|
356
|
+
slack: { label: 'Slack', icon: 'slack' },
|
|
357
|
+
medium: { label: 'Medium', icon: 'medium' },
|
|
358
|
+
reddit: { label: 'Reddit', icon: 'reddit' },
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
function normalizeSocialKey(value: string): string {
|
|
362
|
+
return value.trim().toLowerCase().replace(/[\s_]+/g, '-');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function titleCaseWords(value: string): string {
|
|
366
|
+
const normalized = value.trim().replace(/[_-]+/g, ' ');
|
|
367
|
+
if (!normalized) return 'Social';
|
|
368
|
+
return normalized
|
|
369
|
+
.split(/\s+/)
|
|
370
|
+
.filter(Boolean)
|
|
371
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
372
|
+
.join(' ');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function trimString(value: unknown): string | undefined {
|
|
376
|
+
if (typeof value !== 'string') return undefined;
|
|
377
|
+
const trimmed = value.trim();
|
|
378
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function normalizeFooterSocialMap(value: unknown): VeluFooterSocialLink[] {
|
|
382
|
+
if (!isRecord(value)) return [];
|
|
383
|
+
const links: VeluFooterSocialLink[] = [];
|
|
384
|
+
for (const [rawKey, rawHref] of Object.entries(value)) {
|
|
385
|
+
const href = trimString(rawHref);
|
|
386
|
+
if (!href) continue;
|
|
387
|
+
const key = normalizeSocialKey(rawKey);
|
|
388
|
+
if (!key) continue;
|
|
389
|
+
const preset = FOOTER_SOCIAL_PRESETS[key];
|
|
390
|
+
links.push({
|
|
391
|
+
key,
|
|
392
|
+
href,
|
|
393
|
+
label: preset?.label ?? titleCaseWords(key),
|
|
394
|
+
icon: preset?.icon ?? key,
|
|
395
|
+
iconType: 'brands',
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
return links;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function normalizeFooterSocialList(value: unknown): VeluFooterSocialLink[] {
|
|
402
|
+
if (!Array.isArray(value)) return [];
|
|
403
|
+
const links: VeluFooterSocialLink[] = [];
|
|
404
|
+
for (const entry of value) {
|
|
405
|
+
if (!isRecord(entry)) continue;
|
|
406
|
+
const href = trimString(entry.href) ?? trimString(entry.url);
|
|
407
|
+
if (!href) continue;
|
|
408
|
+
const key = normalizeSocialKey(
|
|
409
|
+
trimString(entry.type) ?? trimString(entry.icon) ?? trimString(entry.label) ?? 'social',
|
|
410
|
+
);
|
|
411
|
+
const preset = FOOTER_SOCIAL_PRESETS[key];
|
|
412
|
+
links.push({
|
|
413
|
+
key,
|
|
414
|
+
href,
|
|
415
|
+
label: trimString(entry.label) ?? preset?.label ?? titleCaseWords(key),
|
|
416
|
+
icon: trimString(entry.icon) ?? preset?.icon ?? key,
|
|
417
|
+
iconType: trimString(entry.iconType) ?? 'brands',
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return links;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function dedupeFooterSocialLinks(links: VeluFooterSocialLink[]): VeluFooterSocialLink[] {
|
|
424
|
+
const seen = new Set<string>();
|
|
425
|
+
const unique: VeluFooterSocialLink[] = [];
|
|
426
|
+
for (const link of links) {
|
|
427
|
+
const dedupeKey = `${link.href}::${link.icon}`;
|
|
428
|
+
if (seen.has(dedupeKey)) continue;
|
|
429
|
+
seen.add(dedupeKey);
|
|
430
|
+
unique.push(link);
|
|
431
|
+
}
|
|
432
|
+
return unique;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export interface VeluBannerConfig {
|
|
436
|
+
content: string;
|
|
437
|
+
dismissible: boolean;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function getBannerConfig(): VeluBannerConfig | null {
|
|
441
|
+
const config = loadVeluConfig();
|
|
442
|
+
const banner = config.banner;
|
|
443
|
+
if (!banner) return null;
|
|
444
|
+
const content = typeof banner.content === 'string' ? banner.content.trim() : '';
|
|
445
|
+
if (!content) return null;
|
|
446
|
+
return { content, dismissible: banner.dismissible === true };
|
|
447
|
+
}
|
|
448
|
+
|
|
261
449
|
export function getExternalTabs(): Array<{ label: string; href: string }> {
|
|
262
450
|
const config = loadVeluConfig();
|
|
263
451
|
const tabs = config.navigation?.tabs ?? [];
|
|
@@ -294,6 +482,14 @@ export function getGlobalAnchors(): VeluAnchor[] {
|
|
|
294
482
|
);
|
|
295
483
|
}
|
|
296
484
|
|
|
485
|
+
export function getFooterSocials(): VeluFooterSocialLink[] {
|
|
486
|
+
const config = loadVeluConfig();
|
|
487
|
+
const source = config.footer?.socials ?? config.footerSocials;
|
|
488
|
+
const fromMap = normalizeFooterSocialMap(source);
|
|
489
|
+
const fromList = normalizeFooterSocialList(source);
|
|
490
|
+
return dedupeFooterSocialLinks([...fromMap, ...fromList]);
|
|
491
|
+
}
|
|
492
|
+
|
|
297
493
|
export interface VeluTabMenuDefinition {
|
|
298
494
|
tab: string;
|
|
299
495
|
items: Array<{
|
|
@@ -357,6 +553,46 @@ export function getLanguages(): string[] {
|
|
|
357
553
|
return config.languages ?? [];
|
|
358
554
|
}
|
|
359
555
|
|
|
556
|
+
export function getDropdownOptions(): VeluDropdownOption[] {
|
|
557
|
+
const raw = loadRawConfig();
|
|
558
|
+
const navigation = isRecord(raw.navigation) ? raw.navigation : {};
|
|
559
|
+
const dropdowns = Array.isArray(navigation.dropdowns) ? navigation.dropdowns : [];
|
|
560
|
+
if (dropdowns.length === 0) return [];
|
|
561
|
+
|
|
562
|
+
const allTabs = loadVeluConfig().navigation.tabs ?? [];
|
|
563
|
+
const out: VeluDropdownOption[] = [];
|
|
564
|
+
|
|
565
|
+
dropdowns.forEach((entry, index) => {
|
|
566
|
+
if (!isRecord(entry)) return;
|
|
567
|
+
const dropdown = trimString(entry.dropdown);
|
|
568
|
+
if (!dropdown) return;
|
|
569
|
+
|
|
570
|
+
const slug = slugify(trimString(entry.slug) ?? dropdown, `dropdown-${index + 1}`);
|
|
571
|
+
const scopedTabs = allTabs.filter((tab) => {
|
|
572
|
+
const tabSlug = tab.slug ?? '';
|
|
573
|
+
return tabSlug === slug || tabSlug.startsWith(`${slug}/`);
|
|
574
|
+
});
|
|
575
|
+
const firstTab = scopedTabs[0];
|
|
576
|
+
const firstRoute = firstTab ? resolveFirstRouteInTab(firstTab) : undefined;
|
|
577
|
+
const href = trimString(entry.href);
|
|
578
|
+
const defaultPath = withTrailingSlashPath(firstRoute ?? href ?? `/${slug}`);
|
|
579
|
+
|
|
580
|
+
out.push({
|
|
581
|
+
dropdown,
|
|
582
|
+
slug,
|
|
583
|
+
description: trimString(entry.description),
|
|
584
|
+
icon: trimString(entry.icon),
|
|
585
|
+
iconType: trimString(entry.iconType),
|
|
586
|
+
tabSlugs: scopedTabs
|
|
587
|
+
.map((tab) => tab.slug)
|
|
588
|
+
.filter((tabSlug): tabSlug is string => typeof tabSlug === 'string' && tabSlug.length > 0),
|
|
589
|
+
defaultPath,
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
return out;
|
|
594
|
+
}
|
|
595
|
+
|
|
360
596
|
export function getProductOptions(): VeluProductOption[] {
|
|
361
597
|
const config = loadVeluConfig();
|
|
362
598
|
const products = (config.navigation.products ?? []).filter((p) => !p.hidden);
|
|
@@ -484,6 +720,10 @@ export interface VeluResolvedSeoConfig {
|
|
|
484
720
|
indexing: 'navigable' | 'all';
|
|
485
721
|
}
|
|
486
722
|
|
|
723
|
+
export interface VeluResolvedMetadataConfig {
|
|
724
|
+
timestamp: boolean;
|
|
725
|
+
}
|
|
726
|
+
|
|
487
727
|
function normalizePlaygroundDisplay(api: VeluApiConfig | undefined): PlaygroundDisplayMode {
|
|
488
728
|
const display = api?.playground?.display;
|
|
489
729
|
if (display === 'interactive' || display === 'simple' || display === 'none') return display;
|
|
@@ -629,6 +869,13 @@ export function getSeoConfig(): VeluResolvedSeoConfig {
|
|
|
629
869
|
};
|
|
630
870
|
}
|
|
631
871
|
|
|
872
|
+
export function getMetadataConfig(): VeluResolvedMetadataConfig {
|
|
873
|
+
const config = loadVeluConfig();
|
|
874
|
+
return {
|
|
875
|
+
timestamp: config.metadata?.timestamp === true,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
632
879
|
export function getSiteName(): string {
|
|
633
880
|
const config = loadVeluConfig();
|
|
634
881
|
const fromName = normalizeAssetPath(config.name);
|
|
@@ -638,6 +885,11 @@ export function getSiteName(): string {
|
|
|
638
885
|
return 'Velu Docs';
|
|
639
886
|
}
|
|
640
887
|
|
|
888
|
+
export function getSiteDescription(): string | undefined {
|
|
889
|
+
const config = loadVeluConfig();
|
|
890
|
+
return normalizeAssetPath(config.description);
|
|
891
|
+
}
|
|
892
|
+
|
|
641
893
|
export function getSiteFavicon(): string | undefined {
|
|
642
894
|
const config = loadVeluConfig();
|
|
643
895
|
const asset = normalizeThemeAsset(config.favicon);
|
|
@@ -656,6 +908,51 @@ export function getSitePrimaryColor(): string | undefined {
|
|
|
656
908
|
return normalizeAssetPath(colors.primary) ?? normalizeAssetPath(colors.light) ?? normalizeAssetPath(colors.dark);
|
|
657
909
|
}
|
|
658
910
|
|
|
911
|
+
export interface VeluContextualOption {
|
|
912
|
+
id: string;
|
|
913
|
+
title: string;
|
|
914
|
+
description: string;
|
|
915
|
+
href?: string;
|
|
916
|
+
type: 'builtin' | 'custom';
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const CONTEXTUAL_PRESETS: Record<string, { title: string; description: string }> = {
|
|
920
|
+
copy: { title: 'Copy page', description: 'Copy page as Markdown for LLMs' },
|
|
921
|
+
view: { title: 'View as Markdown', description: 'View this page in Markdown format' },
|
|
922
|
+
chatgpt: { title: 'Open in ChatGPT', description: 'Ask questions about this page' },
|
|
923
|
+
claude: { title: 'Open in Claude', description: 'Ask questions about this page' },
|
|
924
|
+
perplexity: { title: 'Open in Perplexity', description: 'Ask questions about this page' },
|
|
925
|
+
grok: { title: 'Open in Grok', description: 'Ask questions about this page' },
|
|
926
|
+
mcp: { title: 'Copy MCP URL', description: 'Copy the MCP server URL' },
|
|
927
|
+
'add-mcp': { title: 'Copy MCP install command', description: 'Copy npx command to install MCP server' },
|
|
928
|
+
cursor: { title: 'Connect to Cursor', description: 'Install MCP Server on Cursor' },
|
|
929
|
+
vscode: { title: 'Connect to VS Code', description: 'Install MCP Server on VS Code' },
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
const DEFAULT_CONTEXTUAL_OPTIONS = ['copy', 'chatgpt', 'claude', 'add-mcp', 'cursor', 'vscode'];
|
|
933
|
+
|
|
934
|
+
export function getContextualOptions(): VeluContextualOption[] {
|
|
935
|
+
const config = loadVeluConfig();
|
|
936
|
+
const raw = config.contextual?.options;
|
|
937
|
+
|
|
938
|
+
const ids = raw ?? DEFAULT_CONTEXTUAL_OPTIONS;
|
|
939
|
+
|
|
940
|
+
return ids.map((entry, index) => {
|
|
941
|
+
if (typeof entry === 'string') {
|
|
942
|
+
const preset = CONTEXTUAL_PRESETS[entry];
|
|
943
|
+
if (!preset) return null;
|
|
944
|
+
return { id: entry, title: preset.title, description: preset.description, type: 'builtin' as const };
|
|
945
|
+
}
|
|
946
|
+
return {
|
|
947
|
+
id: `custom-${index}`,
|
|
948
|
+
title: entry.title,
|
|
949
|
+
description: entry.description ?? '',
|
|
950
|
+
href: entry.href,
|
|
951
|
+
type: 'custom' as const,
|
|
952
|
+
};
|
|
953
|
+
}).filter((item): item is VeluContextualOption => item !== null);
|
|
954
|
+
}
|
|
955
|
+
|
|
659
956
|
export function getSiteOrigin(): string {
|
|
660
957
|
const seo = getSeoConfig();
|
|
661
958
|
const envCandidates = [
|
package/src/validate.ts
CHANGED
|
@@ -115,6 +115,8 @@ type VeluOpenApiSource = string | string[] | Record<string, unknown>;
|
|
|
115
115
|
|
|
116
116
|
interface VeluConfig {
|
|
117
117
|
$schema?: string;
|
|
118
|
+
variables?: Record<string, string>;
|
|
119
|
+
languages?: string[];
|
|
118
120
|
icons?: {
|
|
119
121
|
library?: "fontawesome" | "lucide" | "tabler";
|
|
120
122
|
};
|
|
@@ -145,6 +147,13 @@ interface VeluConfig {
|
|
|
145
147
|
};
|
|
146
148
|
};
|
|
147
149
|
};
|
|
150
|
+
metadata?: {
|
|
151
|
+
timestamp?: boolean;
|
|
152
|
+
};
|
|
153
|
+
footer?: {
|
|
154
|
+
socials?: Record<string, unknown>;
|
|
155
|
+
};
|
|
156
|
+
footerSocials?: Record<string, unknown>;
|
|
148
157
|
navigation: {
|
|
149
158
|
openapi?: VeluOpenApiSource;
|
|
150
159
|
asyncapi?: VeluOpenApiSource;
|
|
@@ -254,6 +263,28 @@ function collectPages(config: VeluConfig): string[] {
|
|
|
254
263
|
return collectPagesFromTabs(tabs);
|
|
255
264
|
}
|
|
256
265
|
|
|
266
|
+
function collectPagesByLanguage(config: VeluConfig): Record<string, string[]> {
|
|
267
|
+
const grouped: Record<string, string[]> = {};
|
|
268
|
+
|
|
269
|
+
if (config.navigation.languages && config.navigation.languages.length > 0) {
|
|
270
|
+
for (const lang of config.navigation.languages) {
|
|
271
|
+
grouped[lang.language] = collectPagesFromTabs(lang.tabs);
|
|
272
|
+
}
|
|
273
|
+
return grouped;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const basePages = collectPagesFromTabs(config.navigation.tabs ?? []);
|
|
277
|
+
if (config.languages && config.languages.length > 0) {
|
|
278
|
+
for (const lang of config.languages) {
|
|
279
|
+
grouped[lang] = [...basePages];
|
|
280
|
+
}
|
|
281
|
+
return grouped;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
grouped.english = basePages;
|
|
285
|
+
return grouped;
|
|
286
|
+
}
|
|
287
|
+
|
|
257
288
|
function validateVeluConfig(docsDir: string, schemaPath: string): { valid: boolean; errors: string[] } {
|
|
258
289
|
const errors: string[] = [];
|
|
259
290
|
|
|
@@ -323,4 +354,4 @@ function validateVeluConfig(docsDir: string, schemaPath: string): { valid: boole
|
|
|
323
354
|
return { valid: errors.length === 0, errors };
|
|
324
355
|
}
|
|
325
356
|
|
|
326
|
-
export { validateVeluConfig, collectPages, VeluConfig, VeluGroup, VeluTab, VeluSeparator, VeluLink, VeluAnchor };
|
|
357
|
+
export { validateVeluConfig, collectPages, collectPagesByLanguage, VeluConfig, VeluGroup, VeluTab, VeluSeparator, VeluLink, VeluAnchor };
|