@raystack/chronicle 0.1.0-canary.a638730 → 0.1.0-canary.bd7f9af
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/dist/cli/index.js +204 -530
- package/package.json +13 -9
- package/src/cli/commands/build.ts +30 -63
- package/src/cli/commands/dev.ts +24 -13
- package/src/cli/commands/init.ts +38 -123
- package/src/cli/commands/serve.ts +35 -50
- package/src/cli/commands/start.ts +20 -16
- package/src/cli/index.ts +14 -14
- package/src/cli/utils/config.ts +25 -26
- package/src/cli/utils/index.ts +3 -2
- package/src/cli/utils/resolve.ts +7 -3
- package/src/cli/utils/scaffold.ts +14 -16
- package/src/components/mdx/details.module.css +0 -2
- package/src/components/mdx/image.tsx +5 -20
- package/src/components/mdx/index.tsx +3 -3
- package/src/components/mdx/link.tsx +24 -20
- package/src/components/ui/breadcrumbs.tsx +8 -42
- package/src/components/ui/footer.tsx +2 -3
- package/src/components/ui/search.tsx +116 -71
- package/src/lib/api-routes.ts +6 -8
- package/src/lib/config.ts +31 -29
- package/src/lib/get-llm-text.ts +10 -0
- package/src/lib/head.tsx +26 -22
- package/src/lib/openapi.ts +8 -8
- package/src/lib/page-context.tsx +87 -58
- package/src/lib/source.ts +117 -116
- package/src/pages/ApiLayout.tsx +22 -18
- package/src/pages/ApiPage.tsx +32 -27
- package/src/pages/DocsLayout.tsx +7 -7
- package/src/pages/DocsPage.tsx +11 -11
- package/src/pages/NotFound.tsx +11 -4
- package/src/server/App.tsx +35 -27
- package/src/server/api/apis-proxy.ts +69 -0
- package/src/server/api/health.ts +5 -0
- package/src/server/api/page/[...slug].ts +26 -0
- package/src/server/api/search.ts +170 -0
- package/src/server/api/specs.ts +9 -0
- package/src/server/build-search-index.ts +117 -0
- package/src/server/entry-client.tsx +66 -58
- package/src/server/entry-server.tsx +104 -35
- package/src/server/routes/llms.txt.ts +61 -0
- package/src/server/routes/og.tsx +75 -0
- package/src/server/routes/robots.txt.ts +11 -0
- package/src/server/routes/sitemap.xml.ts +40 -0
- package/src/server/utils/safe-path.ts +17 -0
- package/src/server/vite-config.ts +79 -49
- package/src/themes/default/Layout.tsx +78 -47
- package/src/themes/default/Page.module.css +0 -16
- package/src/themes/default/Page.tsx +9 -11
- package/src/themes/default/Toc.tsx +25 -39
- package/src/themes/default/index.ts +7 -9
- package/src/themes/paper/ChapterNav.tsx +63 -43
- package/src/themes/paper/Layout.module.css +1 -1
- package/src/themes/paper/Layout.tsx +24 -12
- package/src/themes/paper/Page.module.css +16 -4
- package/src/themes/paper/Page.tsx +56 -62
- package/src/themes/paper/ReadingProgress.tsx +160 -139
- package/src/themes/paper/index.ts +5 -5
- package/src/themes/registry.ts +7 -7
- package/src/types/content.ts +5 -21
- package/src/types/globals.d.ts +3 -0
- package/src/types/theme.ts +4 -3
- package/src/cli/__tests__/config.test.ts +0 -25
- package/src/cli/__tests__/scaffold.test.ts +0 -10
- package/src/pages/__tests__/head.test.tsx +0 -57
- package/src/server/__tests__/entry-server.test.tsx +0 -35
- package/src/server/__tests__/handlers.test.ts +0 -77
- package/src/server/__tests__/og.test.ts +0 -23
- package/src/server/__tests__/router.test.ts +0 -72
- package/src/server/__tests__/vite-config.test.ts +0 -25
- package/src/server/adapters/vercel.ts +0 -133
- package/src/server/dev.ts +0 -156
- package/src/server/entry-prod.ts +0 -97
- package/src/server/entry-vercel.ts +0 -26
- package/src/server/handlers/apis-proxy.ts +0 -52
- package/src/server/handlers/health.ts +0 -3
- package/src/server/handlers/llms.ts +0 -58
- package/src/server/handlers/og.ts +0 -87
- package/src/server/handlers/robots.ts +0 -11
- package/src/server/handlers/search.ts +0 -140
- package/src/server/handlers/sitemap.ts +0 -39
- package/src/server/handlers/specs.ts +0 -9
- package/src/server/index.html +0 -12
- package/src/server/prod.ts +0 -18
- package/src/server/request-handler.ts +0 -63
- package/src/server/router.ts +0 -42
- package/src/themes/default/font.ts +0 -4
|
@@ -1,74 +1,82 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { mdxComponents } from '@/components/mdx'
|
|
7
|
-
import
|
|
8
|
-
import type {
|
|
9
|
-
import type {
|
|
10
|
-
import
|
|
1
|
+
import '@vitejs/plugin-react/preamble';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { hydrateRoot } from 'react-dom/client';
|
|
4
|
+
import { BrowserRouter } from 'react-router';
|
|
5
|
+
import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
|
|
6
|
+
import { mdxComponents } from '@/components/mdx';
|
|
7
|
+
import { PageProvider } from '@/lib/page-context';
|
|
8
|
+
import type { ChronicleConfig, Frontmatter, Root } from '@/types';
|
|
9
|
+
import type { ApiSpec } from '@/lib/openapi';
|
|
10
|
+
import type { ReactNode } from 'react';
|
|
11
|
+
import { App } from './App';
|
|
11
12
|
|
|
12
13
|
interface EmbeddedData {
|
|
13
|
-
config: ChronicleConfig
|
|
14
|
-
tree:
|
|
15
|
-
slug: string[]
|
|
16
|
-
frontmatter:
|
|
17
|
-
|
|
14
|
+
config: ChronicleConfig;
|
|
15
|
+
tree: Root;
|
|
16
|
+
slug: string[];
|
|
17
|
+
frontmatter: Frontmatter;
|
|
18
|
+
relativePath: string;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
async function hydrate() {
|
|
21
22
|
try {
|
|
22
|
-
const embedded
|
|
23
|
+
const embedded = (
|
|
24
|
+
window as unknown as { __PAGE_DATA__?: EmbeddedData }
|
|
25
|
+
).__PAGE_DATA__;
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
const config: ChronicleConfig = embedded?.config ?? {
|
|
28
|
+
title: 'Documentation'
|
|
29
|
+
};
|
|
30
|
+
const tree: Root = embedded?.tree ?? { name: 'root', children: [] };
|
|
31
|
+
const isApiPage =
|
|
32
|
+
window.location.pathname.startsWith('/apis') && !!config.api?.length;
|
|
33
|
+
const apiSpecs: ApiSpec[] = isApiPage
|
|
34
|
+
? await fetch('/api/specs')
|
|
35
|
+
.then(r => r.json())
|
|
36
|
+
.catch(() => [])
|
|
37
|
+
: [];
|
|
28
38
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// Fetch API specs if on /apis route
|
|
34
|
-
const isApiRoute = window.location.pathname.startsWith('/apis')
|
|
35
|
-
if (isApiRoute && config.api?.length) {
|
|
36
|
-
try {
|
|
37
|
-
const res = await fetch('/api/specs')
|
|
38
|
-
apiSpecs = await res.json()
|
|
39
|
-
} catch { /* will load on demand */ }
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const sourcePage = await getPage(embedded.slug)
|
|
43
|
-
if (sourcePage) {
|
|
44
|
-
const component = await loadPageComponent(sourcePage)
|
|
45
|
-
page = {
|
|
46
|
-
slug: embedded.slug,
|
|
47
|
-
frontmatter: embedded.frontmatter,
|
|
48
|
-
content: component ? React.createElement(component, { components: mdxComponents }) : null,
|
|
49
|
-
}
|
|
50
|
-
} else {
|
|
51
|
-
page = {
|
|
52
|
-
slug: embedded.slug,
|
|
53
|
-
frontmatter: embedded.frontmatter,
|
|
54
|
-
content: null,
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
} else {
|
|
58
|
-
tree = await buildPageTree()
|
|
59
|
-
}
|
|
39
|
+
const page = embedded?.relativePath
|
|
40
|
+
? await loadPage(embedded)
|
|
41
|
+
: null;
|
|
60
42
|
|
|
61
43
|
hydrateRoot(
|
|
62
44
|
document.getElementById('root') as HTMLElement,
|
|
63
45
|
<BrowserRouter>
|
|
64
|
-
<
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
46
|
+
<ReactRouterProvider>
|
|
47
|
+
<PageProvider
|
|
48
|
+
initialConfig={config}
|
|
49
|
+
initialTree={tree}
|
|
50
|
+
initialPage={page}
|
|
51
|
+
initialApiSpecs={apiSpecs}
|
|
52
|
+
>
|
|
53
|
+
<App />
|
|
54
|
+
</PageProvider>
|
|
55
|
+
</ReactRouterProvider>
|
|
56
|
+
</BrowserRouter>
|
|
57
|
+
);
|
|
69
58
|
} catch (err) {
|
|
70
|
-
console.error('Hydration failed:', err)
|
|
59
|
+
console.error('Hydration failed:', err);
|
|
71
60
|
}
|
|
72
61
|
}
|
|
73
62
|
|
|
74
|
-
|
|
63
|
+
const contentModules = import.meta.glob<{ default?: React.ComponentType<any> }>(
|
|
64
|
+
'../../.content/**/*.{mdx,md}'
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
async function loadPage(
|
|
68
|
+
embedded: EmbeddedData
|
|
69
|
+
): Promise<{ slug: string[]; frontmatter: Frontmatter; content: ReactNode }> {
|
|
70
|
+
const withoutExt = embedded.relativePath.replace(/\.(mdx|md)$/, '');
|
|
71
|
+
const key = embedded.relativePath.endsWith('.md')
|
|
72
|
+
? `../../.content/${withoutExt}.md`
|
|
73
|
+
: `../../.content/${withoutExt}.mdx`;
|
|
74
|
+
const loader = contentModules[key];
|
|
75
|
+
const mod = loader ? await loader() : null;
|
|
76
|
+
const content = mod?.default
|
|
77
|
+
? React.createElement(mod.default, { components: mdxComponents })
|
|
78
|
+
: null;
|
|
79
|
+
return { slug: embedded.slug, frontmatter: embedded.frontmatter, content };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
hydrate();
|
|
@@ -1,35 +1,104 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
1
|
+
import '@raystack/apsara/normalize.css';
|
|
2
|
+
import '@raystack/apsara/style.css';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { renderToReadableStream } from 'react-dom/server.edge';
|
|
5
|
+
import { StaticRouter } from 'react-router';
|
|
6
|
+
import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
|
|
7
|
+
import { mdxComponents } from '@/components/mdx';
|
|
8
|
+
import { loadConfig } from '@/lib/config';
|
|
9
|
+
import { loadApiSpecs } from '@/lib/openapi';
|
|
10
|
+
import { PageProvider } from '@/lib/page-context';
|
|
11
|
+
import { getPageTree, getPage, loadPageComponent } from '@/lib/source';
|
|
12
|
+
import { App } from './App';
|
|
13
|
+
|
|
14
|
+
// @ts-expect-error virtual import from Nitro
|
|
15
|
+
import clientAssets from './entry-client?assets=client';
|
|
16
|
+
// @ts-expect-error virtual import from Nitro
|
|
17
|
+
import serverAssets from './entry-server?assets=ssr';
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
async fetch(req: Request) {
|
|
21
|
+
const url = new URL(req.url);
|
|
22
|
+
const pathname = url.pathname;
|
|
23
|
+
const slug = pathname === '/' ? [] : pathname.slice(1).split('/').filter(Boolean);
|
|
24
|
+
|
|
25
|
+
const config = loadConfig();
|
|
26
|
+
const apiSpecs = config.api?.length
|
|
27
|
+
? await loadApiSpecs(config.api).catch(() => [])
|
|
28
|
+
: [];
|
|
29
|
+
|
|
30
|
+
const [tree, page] = await Promise.all([
|
|
31
|
+
getPageTree(),
|
|
32
|
+
getPage(slug),
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const data = page?.data as Record<string, unknown> | undefined;
|
|
36
|
+
const relativePath = (data?._relativePath as string) ?? null;
|
|
37
|
+
|
|
38
|
+
const pageData = page
|
|
39
|
+
? {
|
|
40
|
+
slug,
|
|
41
|
+
frontmatter: {
|
|
42
|
+
title: (data?.title as string) ?? slug[slug.length - 1] ?? 'Untitled',
|
|
43
|
+
description: data?.description as string | undefined,
|
|
44
|
+
order: data?.order as number | undefined,
|
|
45
|
+
icon: data?.icon as string | undefined,
|
|
46
|
+
lastModified: data?.lastModified as string | undefined,
|
|
47
|
+
},
|
|
48
|
+
content: relativePath
|
|
49
|
+
? await loadPageComponent(relativePath).then(component =>
|
|
50
|
+
component ? React.createElement(component, { components: mdxComponents }) : null
|
|
51
|
+
)
|
|
52
|
+
: null,
|
|
53
|
+
}
|
|
54
|
+
: null;
|
|
55
|
+
|
|
56
|
+
const embeddedData = {
|
|
57
|
+
config,
|
|
58
|
+
tree,
|
|
59
|
+
slug,
|
|
60
|
+
frontmatter: pageData?.frontmatter ?? null,
|
|
61
|
+
relativePath,
|
|
62
|
+
};
|
|
63
|
+
const safeJson = JSON.stringify(embeddedData).replace(/</g, '\\u003c');
|
|
64
|
+
|
|
65
|
+
const assets = clientAssets.merge(serverAssets);
|
|
66
|
+
|
|
67
|
+
const stream = await renderToReadableStream(
|
|
68
|
+
<html lang="en">
|
|
69
|
+
<head>
|
|
70
|
+
<meta charSet="UTF-8" />
|
|
71
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
72
|
+
{assets.css.map((attr: { href: string }) => (
|
|
73
|
+
<link key={attr.href} rel="stylesheet" {...attr} />
|
|
74
|
+
))}
|
|
75
|
+
{assets.js.map((attr: { href: string }) => (
|
|
76
|
+
<link key={attr.href} rel="modulepreload" {...attr} />
|
|
77
|
+
))}
|
|
78
|
+
<script type="module" src={assets.entry} />
|
|
79
|
+
<script dangerouslySetInnerHTML={{ __html: `window.__PAGE_DATA__ = ${safeJson}` }} />
|
|
80
|
+
</head>
|
|
81
|
+
<body>
|
|
82
|
+
<div id="root">
|
|
83
|
+
<StaticRouter location={pathname}>
|
|
84
|
+
<ReactRouterProvider>
|
|
85
|
+
<PageProvider
|
|
86
|
+
initialConfig={config}
|
|
87
|
+
initialTree={tree}
|
|
88
|
+
initialPage={pageData}
|
|
89
|
+
initialApiSpecs={apiSpecs}
|
|
90
|
+
>
|
|
91
|
+
<App />
|
|
92
|
+
</PageProvider>
|
|
93
|
+
</ReactRouterProvider>
|
|
94
|
+
</StaticRouter>
|
|
95
|
+
</div>
|
|
96
|
+
</body>
|
|
97
|
+
</html>,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
return new Response(stream, {
|
|
101
|
+
headers: { 'Content-Type': 'text/html;charset=utf-8' },
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
import { defineHandler, HTTPError } from 'nitro';
|
|
5
|
+
import { loadConfig } from '@/lib/config';
|
|
6
|
+
|
|
7
|
+
function getContentDir(): string {
|
|
8
|
+
return __CHRONICLE_CONTENT_DIR__ || path.join(process.cwd(), 'content');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function scanPages(): Promise<{ title: string; url: string }[]> {
|
|
12
|
+
const contentDir = getContentDir();
|
|
13
|
+
const pages: { title: string; url: string }[] = [];
|
|
14
|
+
|
|
15
|
+
async function scan(dir: string, prefix: string[] = []) {
|
|
16
|
+
try {
|
|
17
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
20
|
+
continue;
|
|
21
|
+
const fullPath = path.join(dir, entry.name);
|
|
22
|
+
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
await scan(fullPath, [...prefix, entry.name]);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!entry.name.endsWith('.mdx') && !entry.name.endsWith('.md'))
|
|
29
|
+
continue;
|
|
30
|
+
|
|
31
|
+
const raw = await fs.readFile(fullPath, 'utf-8');
|
|
32
|
+
const { data: fm } = matter(raw);
|
|
33
|
+
const baseName = entry.name.replace(/\.(mdx|md)$/, '');
|
|
34
|
+
const slugs = baseName === 'index' ? prefix : [...prefix, baseName];
|
|
35
|
+
const url = slugs.length === 0 ? '/' : `/${slugs.join('/')}`;
|
|
36
|
+
|
|
37
|
+
pages.push({ title: fm.title ?? baseName, url });
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
/* directory not readable */
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await scan(contentDir);
|
|
45
|
+
return pages;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default defineHandler(async event => {
|
|
49
|
+
const config = loadConfig();
|
|
50
|
+
|
|
51
|
+
if (!config.llms?.enabled) {
|
|
52
|
+
throw new HTTPError({ status: 404, message: 'Not Found' });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const pages = await scanPages();
|
|
56
|
+
const index = pages.map(p => `- [${p.title}](${p.url})`).join('\n');
|
|
57
|
+
const body = `# ${config.title}\n\n${config.description ?? ''}\n\n${index}`;
|
|
58
|
+
|
|
59
|
+
event.res.headers.set('Content-Type', 'text/plain');
|
|
60
|
+
return body;
|
|
61
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { defineHandler } from 'nitro';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import satori from 'satori';
|
|
4
|
+
import { loadConfig } from '@/lib/config';
|
|
5
|
+
|
|
6
|
+
let fontData: ArrayBuffer | null = null;
|
|
7
|
+
|
|
8
|
+
async function loadFont(): Promise<ArrayBuffer> {
|
|
9
|
+
if (fontData) return fontData;
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetch(
|
|
13
|
+
'https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hiA.woff2'
|
|
14
|
+
);
|
|
15
|
+
fontData = await response.arrayBuffer();
|
|
16
|
+
} catch {
|
|
17
|
+
fontData = new ArrayBuffer(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return fontData;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default defineHandler(async event => {
|
|
24
|
+
const config = loadConfig();
|
|
25
|
+
const title = event.url.searchParams.get('title') ?? config.title;
|
|
26
|
+
const description = event.url.searchParams.get('description') ?? '';
|
|
27
|
+
const siteName = config.title;
|
|
28
|
+
|
|
29
|
+
const font = await loadFont();
|
|
30
|
+
|
|
31
|
+
const svg = await satori(
|
|
32
|
+
<div
|
|
33
|
+
style={{
|
|
34
|
+
height: '100%',
|
|
35
|
+
width: '100%',
|
|
36
|
+
display: 'flex',
|
|
37
|
+
flexDirection: 'column',
|
|
38
|
+
justifyContent: 'center',
|
|
39
|
+
padding: '60px 80px',
|
|
40
|
+
backgroundColor: '#0a0a0a',
|
|
41
|
+
color: '#fafafa',
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<div style={{ fontSize: 24, color: '#888', marginBottom: 16 }}>
|
|
45
|
+
{siteName}
|
|
46
|
+
</div>
|
|
47
|
+
<div
|
|
48
|
+
style={{
|
|
49
|
+
fontSize: 56,
|
|
50
|
+
fontWeight: 700,
|
|
51
|
+
lineHeight: 1.2,
|
|
52
|
+
marginBottom: 24,
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
{title}
|
|
56
|
+
</div>
|
|
57
|
+
{description && (
|
|
58
|
+
<div style={{ fontSize: 24, color: '#999', lineHeight: 1.4 }}>
|
|
59
|
+
{description}
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
</div>,
|
|
63
|
+
{
|
|
64
|
+
width: 1200,
|
|
65
|
+
height: 630,
|
|
66
|
+
fonts: [
|
|
67
|
+
{ name: 'Inter', data: font, weight: 400, style: 'normal' as const },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
event.res.headers.set('Content-Type', 'image/svg+xml');
|
|
73
|
+
event.res.headers.set('Cache-Control', 'public, max-age=86400');
|
|
74
|
+
return svg;
|
|
75
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineHandler } from 'nitro';
|
|
2
|
+
import { loadConfig } from '@/lib/config';
|
|
3
|
+
|
|
4
|
+
export default defineHandler(event => {
|
|
5
|
+
const config = loadConfig();
|
|
6
|
+
const sitemap = config.url ? `\nSitemap: ${config.url}/sitemap.xml` : '';
|
|
7
|
+
const body = `User-agent: *\nAllow: /${sitemap}`;
|
|
8
|
+
|
|
9
|
+
event.res.headers.set('Content-Type', 'text/plain');
|
|
10
|
+
return body;
|
|
11
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineHandler } from 'nitro';
|
|
2
|
+
import { buildApiRoutes } from '@/lib/api-routes';
|
|
3
|
+
import { loadConfig } from '@/lib/config';
|
|
4
|
+
import { loadApiSpecs } from '@/lib/openapi';
|
|
5
|
+
import { getPages } from '@/lib/source';
|
|
6
|
+
|
|
7
|
+
export default defineHandler(async event => {
|
|
8
|
+
const config = loadConfig();
|
|
9
|
+
|
|
10
|
+
if (!config.url) {
|
|
11
|
+
event.res.headers.set('Content-Type', 'application/xml');
|
|
12
|
+
return '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"/>';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const baseUrl = config.url.replace(/\/$/, '');
|
|
16
|
+
|
|
17
|
+
const pages = await getPages();
|
|
18
|
+
const docPages = pages.map(page => {
|
|
19
|
+
const data = page.data as Record<string, unknown>;
|
|
20
|
+
const lastmod = data.lastModified
|
|
21
|
+
? `<lastmod>${new Date(data.lastModified as string).toISOString()}</lastmod>`
|
|
22
|
+
: '';
|
|
23
|
+
return `<url><loc>${baseUrl}/${page.slugs.join('/')}</loc>${lastmod}</url>`;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const apiPages = config.api?.length
|
|
27
|
+
? buildApiRoutes(await loadApiSpecs(config.api)).map(
|
|
28
|
+
route => `<url><loc>${baseUrl}/apis/${route.slug.join('/')}</loc></url>`
|
|
29
|
+
)
|
|
30
|
+
: [];
|
|
31
|
+
|
|
32
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
33
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
34
|
+
<url><loc>${baseUrl}</loc></url>
|
|
35
|
+
${[...docPages, ...apiPages].join('\n')}
|
|
36
|
+
</urlset>`;
|
|
37
|
+
|
|
38
|
+
event.res.headers.set('Content-Type', 'application/xml');
|
|
39
|
+
return xml;
|
|
40
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a URL path within a base directory, preventing path traversal.
|
|
5
|
+
* Returns null if the resolved path escapes the base directory.
|
|
6
|
+
*/
|
|
7
|
+
export function safePath(baseDir: string, urlPath: string): string | null {
|
|
8
|
+
const decoded = decodeURIComponent(urlPath.split('?')[0]);
|
|
9
|
+
const resolved = path.resolve(baseDir, '.' + decoded);
|
|
10
|
+
if (
|
|
11
|
+
!resolved.startsWith(path.resolve(baseDir) + path.sep) &&
|
|
12
|
+
resolved !== path.resolve(baseDir)
|
|
13
|
+
) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return resolved;
|
|
17
|
+
}
|
|
@@ -1,71 +1,101 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import mdx from '
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
1
|
+
import react from '@vitejs/plugin-react';
|
|
2
|
+
import { remarkDirectiveAdmonition, remarkMdxMermaid } from 'fumadocs-core/mdx-plugins';
|
|
3
|
+
import { defineConfig as defineFumadocsConfig } from 'fumadocs-mdx/config';
|
|
4
|
+
import mdx from 'fumadocs-mdx/vite';
|
|
5
|
+
import { nitro } from 'nitro/vite';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import remarkDirective from 'remark-directive';
|
|
8
|
+
import { type InlineConfig } from 'vite';
|
|
9
|
+
import remarkUnusedDirectives from '../lib/remark-unused-directives';
|
|
10
10
|
|
|
11
11
|
export interface ViteConfigOptions {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
packageRoot: string;
|
|
13
|
+
projectRoot: string;
|
|
14
|
+
contentDir: string;
|
|
15
|
+
preset?: string;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
export async function createViteConfig(
|
|
18
|
-
|
|
18
|
+
export async function createViteConfig(
|
|
19
|
+
options: ViteConfigOptions
|
|
20
|
+
): Promise<InlineConfig> {
|
|
21
|
+
const { packageRoot, projectRoot, contentDir, preset } = options;
|
|
19
22
|
|
|
20
23
|
return {
|
|
21
|
-
root,
|
|
24
|
+
root: packageRoot,
|
|
22
25
|
configFile: false,
|
|
26
|
+
plugins: [
|
|
27
|
+
nitro({
|
|
28
|
+
serverDir: path.resolve(packageRoot, 'src/server'),
|
|
29
|
+
...(preset && { preset }),
|
|
30
|
+
}),
|
|
31
|
+
mdx({
|
|
32
|
+
default: defineFumadocsConfig({
|
|
33
|
+
mdxOptions: {
|
|
34
|
+
remarkPlugins: [
|
|
35
|
+
remarkDirective,
|
|
36
|
+
[remarkDirectiveAdmonition, {
|
|
37
|
+
tags: {
|
|
38
|
+
CalloutContainer: 'Callout',
|
|
39
|
+
CalloutTitle: 'CalloutTitle',
|
|
40
|
+
CalloutDescription: 'CalloutDescription',
|
|
41
|
+
},
|
|
42
|
+
types: {
|
|
43
|
+
note: 'accent',
|
|
44
|
+
tip: 'accent',
|
|
45
|
+
info: 'accent',
|
|
46
|
+
warn: 'attention',
|
|
47
|
+
warning: 'attention',
|
|
48
|
+
danger: 'alert',
|
|
49
|
+
caution: 'alert',
|
|
50
|
+
success: 'success',
|
|
51
|
+
},
|
|
52
|
+
}],
|
|
53
|
+
remarkUnusedDirectives,
|
|
54
|
+
remarkMdxMermaid,
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
}, { index: false }),
|
|
59
|
+
react()
|
|
60
|
+
],
|
|
23
61
|
resolve: {
|
|
24
62
|
alias: {
|
|
25
|
-
'@': path.resolve(
|
|
26
|
-
'@content': contentDir,
|
|
63
|
+
'@': path.resolve(packageRoot, 'src'),
|
|
27
64
|
},
|
|
28
|
-
|
|
65
|
+
conditions: ['module-sync', 'import', 'node'],
|
|
66
|
+
dedupe: [
|
|
67
|
+
'react',
|
|
68
|
+
'react-dom',
|
|
69
|
+
'react/jsx-runtime',
|
|
70
|
+
'react/jsx-dev-runtime',
|
|
71
|
+
'react-router',
|
|
72
|
+
]
|
|
29
73
|
},
|
|
30
74
|
server: {
|
|
31
75
|
fs: {
|
|
32
|
-
allow: [
|
|
33
|
-
}
|
|
76
|
+
allow: [packageRoot, projectRoot, contentDir]
|
|
77
|
+
}
|
|
34
78
|
},
|
|
35
|
-
plugins: [
|
|
36
|
-
mdx({
|
|
37
|
-
remarkPlugins: [
|
|
38
|
-
remarkFrontmatter,
|
|
39
|
-
remarkMdxFrontmatter,
|
|
40
|
-
remarkGfm,
|
|
41
|
-
remarkDirective,
|
|
42
|
-
],
|
|
43
|
-
rehypePlugins: [
|
|
44
|
-
[rehypeShiki, { themes: { light: 'github-light', dark: 'github-dark' }, defaultColor: false }],
|
|
45
|
-
],
|
|
46
|
-
mdExtensions: ['.md'],
|
|
47
|
-
mdxExtensions: ['.mdx'],
|
|
48
|
-
}),
|
|
49
|
-
react(),
|
|
50
|
-
],
|
|
51
79
|
define: {
|
|
52
|
-
|
|
53
|
-
|
|
80
|
+
__CHRONICLE_CONTENT_DIR__: JSON.stringify(contentDir),
|
|
81
|
+
__CHRONICLE_PROJECT_ROOT__: JSON.stringify(projectRoot)
|
|
54
82
|
},
|
|
55
83
|
css: {
|
|
56
84
|
modules: {
|
|
57
|
-
localsConvention: 'camelCase'
|
|
58
|
-
}
|
|
85
|
+
localsConvention: 'camelCase'
|
|
86
|
+
}
|
|
59
87
|
},
|
|
60
88
|
ssr: {
|
|
61
|
-
noExternal: ['@raystack/apsara']
|
|
62
|
-
},
|
|
63
|
-
build: {
|
|
64
|
-
rolldownOptions: {
|
|
65
|
-
input: isDev ? undefined : {
|
|
66
|
-
client: path.resolve(root, 'src/server/index.html'),
|
|
67
|
-
},
|
|
68
|
-
},
|
|
89
|
+
noExternal: ['@raystack/apsara', 'dayjs', 'fumadocs-core']
|
|
69
90
|
},
|
|
70
|
-
|
|
91
|
+
environments: {
|
|
92
|
+
client: {
|
|
93
|
+
build: {
|
|
94
|
+
rollupOptions: {
|
|
95
|
+
input: path.resolve(packageRoot, 'src/server/entry-client.tsx')
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
71
101
|
}
|