@aravindc26/velu 0.11.5 → 0.11.9
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 +157 -2
- package/src/engine/app/(docs)/[...slug]/layout.tsx +179 -13
- package/src/engine/app/(docs)/[...slug]/page.tsx +87 -12
- 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/page.tsx +4 -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/lang-switcher.tsx +7 -1
- package/src/engine/components/page-feedback.tsx +14 -3
- package/src/engine/components/product-switcher.tsx +8 -2
- package/src/engine/components/search.tsx +136 -49
- package/src/engine/components/version-switcher.tsx +8 -2
- package/src/engine/lib/layout.shared.ts +68 -68
- package/src/engine/lib/velu.ts +305 -2
- package/src/engine/next.config.mjs +4 -0
- package/src/validate.ts +8 -0
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
|
2
|
-
import { createElement } from 'react';
|
|
3
|
-
import { VersionSwitcher } from '@/components/version-switcher';
|
|
4
|
-
import { getExternalTabs, getNavbarAnchors, getSiteLogoAsset, getSiteName, getVersionOptions } from '@/lib/velu';
|
|
5
|
-
|
|
6
|
-
export function baseOptions(): BaseLayoutProps {
|
|
7
|
-
const externalTabs = getExternalTabs();
|
|
8
|
-
const navAnchors = getNavbarAnchors();
|
|
9
|
-
const versions = getVersionOptions();
|
|
10
|
-
const siteName = getSiteName();
|
|
11
|
-
const logo = getSiteLogoAsset();
|
|
12
|
-
const lightLogo = logo.light ?? logo.dark;
|
|
13
|
-
const darkLogo = logo.dark ?? logo.light;
|
|
14
|
-
const logoHref = typeof logo.href === 'string' && logo.href.trim().length > 0 ? logo.href.trim() : '/';
|
|
15
|
-
|
|
16
|
-
const navTitle =
|
|
17
|
-
lightLogo || darkLogo
|
|
18
|
-
? createElement(
|
|
19
|
-
'span',
|
|
20
|
-
{ className: 'velu-nav-brand' },
|
|
21
|
-
lightLogo
|
|
22
|
-
? createElement('img', {
|
|
23
|
-
src: lightLogo,
|
|
24
|
-
alt: siteName,
|
|
25
|
-
className: 'velu-nav-logo velu-nav-logo-light',
|
|
26
|
-
})
|
|
27
|
-
: null,
|
|
28
|
-
darkLogo
|
|
29
|
-
? createElement('img', {
|
|
30
|
-
src: darkLogo,
|
|
31
|
-
alt: siteName,
|
|
32
|
-
className: 'velu-nav-logo velu-nav-logo-dark',
|
|
33
|
-
})
|
|
34
|
-
: null,
|
|
35
|
-
)
|
|
36
|
-
: siteName;
|
|
37
|
-
|
|
38
|
-
const links = [
|
|
39
|
-
...externalTabs.map((tab: { label: string; href: string }) => ({
|
|
40
|
-
text: tab.label,
|
|
41
|
-
url: tab.href,
|
|
42
|
-
secondary: false,
|
|
43
|
-
})),
|
|
44
|
-
...navAnchors
|
|
45
|
-
.filter((a): a is { anchor: string; href: string } => typeof a.href === 'string' && a.href.length > 0)
|
|
46
|
-
.map((a) => ({
|
|
47
|
-
text: a.anchor,
|
|
48
|
-
url: a.href,
|
|
49
|
-
secondary: true,
|
|
50
|
-
})),
|
|
51
|
-
];
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
nav: {
|
|
55
|
-
title: navTitle,
|
|
56
|
-
url: logoHref,
|
|
57
|
-
children:
|
|
58
|
-
versions.length > 1
|
|
59
|
-
? createElement(
|
|
60
|
-
'div',
|
|
61
|
-
{ className: 'velu-header-version-switcher' },
|
|
62
|
-
createElement(VersionSwitcher, { versions })
|
|
63
|
-
)
|
|
64
|
-
: undefined,
|
|
65
|
-
},
|
|
66
|
-
links,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
1
|
+
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
|
2
|
+
import { createElement } from 'react';
|
|
3
|
+
import { VersionSwitcher } from '@/components/version-switcher';
|
|
4
|
+
import { getExternalTabs, getNavbarAnchors, getSiteLogoAsset, getSiteName, getVersionOptions } from '@/lib/velu';
|
|
5
|
+
|
|
6
|
+
export function baseOptions(): BaseLayoutProps {
|
|
7
|
+
const externalTabs = getExternalTabs();
|
|
8
|
+
const navAnchors = getNavbarAnchors();
|
|
9
|
+
const versions = getVersionOptions();
|
|
10
|
+
const siteName = getSiteName();
|
|
11
|
+
const logo = getSiteLogoAsset();
|
|
12
|
+
const lightLogo = logo.light ?? logo.dark;
|
|
13
|
+
const darkLogo = logo.dark ?? logo.light;
|
|
14
|
+
const logoHref = typeof logo.href === 'string' && logo.href.trim().length > 0 ? logo.href.trim() : '/';
|
|
15
|
+
|
|
16
|
+
const navTitle =
|
|
17
|
+
lightLogo || darkLogo
|
|
18
|
+
? createElement(
|
|
19
|
+
'span',
|
|
20
|
+
{ className: 'velu-nav-brand' },
|
|
21
|
+
lightLogo
|
|
22
|
+
? createElement('img', {
|
|
23
|
+
src: lightLogo,
|
|
24
|
+
alt: siteName,
|
|
25
|
+
className: 'velu-nav-logo velu-nav-logo-light',
|
|
26
|
+
})
|
|
27
|
+
: null,
|
|
28
|
+
darkLogo
|
|
29
|
+
? createElement('img', {
|
|
30
|
+
src: darkLogo,
|
|
31
|
+
alt: siteName,
|
|
32
|
+
className: 'velu-nav-logo velu-nav-logo-dark',
|
|
33
|
+
})
|
|
34
|
+
: null,
|
|
35
|
+
)
|
|
36
|
+
: siteName;
|
|
37
|
+
|
|
38
|
+
const links = [
|
|
39
|
+
...externalTabs.map((tab: { label: string; href: string }) => ({
|
|
40
|
+
text: tab.label,
|
|
41
|
+
url: tab.href,
|
|
42
|
+
secondary: false,
|
|
43
|
+
})),
|
|
44
|
+
...navAnchors
|
|
45
|
+
.filter((a): a is { anchor: string; href: string } => typeof a.href === 'string' && a.href.length > 0)
|
|
46
|
+
.map((a) => ({
|
|
47
|
+
text: a.anchor,
|
|
48
|
+
url: a.href,
|
|
49
|
+
secondary: true,
|
|
50
|
+
})),
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
nav: {
|
|
55
|
+
title: navTitle,
|
|
56
|
+
url: logoHref,
|
|
57
|
+
children:
|
|
58
|
+
versions.length > 1
|
|
59
|
+
? createElement(
|
|
60
|
+
'div',
|
|
61
|
+
{ className: 'velu-header-version-switcher' },
|
|
62
|
+
createElement(VersionSwitcher, { versions })
|
|
63
|
+
)
|
|
64
|
+
: undefined,
|
|
65
|
+
},
|
|
66
|
+
links,
|
|
67
|
+
};
|
|
68
|
+
}
|
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[];
|
|
@@ -211,6 +261,12 @@ function pageBasename(page: string): string {
|
|
|
211
261
|
return parts[parts.length - 1] ?? page;
|
|
212
262
|
}
|
|
213
263
|
|
|
264
|
+
function withTrailingSlashPath(path: string): string {
|
|
265
|
+
if (!path.startsWith('/')) return path;
|
|
266
|
+
if (path === '/' || path.endsWith('/')) return path;
|
|
267
|
+
return `${path}/`;
|
|
268
|
+
}
|
|
269
|
+
|
|
214
270
|
function findFirstPageInGroup(group: VeluGroup): string | undefined {
|
|
215
271
|
for (const item of group.pages) {
|
|
216
272
|
if (typeof item === 'string') return item;
|
|
@@ -237,6 +293,41 @@ function findFirstPageInTab(tab: VeluTab): string | undefined {
|
|
|
237
293
|
return undefined;
|
|
238
294
|
}
|
|
239
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
|
+
|
|
240
331
|
function parseVersionParts(version: string): number[] {
|
|
241
332
|
const parts = version.match(/\d+/g);
|
|
242
333
|
return parts ? parts.map((n) => Number(n)) : [];
|
|
@@ -252,6 +343,109 @@ function compareVersionParts(a: number[], b: number[]): number {
|
|
|
252
343
|
return 0;
|
|
253
344
|
}
|
|
254
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
|
+
|
|
255
449
|
export function getExternalTabs(): Array<{ label: string; href: string }> {
|
|
256
450
|
const config = loadVeluConfig();
|
|
257
451
|
const tabs = config.navigation?.tabs ?? [];
|
|
@@ -288,6 +482,14 @@ export function getGlobalAnchors(): VeluAnchor[] {
|
|
|
288
482
|
);
|
|
289
483
|
}
|
|
290
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
|
+
|
|
291
493
|
export interface VeluTabMenuDefinition {
|
|
292
494
|
tab: string;
|
|
293
495
|
items: Array<{
|
|
@@ -351,6 +553,46 @@ export function getLanguages(): string[] {
|
|
|
351
553
|
return config.languages ?? [];
|
|
352
554
|
}
|
|
353
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
|
+
|
|
354
596
|
export function getProductOptions(): VeluProductOption[] {
|
|
355
597
|
const config = loadVeluConfig();
|
|
356
598
|
const products = (config.navigation.products ?? []).filter((p) => !p.hidden);
|
|
@@ -382,7 +624,7 @@ export function getProductOptions(): VeluProductOption[] {
|
|
|
382
624
|
icon: product.icon,
|
|
383
625
|
iconType: product.iconType,
|
|
384
626
|
tabSlugs,
|
|
385
|
-
defaultPath,
|
|
627
|
+
defaultPath: withTrailingSlashPath(defaultPath),
|
|
386
628
|
};
|
|
387
629
|
});
|
|
388
630
|
}
|
|
@@ -417,7 +659,7 @@ export function getVersionOptions(): VeluVersionOption[] {
|
|
|
417
659
|
explicitDefault: version.default === true,
|
|
418
660
|
versionParts: parseVersionParts(version.version),
|
|
419
661
|
tabSlugs,
|
|
420
|
-
defaultPath,
|
|
662
|
+
defaultPath: withTrailingSlashPath(defaultPath),
|
|
421
663
|
order: index,
|
|
422
664
|
};
|
|
423
665
|
});
|
|
@@ -478,6 +720,10 @@ export interface VeluResolvedSeoConfig {
|
|
|
478
720
|
indexing: 'navigable' | 'all';
|
|
479
721
|
}
|
|
480
722
|
|
|
723
|
+
export interface VeluResolvedMetadataConfig {
|
|
724
|
+
timestamp: boolean;
|
|
725
|
+
}
|
|
726
|
+
|
|
481
727
|
function normalizePlaygroundDisplay(api: VeluApiConfig | undefined): PlaygroundDisplayMode {
|
|
482
728
|
const display = api?.playground?.display;
|
|
483
729
|
if (display === 'interactive' || display === 'simple' || display === 'none') return display;
|
|
@@ -623,6 +869,13 @@ export function getSeoConfig(): VeluResolvedSeoConfig {
|
|
|
623
869
|
};
|
|
624
870
|
}
|
|
625
871
|
|
|
872
|
+
export function getMetadataConfig(): VeluResolvedMetadataConfig {
|
|
873
|
+
const config = loadVeluConfig();
|
|
874
|
+
return {
|
|
875
|
+
timestamp: config.metadata?.timestamp === true,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
626
879
|
export function getSiteName(): string {
|
|
627
880
|
const config = loadVeluConfig();
|
|
628
881
|
const fromName = normalizeAssetPath(config.name);
|
|
@@ -632,6 +885,11 @@ export function getSiteName(): string {
|
|
|
632
885
|
return 'Velu Docs';
|
|
633
886
|
}
|
|
634
887
|
|
|
888
|
+
export function getSiteDescription(): string | undefined {
|
|
889
|
+
const config = loadVeluConfig();
|
|
890
|
+
return normalizeAssetPath(config.description);
|
|
891
|
+
}
|
|
892
|
+
|
|
635
893
|
export function getSiteFavicon(): string | undefined {
|
|
636
894
|
const config = loadVeluConfig();
|
|
637
895
|
const asset = normalizeThemeAsset(config.favicon);
|
|
@@ -650,6 +908,51 @@ export function getSitePrimaryColor(): string | undefined {
|
|
|
650
908
|
return normalizeAssetPath(colors.primary) ?? normalizeAssetPath(colors.light) ?? normalizeAssetPath(colors.dark);
|
|
651
909
|
}
|
|
652
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
|
+
|
|
653
956
|
export function getSiteOrigin(): string {
|
|
654
957
|
const seo = getSeoConfig();
|
|
655
958
|
const envCandidates = [
|
|
@@ -9,6 +9,10 @@ const withMDX = createMDX({
|
|
|
9
9
|
const config = {
|
|
10
10
|
reactStrictMode: false,
|
|
11
11
|
output: process.env.NODE_ENV === 'production' ? 'export' : undefined,
|
|
12
|
+
// For static hosts without rewrite rules, emit directory routes
|
|
13
|
+
// (e.g. /docs/page/index.html) so extensionless URLs resolve.
|
|
14
|
+
trailingSlash: true,
|
|
15
|
+
skipTrailingSlashRedirect: true,
|
|
12
16
|
distDir: 'dist',
|
|
13
17
|
devIndicators: false,
|
|
14
18
|
turbopack: {
|
package/src/validate.ts
CHANGED
|
@@ -115,6 +115,7 @@ type VeluOpenApiSource = string | string[] | Record<string, unknown>;
|
|
|
115
115
|
|
|
116
116
|
interface VeluConfig {
|
|
117
117
|
$schema?: string;
|
|
118
|
+
variables?: Record<string, string>;
|
|
118
119
|
icons?: {
|
|
119
120
|
library?: "fontawesome" | "lucide" | "tabler";
|
|
120
121
|
};
|
|
@@ -145,6 +146,13 @@ interface VeluConfig {
|
|
|
145
146
|
};
|
|
146
147
|
};
|
|
147
148
|
};
|
|
149
|
+
metadata?: {
|
|
150
|
+
timestamp?: boolean;
|
|
151
|
+
};
|
|
152
|
+
footer?: {
|
|
153
|
+
socials?: Record<string, unknown>;
|
|
154
|
+
};
|
|
155
|
+
footerSocials?: Record<string, unknown>;
|
|
148
156
|
navigation: {
|
|
149
157
|
openapi?: VeluOpenApiSource;
|
|
150
158
|
asyncapi?: VeluOpenApiSource;
|