@aravindc26/velu 0.11.4 → 0.11.6
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/src/cli.ts +98 -1
- package/src/engine/app/(docs)/[...slug]/layout.tsx +27 -3
- package/src/engine/app/(docs)/[...slug]/page.tsx +10 -3
- package/src/engine/app/page.tsx +4 -1
- 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/version-switcher.tsx +8 -2
- package/src/engine/lib/velu.ts +8 -2
- package/src/engine/next.config.mjs +4 -0
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolve, join, dirname, delimiter } from "node:path";
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync, readdirSync, copyFileSync, cpSync, rmSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, readdirSync, copyFileSync, cpSync, rmSync, readFileSync } from "node:fs";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
@@ -61,6 +61,11 @@ function init(targetDir: string) {
|
|
|
61
61
|
const config = {
|
|
62
62
|
$schema: "https://raw.githubusercontent.com/aravindc26/velu/main/schema/velu.schema.json",
|
|
63
63
|
theme: "neutral" as const,
|
|
64
|
+
colors: {
|
|
65
|
+
primary: "#DC143C",
|
|
66
|
+
light: "#DC143C",
|
|
67
|
+
dark: "#DC143C",
|
|
68
|
+
},
|
|
64
69
|
navigation: {
|
|
65
70
|
tabs: [
|
|
66
71
|
{
|
|
@@ -256,11 +261,103 @@ function exportMarkdownRoutes(outDir: string) {
|
|
|
256
261
|
console.log(`📝 Exported ${copied} markdown files to static route paths`);
|
|
257
262
|
}
|
|
258
263
|
|
|
264
|
+
function collectStaticRoutePaths(distDir: string): string[] {
|
|
265
|
+
const routes = new Set<string>();
|
|
266
|
+
|
|
267
|
+
function walk(relDir: string) {
|
|
268
|
+
const absDir = join(distDir, relDir);
|
|
269
|
+
const entries = readdirSync(absDir, { withFileTypes: true });
|
|
270
|
+
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
if (!entry.isDirectory()) continue;
|
|
273
|
+
const childRel = relDir ? join(relDir, entry.name) : entry.name;
|
|
274
|
+
if (existsSync(join(distDir, childRel, "index.html"))) {
|
|
275
|
+
const normalized = childRel.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
|
276
|
+
if (
|
|
277
|
+
normalized.length > 0 &&
|
|
278
|
+
!normalized.startsWith("_next") &&
|
|
279
|
+
!normalized.startsWith("_not-found") &&
|
|
280
|
+
normalized !== "404" &&
|
|
281
|
+
!normalized.startsWith("pagefind")
|
|
282
|
+
) {
|
|
283
|
+
routes.add(`/${normalized}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
walk(childRel);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
walk("");
|
|
291
|
+
return Array.from(routes).sort((a, b) => a.localeCompare(b));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function addStaticRouteCompatibility(outDir: string) {
|
|
295
|
+
const distDir = join(outDir, "dist");
|
|
296
|
+
if (!existsSync(distDir)) return;
|
|
297
|
+
|
|
298
|
+
const routes = collectStaticRoutePaths(distDir);
|
|
299
|
+
if (routes.length === 0) return;
|
|
300
|
+
|
|
301
|
+
let aliasCount = 0;
|
|
302
|
+
for (const route of routes) {
|
|
303
|
+
const rel = route.replace(/^\/+/, "");
|
|
304
|
+
const src = join(distDir, rel, "index.html");
|
|
305
|
+
const htmlAlias = join(distDir, `${rel}.html`);
|
|
306
|
+
if (existsSync(src) && !existsSync(htmlAlias)) {
|
|
307
|
+
copyFileSync(src, htmlAlias);
|
|
308
|
+
aliasCount += 1;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const fallbackPath = join(distDir, "404.html");
|
|
313
|
+
if (existsSync(fallbackPath)) {
|
|
314
|
+
const html = readFileSync(fallbackPath, "utf-8");
|
|
315
|
+
if (!html.includes("velu-noslash-fallback")) {
|
|
316
|
+
const script = [
|
|
317
|
+
'<script id="velu-noslash-fallback">',
|
|
318
|
+
"(function(){",
|
|
319
|
+
" try {",
|
|
320
|
+
` var routes = new Set(${JSON.stringify(routes)});`,
|
|
321
|
+
" var path = (window.location && window.location.pathname ? window.location.pathname : '/').replace(/\\/+$/, '');",
|
|
322
|
+
" if (!path || path === '/') return;",
|
|
323
|
+
" if (/\\.[a-zA-Z0-9]+$/.test(path)) return;",
|
|
324
|
+
" if (!routes.has(path)) return;",
|
|
325
|
+
" var search = window.location.search || '';",
|
|
326
|
+
" var hash = window.location.hash || '';",
|
|
327
|
+
" window.location.replace(path + '/' + search + hash);",
|
|
328
|
+
" } catch (_) {}",
|
|
329
|
+
"})();",
|
|
330
|
+
"</script>",
|
|
331
|
+
].join("");
|
|
332
|
+
const patched = html.includes("</body>") ? html.replace("</body>", `${script}</body>`) : `${html}\n${script}\n`;
|
|
333
|
+
writeFileSync(fallbackPath, patched, "utf-8");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const redirectsPath = join(distDir, "_redirects");
|
|
338
|
+
const existingRedirects = existsSync(redirectsPath) ? readFileSync(redirectsPath, "utf-8") : "";
|
|
339
|
+
const existingLines = new Set(existingRedirects.split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
|
|
340
|
+
const redirectLines = routes.map((route) => `${route} ${route}/ 301`);
|
|
341
|
+
let redirectAdded = 0;
|
|
342
|
+
for (const line of redirectLines) {
|
|
343
|
+
if (existingLines.has(line)) continue;
|
|
344
|
+
existingLines.add(line);
|
|
345
|
+
redirectAdded += 1;
|
|
346
|
+
}
|
|
347
|
+
if (redirectAdded > 0) {
|
|
348
|
+
const merged = Array.from(existingLines).join("\n") + "\n";
|
|
349
|
+
writeFileSync(redirectsPath, merged, "utf-8");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
console.log(`🔁 Added static compatibility for ${routes.length} routes (${aliasCount} .html aliases, ${redirectAdded} redirects)`);
|
|
353
|
+
}
|
|
354
|
+
|
|
259
355
|
async function buildSite(docsDir: string) {
|
|
260
356
|
const docsOutDir = await generateProject(docsDir);
|
|
261
357
|
const runtimeOutDir = prepareRuntimeOutDir(docsOutDir);
|
|
262
358
|
await buildStatic(runtimeOutDir, docsDir);
|
|
263
359
|
exportMarkdownRoutes(runtimeOutDir);
|
|
360
|
+
addStaticRouteCompatibility(runtimeOutDir);
|
|
264
361
|
|
|
265
362
|
if (!samePath(docsOutDir, runtimeOutDir)) {
|
|
266
363
|
const docsDistDir = join(docsOutDir, "dist");
|
|
@@ -41,6 +41,26 @@ interface PageTreeFolderNode {
|
|
|
41
41
|
children?: unknown[];
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function withTrailingSlashUrl(url: string): string {
|
|
45
|
+
const trimmed = url.trim();
|
|
46
|
+
if (trimmed.length === 0) return trimmed;
|
|
47
|
+
if (/^(https?:|mailto:|tel:|#)/i.test(trimmed)) return trimmed;
|
|
48
|
+
|
|
49
|
+
const hashIndex = trimmed.indexOf('#');
|
|
50
|
+
const queryIndex = trimmed.indexOf('?');
|
|
51
|
+
const endIndex = [hashIndex, queryIndex].filter((index) => index >= 0).sort((a, b) => a - b)[0] ?? trimmed.length;
|
|
52
|
+
const path = trimmed.slice(0, endIndex);
|
|
53
|
+
const suffix = trimmed.slice(endIndex);
|
|
54
|
+
|
|
55
|
+
if (!path.startsWith('/')) return trimmed;
|
|
56
|
+
if (path === '/' || path.endsWith('/')) return `${path}${suffix}`;
|
|
57
|
+
|
|
58
|
+
const lastSegment = path.split('/').filter(Boolean).pop() ?? '';
|
|
59
|
+
if (lastSegment.includes('.')) return trimmed;
|
|
60
|
+
|
|
61
|
+
return `${path}/${suffix}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
44
64
|
function resolveLocale(slugInput: string[] | undefined): string {
|
|
45
65
|
const languages = getLanguages();
|
|
46
66
|
const defaultLanguage = languages[0] ?? 'en';
|
|
@@ -122,6 +142,10 @@ function renderIconsInTree<T>(node: T, iconLibrary: 'fontawesome' | 'lucide' | '
|
|
|
122
142
|
out[key] = <VeluIcon name={value} iconType={iconType} library={iconLibrary} fallback={false} />;
|
|
123
143
|
continue;
|
|
124
144
|
}
|
|
145
|
+
if (key === 'url' && typeof value === 'string') {
|
|
146
|
+
out[key] = withTrailingSlashUrl(value);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
125
149
|
out[key] = renderIconsInTree(value, iconLibrary);
|
|
126
150
|
}
|
|
127
151
|
return out as T;
|
|
@@ -347,12 +371,12 @@ export default async function SlugLayout({ children, params }: SlugLayoutProps)
|
|
|
347
371
|
return {
|
|
348
372
|
type: 'menu',
|
|
349
373
|
text: tabText,
|
|
350
|
-
url: tab.url,
|
|
374
|
+
url: withTrailingSlashUrl(tab.url),
|
|
351
375
|
active: 'nested-url',
|
|
352
376
|
secondary: false,
|
|
353
377
|
items: menuLinks.map((item) => ({
|
|
354
378
|
text: item.text,
|
|
355
|
-
url: item.url,
|
|
379
|
+
url: withTrailingSlashUrl(item.url),
|
|
356
380
|
active: 'nested-url',
|
|
357
381
|
})),
|
|
358
382
|
};
|
|
@@ -360,7 +384,7 @@ export default async function SlugLayout({ children, params }: SlugLayoutProps)
|
|
|
360
384
|
|
|
361
385
|
return {
|
|
362
386
|
text: tabText,
|
|
363
|
-
url: tab.url,
|
|
387
|
+
url: withTrailingSlashUrl(tab.url),
|
|
364
388
|
active: 'nested-url',
|
|
365
389
|
secondary: false,
|
|
366
390
|
};
|
|
@@ -215,6 +215,13 @@ function normalizeDocPath(value: string): string {
|
|
|
215
215
|
return collapsed;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
function withTrailingSlashPath(path: string): string {
|
|
219
|
+
if (!path.startsWith('/')) return path;
|
|
220
|
+
if (path === '/' || path.endsWith('/')) return path;
|
|
221
|
+
if (/\.[a-zA-Z0-9]+$/.test(path)) return path;
|
|
222
|
+
return `${path}/`;
|
|
223
|
+
}
|
|
224
|
+
|
|
218
225
|
function toAbsoluteMetaUrl(origin: string, value: string): string {
|
|
219
226
|
const trimmed = value.trim();
|
|
220
227
|
if (!trimmed) return trimmed;
|
|
@@ -614,7 +621,7 @@ export default async function Page({ params }: PageProps) {
|
|
|
614
621
|
const pageUrl = (typeof sourcePageUrl === 'string' && sourcePageUrl.trim())
|
|
615
622
|
? sourcePageUrl
|
|
616
623
|
: (fallbackPath === '' ? '/' : fallbackPath);
|
|
617
|
-
const rssHref = `${pageUrl.replace(/\/$/, '') || ''}/rss.xml`;
|
|
624
|
+
const rssHref = `${withTrailingSlashPath(pageUrl).replace(/\/$/, '') || ''}/rss.xml`;
|
|
618
625
|
const shouldReplaceTocWithApiExample = !hasExplicitApiRendering && Boolean(inlineApiDoc) && playgroundDisplay === 'interactive';
|
|
619
626
|
const shouldShowOpenApiExampleInToc = !hasExplicitApiRendering && !parsedApiFrontmatter && Boolean(parsedOpenApiFrontmatter);
|
|
620
627
|
const hasApiTocRail = shouldReplaceTocWithApiExample || shouldShowOpenApiExampleInToc;
|
|
@@ -741,7 +748,7 @@ export default async function Page({ params }: PageProps) {
|
|
|
741
748
|
{(previousPage || nextPage) ? (
|
|
742
749
|
<div className={['velu-page-nav-grid', previousPage && nextPage ? 'velu-page-nav-grid-two' : 'velu-page-nav-grid-one'].join(' ')}>
|
|
743
750
|
{previousPage ? (
|
|
744
|
-
<a href={previousPage.url} className="velu-page-nav-card">
|
|
751
|
+
<a href={withTrailingSlashPath(previousPage.url)} className="velu-page-nav-card">
|
|
745
752
|
<p className="velu-page-nav-title">{previousPage.data.title}</p>
|
|
746
753
|
<p className="velu-page-nav-meta">
|
|
747
754
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 18l-6-6 6-6" /></svg>
|
|
@@ -750,7 +757,7 @@ export default async function Page({ params }: PageProps) {
|
|
|
750
757
|
</a>
|
|
751
758
|
) : null}
|
|
752
759
|
{nextPage ? (
|
|
753
|
-
<a href={nextPage.url} className="velu-page-nav-card velu-page-nav-card-next">
|
|
760
|
+
<a href={withTrailingSlashPath(nextPage.url)} className="velu-page-nav-card velu-page-nav-card-next">
|
|
754
761
|
<p className="velu-page-nav-title">{nextPage.data.title}</p>
|
|
755
762
|
<p className="velu-page-nav-meta velu-page-nav-meta-next">
|
|
756
763
|
<span>{nextPage.data.description ?? 'Next'}</span>
|
package/src/engine/app/page.tsx
CHANGED
|
@@ -37,7 +37,10 @@ function resolveDefaultDocsHref(): string {
|
|
|
37
37
|
const defaultLanguage = getLanguages()[0] ?? 'en';
|
|
38
38
|
const tree = source.getPageTree(defaultLanguage);
|
|
39
39
|
const first = findFirstPageUrl(tree);
|
|
40
|
-
|
|
40
|
+
if (!first || first === '/') return '/';
|
|
41
|
+
if (!first.startsWith('/')) return first;
|
|
42
|
+
if (first.endsWith('/')) return first;
|
|
43
|
+
return `${first}/`;
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
export default function HomePage() {
|
|
@@ -30,6 +30,12 @@ function nativeLabel(code: string): string {
|
|
|
30
30
|
return code.toUpperCase();
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function withTrailingSlashPath(path: string): string {
|
|
34
|
+
if (!path.startsWith('/')) return path;
|
|
35
|
+
if (path === '/' || path.endsWith('/')) return path;
|
|
36
|
+
return `${path}/`;
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
export function LanguageSwitcher({ languages, defaultLang }: { languages: string[]; defaultLang: string }) {
|
|
34
40
|
const [open, setOpen] = useState(false);
|
|
35
41
|
const [mounted, setMounted] = useState(false);
|
|
@@ -63,7 +69,7 @@ export function LanguageSwitcher({ languages, defaultLang }: { languages: string
|
|
|
63
69
|
const newPath = code === defaultLang
|
|
64
70
|
? '/' + rest.join('/')
|
|
65
71
|
: '/' + code + '/' + rest.join('/');
|
|
66
|
-
window.location.href = newPath;
|
|
72
|
+
window.location.href = withTrailingSlashPath(newPath);
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
return (
|
|
@@ -5,6 +5,14 @@ import { ThumbsDown, ThumbsUp } from 'lucide-react';
|
|
|
5
5
|
|
|
6
6
|
type Vote = 'yes' | 'no';
|
|
7
7
|
|
|
8
|
+
const YES_OPTIONS = [
|
|
9
|
+
'The guide worked as expected',
|
|
10
|
+
'It was easy to find the information I needed',
|
|
11
|
+
'It was easy to understand the product and features',
|
|
12
|
+
'The documentation is up to date',
|
|
13
|
+
'Something else',
|
|
14
|
+
];
|
|
15
|
+
|
|
8
16
|
const NO_OPTIONS = [
|
|
9
17
|
'Help me get started faster',
|
|
10
18
|
'Make it easier to find what I\'m looking for',
|
|
@@ -19,7 +27,8 @@ export function PageFeedback() {
|
|
|
19
27
|
const [details, setDetails] = useState('');
|
|
20
28
|
const [email, setEmail] = useState('');
|
|
21
29
|
|
|
22
|
-
const showForm = vote
|
|
30
|
+
const showForm = vote !== null;
|
|
31
|
+
const options = vote === 'yes' ? YES_OPTIONS : NO_OPTIONS;
|
|
23
32
|
const showOptionalInputs = selectedReason === 'Something else';
|
|
24
33
|
|
|
25
34
|
const onChooseVote = (value: Vote) => {
|
|
@@ -81,10 +90,12 @@ export function PageFeedback() {
|
|
|
81
90
|
|
|
82
91
|
{showForm ? (
|
|
83
92
|
<div className="velu-page-feedback-panel">
|
|
84
|
-
<h3 className="velu-page-feedback-panel-title">
|
|
93
|
+
<h3 className="velu-page-feedback-panel-title">
|
|
94
|
+
{vote === 'yes' ? 'Great! What worked best for you?' : 'How can we improve our product?'}
|
|
95
|
+
</h3>
|
|
85
96
|
|
|
86
97
|
<div className="velu-page-feedback-options" role="radiogroup" aria-label="Feedback reasons">
|
|
87
|
-
{
|
|
98
|
+
{options.map((option) => {
|
|
88
99
|
const checked = selectedReason === option;
|
|
89
100
|
return (
|
|
90
101
|
<button
|
|
@@ -14,6 +14,12 @@ function ChevronDownIcon() {
|
|
|
14
14
|
);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function withTrailingSlashPath(path: string): string {
|
|
18
|
+
if (!path.startsWith('/')) return path;
|
|
19
|
+
if (path === '/' || path.endsWith('/')) return path;
|
|
20
|
+
return `${path}/`;
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
export function ProductSwitcher({
|
|
18
24
|
products,
|
|
19
25
|
iconLibrary,
|
|
@@ -50,12 +56,12 @@ export function ProductSwitcher({
|
|
|
50
56
|
// Replace the product segment, keep tab/group/page segments
|
|
51
57
|
const rest = segments.slice(1);
|
|
52
58
|
if (rest.length > 0) {
|
|
53
|
-
window.location.href = '/' + [target.slug, ...rest].join('/');
|
|
59
|
+
window.location.href = withTrailingSlashPath('/' + [target.slug, ...rest].join('/'));
|
|
54
60
|
return;
|
|
55
61
|
}
|
|
56
62
|
}
|
|
57
63
|
|
|
58
|
-
window.location.href = target.defaultPath;
|
|
64
|
+
window.location.href = withTrailingSlashPath(target.defaultPath);
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
return (
|
|
@@ -22,6 +22,12 @@ function ChevronDownIcon() {
|
|
|
22
22
|
);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function withTrailingSlashPath(path: string): string {
|
|
26
|
+
if (!path.startsWith('/')) return path;
|
|
27
|
+
if (path === '/' || path.endsWith('/')) return path;
|
|
28
|
+
return `${path}/`;
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
export function VersionSwitcher({ versions }: { versions: VeluVersionOption[] }) {
|
|
26
32
|
const pathname = usePathname();
|
|
27
33
|
const [open, setOpen] = useState(false);
|
|
@@ -55,12 +61,12 @@ export function VersionSwitcher({ versions }: { versions: VeluVersionOption[] })
|
|
|
55
61
|
const targetTab = target.tabSlugs[index] ?? target.tabSlugs[0];
|
|
56
62
|
if (targetTab) {
|
|
57
63
|
const rest = segments.slice(1);
|
|
58
|
-
window.location.href = '/' + [targetTab, ...rest].join('/');
|
|
64
|
+
window.location.href = withTrailingSlashPath('/' + [targetTab, ...rest].join('/'));
|
|
59
65
|
return;
|
|
60
66
|
}
|
|
61
67
|
}
|
|
62
68
|
|
|
63
|
-
window.location.href = target.defaultPath;
|
|
69
|
+
window.location.href = withTrailingSlashPath(target.defaultPath);
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
return (
|
package/src/engine/lib/velu.ts
CHANGED
|
@@ -211,6 +211,12 @@ function pageBasename(page: string): string {
|
|
|
211
211
|
return parts[parts.length - 1] ?? page;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
function withTrailingSlashPath(path: string): string {
|
|
215
|
+
if (!path.startsWith('/')) return path;
|
|
216
|
+
if (path === '/' || path.endsWith('/')) return path;
|
|
217
|
+
return `${path}/`;
|
|
218
|
+
}
|
|
219
|
+
|
|
214
220
|
function findFirstPageInGroup(group: VeluGroup): string | undefined {
|
|
215
221
|
for (const item of group.pages) {
|
|
216
222
|
if (typeof item === 'string') return item;
|
|
@@ -382,7 +388,7 @@ export function getProductOptions(): VeluProductOption[] {
|
|
|
382
388
|
icon: product.icon,
|
|
383
389
|
iconType: product.iconType,
|
|
384
390
|
tabSlugs,
|
|
385
|
-
defaultPath,
|
|
391
|
+
defaultPath: withTrailingSlashPath(defaultPath),
|
|
386
392
|
};
|
|
387
393
|
});
|
|
388
394
|
}
|
|
@@ -417,7 +423,7 @@ export function getVersionOptions(): VeluVersionOption[] {
|
|
|
417
423
|
explicitDefault: version.default === true,
|
|
418
424
|
versionParts: parseVersionParts(version.version),
|
|
419
425
|
tabSlugs,
|
|
420
|
-
defaultPath,
|
|
426
|
+
defaultPath: withTrailingSlashPath(defaultPath),
|
|
421
427
|
order: index,
|
|
422
428
|
};
|
|
423
429
|
});
|
|
@@ -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: {
|