@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/engine/_server.mjs
CHANGED
|
@@ -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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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 = {
|
|
@@ -128,28 +165,100 @@ function writeMetaFiles(metaFiles) {
|
|
|
128
165
|
function writeIndexPage(firstPage) {
|
|
129
166
|
writeFileSync(
|
|
130
167
|
join(contentDir, 'index.mdx'),
|
|
131
|
-
`---\ntitle: "Overview"\ndescription: Documentation powered by Velu
|
|
168
|
+
`---\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`,
|
|
169
|
+
'utf-8'
|
|
170
|
+
);
|
|
171
|
+
}
|
|
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`,
|
|
132
203
|
'utf-8'
|
|
133
204
|
);
|
|
134
205
|
}
|
|
135
206
|
|
|
136
207
|
function rebuildFromConfig() {
|
|
137
208
|
const config = loadConfig();
|
|
138
|
-
const
|
|
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
|
-
|
|
215
|
+
// ── Mode 1: Per-language navigation (Mintlify-style) ──────────────
|
|
216
|
+
if (navLanguages && navLanguages.length > 0) {
|
|
217
|
+
const rootPages = [];
|
|
144
218
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
|
@@ -244,7 +353,7 @@ const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : 4321;
|
|
|
244
353
|
|
|
245
354
|
if (command === 'dev') {
|
|
246
355
|
console.log('');
|
|
247
|
-
console.log(' \x1b[36mvelu\x1b[0m
|
|
356
|
+
console.log(' \x1b[36mvelu\x1b[0m dev server');
|
|
248
357
|
console.log('');
|
|
249
358
|
console.log(' watching for file changes...');
|
|
250
359
|
startWatcher();
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
|
3
|
+
import { baseOptions } from '@/lib/layout.shared';
|
|
4
|
+
import { source } from '@/lib/source';
|
|
5
|
+
import { getLanguages, getVersionOptions, getProductOptions, type VeluVersionOption, type VeluProductOption } from '@/lib/velu';
|
|
6
|
+
import { SidebarLinks } from '@/components/sidebar-links';
|
|
7
|
+
import { ProductSwitcher } from '@/components/product-switcher';
|
|
8
|
+
|
|
9
|
+
interface LayoutParams {
|
|
10
|
+
slug?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface SlugLayoutProps {
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
params: Promise<LayoutParams>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveLocale(slugInput: string[] | undefined): string {
|
|
19
|
+
const languages = getLanguages();
|
|
20
|
+
const defaultLanguage = languages[0] ?? 'en';
|
|
21
|
+
const slug = slugInput ?? [];
|
|
22
|
+
const firstSeg = slug[0];
|
|
23
|
+
|
|
24
|
+
return languages.includes(firstSeg ?? '') ? firstSeg! : defaultLanguage;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveCurrentVersion(slugInput: string[] | undefined, versions: VeluVersionOption[]): VeluVersionOption | undefined {
|
|
28
|
+
if (versions.length === 0) return undefined;
|
|
29
|
+
const firstSeg = (slugInput ?? [])[0] ?? '';
|
|
30
|
+
return versions.find((v) => v.slug === firstSeg) ?? versions.find((v) => v.isDefault) ?? versions[0];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function filterTreeBySlugPrefix<T extends { children?: unknown[] }>(tree: T, prefix?: string): T {
|
|
34
|
+
if (!prefix) return tree;
|
|
35
|
+
|
|
36
|
+
const children = Array.isArray(tree.children) ? tree.children : [];
|
|
37
|
+
|
|
38
|
+
const filtered = children.filter((node) => {
|
|
39
|
+
if (typeof node !== 'object' || node === null) return false;
|
|
40
|
+
const entry = node as { url?: unknown; path?: unknown; $ref?: { metaFile?: unknown; file?: unknown } };
|
|
41
|
+
const candidates = [entry.url, entry.path, entry.$ref?.metaFile, entry.$ref?.file]
|
|
42
|
+
.filter((value): value is string => typeof value === 'string');
|
|
43
|
+
return candidates.some((value) => value.includes(`${prefix}/`));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (filtered.length === 0) return tree;
|
|
47
|
+
return { ...tree, children: filtered } as T;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveCurrentProduct(slugInput: string[] | undefined, products: VeluProductOption[]): VeluProductOption | undefined {
|
|
51
|
+
if (products.length === 0) return undefined;
|
|
52
|
+
const firstSeg = (slugInput ?? [])[0] ?? '';
|
|
53
|
+
return products.find((p) => p.slug === firstSeg) ?? products[0];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default async function SlugLayout({ children, params }: SlugLayoutProps) {
|
|
57
|
+
const resolvedParams = await params;
|
|
58
|
+
const locale = resolveLocale(resolvedParams.slug);
|
|
59
|
+
const versions = getVersionOptions();
|
|
60
|
+
const products = getProductOptions();
|
|
61
|
+
const currentVersion = resolveCurrentVersion(resolvedParams.slug, versions);
|
|
62
|
+
const currentProduct = resolveCurrentProduct(resolvedParams.slug, products);
|
|
63
|
+
|
|
64
|
+
const activePrefix = currentVersion?.slug ?? currentProduct?.slug;
|
|
65
|
+
const tree = filterTreeBySlugPrefix(source.getPageTree(locale), activePrefix);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<DocsLayout
|
|
69
|
+
tree={tree}
|
|
70
|
+
sidebar={{
|
|
71
|
+
collapsible: false,
|
|
72
|
+
banner: products.length > 1 ? (
|
|
73
|
+
<div className="velu-sidebar-banner">
|
|
74
|
+
<ProductSwitcher products={products} />
|
|
75
|
+
</div>
|
|
76
|
+
) : undefined,
|
|
77
|
+
footer: <SidebarLinks />,
|
|
78
|
+
}}
|
|
79
|
+
{...baseOptions()}
|
|
80
|
+
themeSwitch={{ enabled: false }}
|
|
81
|
+
>
|
|
82
|
+
{children}
|
|
83
|
+
</DocsLayout>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { generateStaticParams } from './page';
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from 'fumadocs-ui/layouts/docs/page';
|
|
10
10
|
import { getMDXComponents } from '@/mdx-components';
|
|
11
11
|
import { source } from '@/lib/source';
|
|
12
|
+
import { getLanguages, getVersionOptions, getProductOptions } from '@/lib/velu';
|
|
12
13
|
import { CopyPageButton } from '@/components/copy-page';
|
|
13
14
|
|
|
14
15
|
interface RouteParams {
|
|
@@ -19,17 +20,79 @@ interface PageProps {
|
|
|
19
20
|
params: Promise<RouteParams>;
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
function resolveLocaleSlug(slugInput: string[] | undefined) {
|
|
24
|
+
const languages = getLanguages();
|
|
25
|
+
const defaultLanguage = languages[0] ?? 'en';
|
|
26
|
+
const slug = slugInput ?? [];
|
|
27
|
+
const firstSeg = slug[0];
|
|
28
|
+
const hasLocalePrefix = languages.includes(firstSeg ?? '');
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
defaultLanguage,
|
|
32
|
+
locale: hasLocalePrefix ? firstSeg! : defaultLanguage,
|
|
33
|
+
pageSlug: hasLocalePrefix ? slug.slice(1) : slug,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveContextFromSlug(slugInput: string[] | undefined) {
|
|
38
|
+
const languages = getLanguages();
|
|
39
|
+
const versions = getVersionOptions();
|
|
40
|
+
const products = getProductOptions();
|
|
41
|
+
const slug = slugInput ?? [];
|
|
42
|
+
|
|
43
|
+
// Check for language prefix
|
|
44
|
+
const firstSeg = slug[0];
|
|
45
|
+
const hasLocalePrefix = languages.includes(firstSeg ?? '');
|
|
46
|
+
const locale = hasLocalePrefix ? firstSeg! : (languages[0] ?? 'en');
|
|
47
|
+
const remainingSlug = hasLocalePrefix ? slug.slice(1) : slug;
|
|
48
|
+
|
|
49
|
+
// Check for version/product in remaining slug
|
|
50
|
+
const contextSeg = remainingSlug[0] ?? '';
|
|
51
|
+
const version = versions.find((v) => v.slug === contextSeg);
|
|
52
|
+
const product = products.find((p) => p.slug === contextSeg);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
locale,
|
|
56
|
+
version: version?.slug,
|
|
57
|
+
product: product?.slug,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
22
61
|
export default async function Page({ params }: PageProps) {
|
|
23
62
|
const resolvedParams = await params;
|
|
24
|
-
const
|
|
63
|
+
const { locale, pageSlug } = resolveLocaleSlug(resolvedParams.slug);
|
|
64
|
+
const { locale: filterLocale, version, product } = resolveContextFromSlug(resolvedParams.slug);
|
|
65
|
+
const hasI18n = getLanguages().length > 1;
|
|
66
|
+
|
|
67
|
+
const page = hasI18n ? source.getPage(pageSlug, locale) : source.getPage(pageSlug);
|
|
25
68
|
|
|
26
69
|
if (!page) notFound();
|
|
27
70
|
|
|
28
71
|
const MDX = page.data.body;
|
|
29
72
|
|
|
73
|
+
// Build pagefind filter attributes
|
|
74
|
+
const metaAttrs: string[] = [`title:${page.data.title}`];
|
|
75
|
+
const filterAttrs: string[] = [];
|
|
76
|
+
if (hasI18n) {
|
|
77
|
+
metaAttrs.push(`language:${filterLocale}`);
|
|
78
|
+
filterAttrs.push(`language:${filterLocale}`);
|
|
79
|
+
}
|
|
80
|
+
if (version) {
|
|
81
|
+
metaAttrs.push(`version:${version}`);
|
|
82
|
+
filterAttrs.push(`version:${version}`);
|
|
83
|
+
}
|
|
84
|
+
if (product) {
|
|
85
|
+
metaAttrs.push(`product:${product}`);
|
|
86
|
+
filterAttrs.push(`product:${product}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
30
89
|
return (
|
|
31
90
|
<DocsPage toc={page.data.toc} full={page.data.full}>
|
|
32
|
-
<div
|
|
91
|
+
<div
|
|
92
|
+
data-pagefind-body
|
|
93
|
+
data-pagefind-meta={metaAttrs.join(',')}
|
|
94
|
+
data-pagefind-filter={filterAttrs.length > 0 ? filterAttrs.join(',') : undefined}
|
|
95
|
+
>
|
|
33
96
|
<div className="velu-title-row">
|
|
34
97
|
<DocsTitle>{page.data.title}</DocsTitle>
|
|
35
98
|
<CopyPageButton />
|
|
@@ -43,19 +106,36 @@ export default async function Page({ params }: PageProps) {
|
|
|
43
106
|
/>
|
|
44
107
|
</DocsBody>
|
|
45
108
|
</div>
|
|
109
|
+
<footer className="velu-footer">
|
|
110
|
+
Powered by <a href="https://getvelu.com" target="_blank" rel="noopener noreferrer">Velu</a>
|
|
111
|
+
</footer>
|
|
46
112
|
</DocsPage>
|
|
47
113
|
);
|
|
48
114
|
}
|
|
49
115
|
|
|
50
116
|
export async function generateStaticParams() {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
117
|
+
const generated = source.generateParams('slug') as Array<{ slug?: string[] }>;
|
|
118
|
+
const seen = new Set<string>();
|
|
119
|
+
|
|
120
|
+
const nonRoot = generated.filter((entry) => {
|
|
121
|
+
const slug = entry.slug ?? [];
|
|
122
|
+
if (slug.length === 0) return false;
|
|
123
|
+
const key = slug.join('/');
|
|
124
|
+
if (seen.has(key)) return false;
|
|
125
|
+
seen.add(key);
|
|
126
|
+
return true;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Include root variants for optional catch-all [[...slug]] in export mode.
|
|
130
|
+
return [{}, { slug: undefined }, { slug: [] }, ...nonRoot];
|
|
54
131
|
}
|
|
55
132
|
|
|
56
133
|
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
|
57
134
|
const resolvedParams = await params;
|
|
58
|
-
const
|
|
135
|
+
const { locale, pageSlug } = resolveLocaleSlug(resolvedParams.slug);
|
|
136
|
+
const hasI18n = getLanguages().length > 1;
|
|
137
|
+
|
|
138
|
+
const page = hasI18n ? source.getPage(pageSlug, locale) : source.getPage(pageSlug);
|
|
59
139
|
|
|
60
140
|
if (!page) notFound();
|
|
61
141
|
|
|
@@ -1,16 +1,4 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
|
3
|
-
import { baseOptions } from '@/lib/layout.shared';
|
|
4
|
-
import { source } from '@/lib/source';
|
|
5
|
-
|
|
6
2
|
export default function DocsRootLayout({ children }: { children: ReactNode }) {
|
|
7
|
-
return
|
|
8
|
-
<DocsLayout
|
|
9
|
-
tree={source.getPageTree()}
|
|
10
|
-
sidebar={{ collapsible: false }}
|
|
11
|
-
{...baseOptions()}
|
|
12
|
-
>
|
|
13
|
-
{children}
|
|
14
|
-
</DocsLayout>
|
|
15
|
-
);
|
|
3
|
+
return children;
|
|
16
4
|
}
|