@aravindc26/velu 0.8.0 → 0.9.1
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/README.md +7 -0
- package/package.json +19 -7
- package/src/build.ts +121 -1503
- package/src/cli.ts +24 -25
- package/src/engine/_server.mjs +271 -0
- package/src/engine/app/(docs)/[[...slug]]/page.tsx +66 -0
- package/src/engine/app/(docs)/layout.tsx +16 -0
- package/src/engine/app/assistant.css +326 -0
- package/src/engine/app/copy-page.css +132 -0
- package/src/engine/app/global.css +21 -0
- package/src/engine/app/layout.tsx +34 -0
- package/src/engine/app/search.css +118 -0
- package/src/engine/components/assistant.tsx +350 -0
- package/src/engine/components/copy-page.tsx +96 -0
- package/src/engine/components/search.tsx +164 -0
- package/src/engine/lib/layout.shared.ts +18 -0
- package/src/engine/lib/source.ts +9 -0
- package/src/engine/lib/velu.ts +42 -0
- package/src/engine/mdx-components.tsx +9 -0
- package/src/engine/next-env.d.ts +4 -0
- package/src/engine/next.config.mjs +22 -0
- package/src/engine/postcss.config.mjs +7 -0
- package/src/engine/source.config.ts +17 -0
- package/src/engine/src/components/Footer.astro +44 -0
- package/src/engine/src/components/PageTitle.astro +451 -0
- package/src/engine/src/components/Sidebar.astro +60 -0
- package/src/engine/src/content.config.ts +6 -0
- package/src/engine/src/lib/velu.ts +153 -0
- package/src/engine/src/styles/assistant.css +351 -0
- package/src/engine/src/styles/tabs.css +183 -0
- package/src/engine/tsconfig.json +32 -0
- package/src/themes.ts +34 -29
package/src/cli.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolve, join, dirname } from "node:path";
|
|
1
|
+
import { resolve, join, dirname, delimiter } from "node:path";
|
|
2
2
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -6,6 +6,17 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
7
|
const PACKAGE_ROOT = resolve(dirname(__filename), "..");
|
|
8
8
|
const SCHEMA_PATH = join(PACKAGE_ROOT, "schema", "velu.schema.json");
|
|
9
|
+
const NODE_MODULES_PATH = join(PACKAGE_ROOT, "node_modules");
|
|
10
|
+
|
|
11
|
+
/** Build env that lets spawned processes resolve deps from the CLI's own node_modules */
|
|
12
|
+
function engineEnv(docsDir?: string): NodeJS.ProcessEnv {
|
|
13
|
+
const existing = process.env.NODE_PATH || "";
|
|
14
|
+
return {
|
|
15
|
+
...process.env,
|
|
16
|
+
NODE_PATH: existing ? `${NODE_MODULES_PATH}${delimiter}${existing}` : NODE_MODULES_PATH,
|
|
17
|
+
...(docsDir ? { VELU_DOCS_DIR: docsDir } : {}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
9
20
|
|
|
10
21
|
// ── Help ────────────────────────────────────────────────────────────────────────
|
|
11
22
|
|
|
@@ -65,7 +76,7 @@ function init(targetDir: string) {
|
|
|
65
76
|
|
|
66
77
|
// Example pages
|
|
67
78
|
const pages: Record<string, string> = {
|
|
68
|
-
"quickstart.md": `# Quickstart\n\nWelcome to your new documentation site!\n\n## Prerequisites\n\n- Node.js
|
|
79
|
+
"quickstart.md": `# Quickstart\n\nWelcome to your new documentation site!\n\n## Prerequisites\n\n- Node.js 20.9+\n- npm\n\n## Getting Started\n\n1. Edit the markdown files in this directory\n2. Update \`velu.json\` to configure navigation\n3. Run \`velu run\` to start the dev server\n\n\`\`\`bash\nvelu run\n\`\`\`\n\nYour site is live at \`http://localhost:4321\`.\n`,
|
|
69
80
|
"installation.md": `# Installation\n\nInstall Velu globally:\n\n\`\`\`bash\nnpm install -g @aravindc26/velu\n\`\`\`\n\nOr run directly with npx:\n\n\`\`\`bash\nnpx @aravindc26/velu run\n\`\`\`\n`,
|
|
70
81
|
"guides/configuration.md": `# Configuration\n\nVelu uses a \`velu.json\` file to define your site's navigation.\n\n## Navigation Structure\n\n- **Tabs** — Top-level horizontal navigation\n- **Groups** — Collapsible sidebar sections within a tab\n- **Pages** — Individual markdown documents\n\n## Example\n\n\`\`\`json\n{\n "navigation": {\n "tabs": [\n {\n "tab": "Getting Started",\n "slug": "getting-started",\n "groups": [\n {\n "group": "Basics",\n "slug": "getting-started",\n "pages": ["quickstart"]\n }\n ]\n }\n ]\n }\n}\n\`\`\`\n`,
|
|
71
82
|
"guides/deployment.md": `# Deployment\n\nBuild your site for production:\n\n\`\`\`bash\nvelu build\n\`\`\`\n\nThe output is a static site you can deploy anywhere — Netlify, Vercel, GitHub Pages, etc.\n`,
|
|
@@ -115,16 +126,19 @@ async function lint(docsDir: string) {
|
|
|
115
126
|
|
|
116
127
|
async function generateProject(docsDir: string): Promise<string> {
|
|
117
128
|
const { build } = await import("./build.js");
|
|
118
|
-
|
|
129
|
+
// Place .velu-out inside CLI package dir so node_modules resolves
|
|
130
|
+
// naturally by walking up — avoids symlinks that Turbopack rejects.
|
|
131
|
+
const outDir = join(PACKAGE_ROOT, ".velu-out");
|
|
119
132
|
build(docsDir, outDir);
|
|
120
133
|
return outDir;
|
|
121
134
|
}
|
|
122
135
|
|
|
123
|
-
async function buildStatic(outDir: string) {
|
|
136
|
+
async function buildStatic(outDir: string, docsDir: string) {
|
|
124
137
|
await new Promise<void>((res, rej) => {
|
|
125
138
|
const child = spawn("node", ["_server.mjs", "build"], {
|
|
126
139
|
cwd: outDir,
|
|
127
140
|
stdio: "inherit",
|
|
141
|
+
env: engineEnv(docsDir),
|
|
128
142
|
});
|
|
129
143
|
child.on("exit", (code) => (code === 0 ? res() : rej(new Error(`Build exited with ${code}`))));
|
|
130
144
|
});
|
|
@@ -132,32 +146,18 @@ async function buildStatic(outDir: string) {
|
|
|
132
146
|
|
|
133
147
|
async function buildSite(docsDir: string) {
|
|
134
148
|
const outDir = await generateProject(docsDir);
|
|
135
|
-
await
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
console.log(`\n📁 Static site output: ${distDir}`);
|
|
149
|
+
await buildStatic(outDir, docsDir);
|
|
150
|
+
const staticOutDir = join(outDir, "dist");
|
|
151
|
+
console.log(`\n📁 Static site output: ${staticOutDir}`);
|
|
139
152
|
}
|
|
140
153
|
|
|
141
154
|
// ── run ──────────────────────────────────────────────────────────────────────────
|
|
142
155
|
|
|
143
|
-
|
|
144
|
-
if (!existsSync(join(outDir, "node_modules"))) {
|
|
145
|
-
console.log("\n📦 Installing dependencies...\n");
|
|
146
|
-
await new Promise<void>((res, rej) => {
|
|
147
|
-
const child = spawn("npm", ["install", "--silent"], {
|
|
148
|
-
cwd: outDir,
|
|
149
|
-
stdio: "inherit",
|
|
150
|
-
shell: true,
|
|
151
|
-
});
|
|
152
|
-
child.on("exit", (code) => (code === 0 ? res() : rej(new Error(`npm install exited with ${code}`))));
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function spawnServer(outDir: string, command: string, port: number) {
|
|
156
|
+
function spawnServer(outDir: string, command: string, port: number, docsDir: string) {
|
|
158
157
|
const child = spawn("node", ["_server.mjs", command, "--port", String(port)], {
|
|
159
158
|
cwd: outDir,
|
|
160
159
|
stdio: "inherit",
|
|
160
|
+
env: engineEnv(docsDir),
|
|
161
161
|
});
|
|
162
162
|
|
|
163
163
|
child.on("exit", (code) => process.exit(code ?? 0));
|
|
@@ -169,8 +169,7 @@ function spawnServer(outDir: string, command: string, port: number) {
|
|
|
169
169
|
|
|
170
170
|
async function run(docsDir: string, port: number) {
|
|
171
171
|
const outDir = await generateProject(docsDir);
|
|
172
|
-
|
|
173
|
-
spawnServer(outDir, "dev", port);
|
|
172
|
+
spawnServer(outDir, "dev", port, docsDir);
|
|
174
173
|
}
|
|
175
174
|
|
|
176
175
|
// ── Parse args ───────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { watch } from 'node:fs';
|
|
4
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, extname, join, resolve } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const nextBinPath = require.resolve('next/dist/bin/next');
|
|
9
|
+
|
|
10
|
+
// ── Docs directory (passed via env var from CLI) ────────────────────────────
|
|
11
|
+
const docsDir = process.env.VELU_DOCS_DIR || resolve('..');
|
|
12
|
+
const contentDir = resolve('content', 'docs');
|
|
13
|
+
|
|
14
|
+
function loadConfig() {
|
|
15
|
+
const raw = readFileSync(join(docsDir, 'velu.json'), 'utf-8');
|
|
16
|
+
return JSON.parse(raw);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function pageBasename(page) {
|
|
20
|
+
return page.split('/').pop();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function pageLabelFromSlug(slug) {
|
|
24
|
+
const last = slug.split('/').pop() || slug;
|
|
25
|
+
return last.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildArtifacts(config) {
|
|
29
|
+
const pageMap = [];
|
|
30
|
+
const metaFiles = [];
|
|
31
|
+
const rootTabs = (config.navigation?.tabs || []).filter((tab) => !tab.href);
|
|
32
|
+
const rootPages = rootTabs.map((tab) => tab.slug);
|
|
33
|
+
let firstPage = 'quickstart';
|
|
34
|
+
let hasFirstPage = false;
|
|
35
|
+
|
|
36
|
+
function trackFirstPage(dest) {
|
|
37
|
+
if (!hasFirstPage) {
|
|
38
|
+
firstPage = dest;
|
|
39
|
+
hasFirstPage = true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function addGroup(group, parentDir) {
|
|
44
|
+
const groupDir = `${parentDir}/${group.slug}`;
|
|
45
|
+
const pages = [];
|
|
46
|
+
|
|
47
|
+
for (const item of group.pages || []) {
|
|
48
|
+
if (typeof item === 'string') {
|
|
49
|
+
const basename = pageBasename(item);
|
|
50
|
+
const dest = `${groupDir}/${basename}`;
|
|
51
|
+
pageMap.push({ src: item, dest });
|
|
52
|
+
pages.push(basename);
|
|
53
|
+
trackFirstPage(dest);
|
|
54
|
+
} else {
|
|
55
|
+
addGroup(item, groupDir);
|
|
56
|
+
pages.push(item.slug);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const groupMeta = {
|
|
61
|
+
title: group.group,
|
|
62
|
+
pages,
|
|
63
|
+
defaultOpen: group.expanded !== false,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (group.icon) groupMeta.icon = group.icon;
|
|
67
|
+
|
|
68
|
+
metaFiles.push({ dir: groupDir, data: groupMeta });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const tab of rootTabs) {
|
|
72
|
+
const tabPages = [];
|
|
73
|
+
|
|
74
|
+
for (const group of tab.groups || []) {
|
|
75
|
+
addGroup(group, tab.slug);
|
|
76
|
+
tabPages.push(group.slug);
|
|
77
|
+
}
|
|
78
|
+
|
|
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);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const tabMeta = {
|
|
88
|
+
title: tab.tab,
|
|
89
|
+
root: true,
|
|
90
|
+
pages: tabPages,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (tab.icon) tabMeta.icon = tab.icon;
|
|
94
|
+
|
|
95
|
+
metaFiles.push({ dir: tab.slug, data: tabMeta });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (rootPages.length > 0) {
|
|
99
|
+
metaFiles.push({ dir: '', data: { pages: rootPages } });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { pageMap, metaFiles, firstPage };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function processPage(srcPath, destPath, slug) {
|
|
106
|
+
let content = readFileSync(srcPath, 'utf-8');
|
|
107
|
+
if (!content.startsWith('---')) {
|
|
108
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
109
|
+
const title = titleMatch ? titleMatch[1] : pageLabelFromSlug(slug);
|
|
110
|
+
if (titleMatch) {
|
|
111
|
+
content = content.replace(/^#\s+.+$/m, '').trimStart();
|
|
112
|
+
}
|
|
113
|
+
content = `---\ntitle: "${title}"\n---\n\n${content}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
117
|
+
writeFileSync(destPath, content, 'utf-8');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function writeMetaFiles(metaFiles) {
|
|
121
|
+
for (const meta of metaFiles) {
|
|
122
|
+
const metaPath = join(contentDir, meta.dir, 'meta.json');
|
|
123
|
+
mkdirSync(dirname(metaPath), { recursive: true });
|
|
124
|
+
writeFileSync(metaPath, JSON.stringify(meta.data, null, 2) + '\n', 'utf-8');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function writeIndexPage(firstPage) {
|
|
129
|
+
writeFileSync(
|
|
130
|
+
join(contentDir, 'index.mdx'),
|
|
131
|
+
`---\ntitle: "Overview"\ndescription: Documentation powered by Velu + Fumadocs\n---\n\nimport { Card, Cards } from "fumadocs-ui/components/card"\nimport { Callout } from "fumadocs-ui/components/callout"\n\n<Callout type="info">\n This site is powered by Velu + Fumadocs.\n</Callout>\n\n## Start here\n\n<Cards>\n <Card\n title="Read the docs"\n href="/${firstPage}/"\n description="Begin with the first page in your configured navigation."\n />\n</Cards>\n`,
|
|
132
|
+
'utf-8'
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function rebuildFromConfig() {
|
|
137
|
+
const config = loadConfig();
|
|
138
|
+
const artifacts = buildArtifacts(config);
|
|
139
|
+
|
|
140
|
+
rmSync(contentDir, { recursive: true, force: true });
|
|
141
|
+
mkdirSync(contentDir, { recursive: true });
|
|
142
|
+
|
|
143
|
+
writeMetaFiles(artifacts.metaFiles);
|
|
144
|
+
|
|
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);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
writeIndexPage(artifacts.firstPage);
|
|
153
|
+
return artifacts.pageMap;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let pageMap = rebuildFromConfig();
|
|
157
|
+
|
|
158
|
+
function syncMarkdownFile(filename) {
|
|
159
|
+
const srcSlug = filename.replace(/\\/g, '/').replace(/\.md$/, '');
|
|
160
|
+
const srcPath = join(docsDir, `${srcSlug}.md`);
|
|
161
|
+
|
|
162
|
+
if (!existsSync(srcPath)) {
|
|
163
|
+
pageMap = rebuildFromConfig();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const matches = pageMap.filter((entry) => entry.src === srcSlug);
|
|
168
|
+
if (matches.length === 0) return;
|
|
169
|
+
|
|
170
|
+
for (const match of matches) {
|
|
171
|
+
const destPath = join(contentDir, `${match.dest}.mdx`);
|
|
172
|
+
processPage(srcPath, destPath, srcSlug);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log(' \x1b[32m↻\x1b[0m ' + srcSlug);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function syncConfig() {
|
|
179
|
+
const srcPath = join(docsDir, 'velu.json');
|
|
180
|
+
copyFileSync(srcPath, resolve('velu.json'));
|
|
181
|
+
pageMap = rebuildFromConfig();
|
|
182
|
+
console.log(' \x1b[32m↻\x1b[0m velu.json updated (navigation/content synced)');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function startWatcher() {
|
|
186
|
+
const debounce = new Map();
|
|
187
|
+
|
|
188
|
+
watch(docsDir, { recursive: true }, (_, rawFilename) => {
|
|
189
|
+
if (!rawFilename) return;
|
|
190
|
+
const filename = rawFilename.replace(/\\/g, '/');
|
|
191
|
+
|
|
192
|
+
if (filename.startsWith('.velu-out/')) return;
|
|
193
|
+
if (filename.includes('node_modules')) return;
|
|
194
|
+
if (filename.startsWith('.')) return;
|
|
195
|
+
|
|
196
|
+
if (debounce.has(filename)) clearTimeout(debounce.get(filename));
|
|
197
|
+
debounce.set(
|
|
198
|
+
filename,
|
|
199
|
+
setTimeout(() => {
|
|
200
|
+
debounce.delete(filename);
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
if (filename === 'velu.json') {
|
|
204
|
+
syncConfig();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (extname(filename) === '.md') {
|
|
209
|
+
syncMarkdownFile(filename);
|
|
210
|
+
}
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.error(' \x1b[31m✗\x1b[0m Failed to sync ' + filename + ': ' + error.message);
|
|
213
|
+
}
|
|
214
|
+
}, 120)
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function runNext(command, port) {
|
|
220
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
221
|
+
const args = [nextBinPath, command];
|
|
222
|
+
if (command === 'dev' || command === 'start') {
|
|
223
|
+
args.push('--port', String(port));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const child = spawn(process.execPath, args, {
|
|
227
|
+
cwd: '.',
|
|
228
|
+
stdio: 'inherit',
|
|
229
|
+
env: process.env,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
child.on('exit', (code) => {
|
|
233
|
+
if (code === 0) resolvePromise();
|
|
234
|
+
else rejectPromise(new Error(`${command} exited with ${code}`));
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── CLI ──────────────────────────────────────────────────────────────────────
|
|
240
|
+
const args = process.argv.slice(2);
|
|
241
|
+
const command = args[0] || 'dev';
|
|
242
|
+
const portIdx = args.indexOf('--port');
|
|
243
|
+
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : 4321;
|
|
244
|
+
|
|
245
|
+
if (command === 'dev') {
|
|
246
|
+
console.log('');
|
|
247
|
+
console.log(' \x1b[36mvelu\x1b[0m fumadocs dev');
|
|
248
|
+
console.log('');
|
|
249
|
+
console.log(' watching for file changes...');
|
|
250
|
+
startWatcher();
|
|
251
|
+
await runNext('dev', port);
|
|
252
|
+
} else if (command === 'build') {
|
|
253
|
+
console.log('\n Building site...\n');
|
|
254
|
+
await runNext('build', port);
|
|
255
|
+
|
|
256
|
+
// Run Pagefind to index the static output for search
|
|
257
|
+
console.log(' Indexing for search...');
|
|
258
|
+
const pagefindBin = join(dirname(require.resolve('next/package.json')), '..', 'pagefind', 'lib', 'runner', 'bin.cjs');
|
|
259
|
+
await new Promise((res, rej) => {
|
|
260
|
+
const pf = spawn(process.execPath, [pagefindBin, '--site', 'dist', '--output-path', 'dist/pagefind'], {
|
|
261
|
+
cwd: '.',
|
|
262
|
+
stdio: 'inherit',
|
|
263
|
+
});
|
|
264
|
+
pf.on('exit', (code) => (code === 0 ? res() : rej(new Error(`pagefind exited with ${code}`))));
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
console.log('\n ✅ Site built successfully.\n');
|
|
268
|
+
} else {
|
|
269
|
+
console.error(`Unknown server command: ${command}`);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import { notFound } from 'next/navigation';
|
|
3
|
+
import { createRelativeLink } from 'fumadocs-ui/mdx';
|
|
4
|
+
import {
|
|
5
|
+
DocsBody,
|
|
6
|
+
DocsDescription,
|
|
7
|
+
DocsPage,
|
|
8
|
+
DocsTitle,
|
|
9
|
+
} from 'fumadocs-ui/layouts/docs/page';
|
|
10
|
+
import { getMDXComponents } from '@/mdx-components';
|
|
11
|
+
import { source } from '@/lib/source';
|
|
12
|
+
import { CopyPageButton } from '@/components/copy-page';
|
|
13
|
+
|
|
14
|
+
interface RouteParams {
|
|
15
|
+
slug?: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface PageProps {
|
|
19
|
+
params: Promise<RouteParams>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default async function Page({ params }: PageProps) {
|
|
23
|
+
const resolvedParams = await params;
|
|
24
|
+
const page = source.getPage(resolvedParams.slug);
|
|
25
|
+
|
|
26
|
+
if (!page) notFound();
|
|
27
|
+
|
|
28
|
+
const MDX = page.data.body;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<DocsPage toc={page.data.toc} full={page.data.full}>
|
|
32
|
+
<div data-pagefind-body data-pagefind-meta={`title:${page.data.title}`}>
|
|
33
|
+
<div className="velu-title-row">
|
|
34
|
+
<DocsTitle>{page.data.title}</DocsTitle>
|
|
35
|
+
<CopyPageButton />
|
|
36
|
+
</div>
|
|
37
|
+
{page.data.description ? <DocsDescription>{page.data.description}</DocsDescription> : null}
|
|
38
|
+
<DocsBody>
|
|
39
|
+
<MDX
|
|
40
|
+
components={getMDXComponents({
|
|
41
|
+
a: createRelativeLink(source, page),
|
|
42
|
+
})}
|
|
43
|
+
/>
|
|
44
|
+
</DocsBody>
|
|
45
|
+
</div>
|
|
46
|
+
</DocsPage>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function generateStaticParams() {
|
|
51
|
+
const params = source.generateParams();
|
|
52
|
+
// Include root path for the optional catch-all [[...slug]]
|
|
53
|
+
return [{ slug: [] }, ...params];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
|
57
|
+
const resolvedParams = await params;
|
|
58
|
+
const page = source.getPage(resolvedParams.slug);
|
|
59
|
+
|
|
60
|
+
if (!page) notFound();
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
title: page.data.title,
|
|
64
|
+
description: page.data.description,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
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
|
+
);
|
|
16
|
+
}
|