@raystack/chronicle 0.5.2 → 0.5.4
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 +98 -0
- package/dist/cli/index.js +7 -1
- package/package.json +7 -2
- package/src/components/mdx/link.tsx +34 -13
- package/src/lib/page-context.tsx +35 -7
- package/src/pages/DocsPage.tsx +5 -2
- package/src/pages/NotFound.module.css +3 -0
- package/src/pages/NotFound.tsx +7 -12
- package/src/server/api/{page/[...slug].ts → page.ts} +2 -2
- package/src/server/entry-server.tsx +6 -0
- package/src/server/plugins/telemetry.ts +61 -0
- package/src/server/routes/[...slug].md.ts +45 -0
- package/src/server/routes/llms.txt.ts +2 -1
- package/src/types/config.ts +8 -0
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Chronicle
|
|
2
|
+
|
|
3
|
+
Config-driven documentation framework built on Vite, Nitro, and Apsara UI.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Config-driven** — Single `chronicle.yaml` for all site configuration
|
|
8
|
+
- **Themeable** — Built-in themes: `default` (sidebar + TOC) and `paper` (book-style)
|
|
9
|
+
- **MDX** — Write docs in MDX with callouts, tabs, mermaid diagrams, and syntax highlighting
|
|
10
|
+
- **API docs** — Interactive OpenAPI documentation with "Try it out" panel
|
|
11
|
+
- **LLMs** — Auto-generate `/llms.txt` and `/llms-full.txt` for AI consumption
|
|
12
|
+
- **CLI** — `init`, `dev`, `build`, `start`, `serve` commands
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g @raystack/chronicle
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Initialize
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
chronicle init
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Creates a `chronicle.yaml` and sample `index.mdx`.
|
|
29
|
+
|
|
30
|
+
### Develop
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
chronicle dev
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Open [http://localhost:3000](http://localhost:3000).
|
|
37
|
+
|
|
38
|
+
### Build for production
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
chronicle build
|
|
42
|
+
chronicle start
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Contributing
|
|
46
|
+
|
|
47
|
+
We welcome contributions! Here's how to get started:
|
|
48
|
+
|
|
49
|
+
### Prerequisites
|
|
50
|
+
|
|
51
|
+
- [Node.js](https://nodejs.org/) >= 22
|
|
52
|
+
- [Bun](https://bun.sh/) >= 1.3
|
|
53
|
+
|
|
54
|
+
### Running Locally
|
|
55
|
+
|
|
56
|
+
1. Fork and clone the repository
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git clone https://github.com/<your-username>/chronicle.git
|
|
60
|
+
cd chronicle
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
2. Install dependencies
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
bun install
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
3. Build the CLI
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
bun run build:cli
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
4. Run the docs site locally
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
bun run dev:docs
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Open [http://localhost:3000](http://localhost:3000) to see the docs site.
|
|
82
|
+
|
|
83
|
+
You can also run the CLI directly:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
./packages/chronicle/bin/chronicle.js dev --config docs/chronicle.yaml
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Making Changes
|
|
90
|
+
|
|
91
|
+
1. Create a branch from `main`
|
|
92
|
+
2. Make your changes
|
|
93
|
+
3. Test locally from the `docs/` directory
|
|
94
|
+
4. Open a pull request
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
[Apache-2.0](LICENSE)
|
package/dist/cli/index.js
CHANGED
|
@@ -254,6 +254,11 @@ var analyticsSchema = z.object({
|
|
|
254
254
|
enabled: z.boolean().optional(),
|
|
255
255
|
googleAnalytics: googleAnalyticsSchema.optional()
|
|
256
256
|
});
|
|
257
|
+
var telemetrySchema = z.object({
|
|
258
|
+
enabled: z.boolean().optional(),
|
|
259
|
+
serviceName: z.string().optional(),
|
|
260
|
+
port: z.number().int().min(1).max(65535).default(9090)
|
|
261
|
+
});
|
|
257
262
|
var chronicleConfigSchema = z.object({
|
|
258
263
|
title: z.string(),
|
|
259
264
|
description: z.string().optional(),
|
|
@@ -267,7 +272,8 @@ var chronicleConfigSchema = z.object({
|
|
|
267
272
|
footer: footerSchema.optional(),
|
|
268
273
|
api: z.array(apiSchema).optional(),
|
|
269
274
|
llms: llmsSchema.optional(),
|
|
270
|
-
analytics: analyticsSchema.optional()
|
|
275
|
+
analytics: analyticsSchema.optional(),
|
|
276
|
+
telemetry: telemetrySchema.optional()
|
|
271
277
|
});
|
|
272
278
|
// src/cli/utils/config.ts
|
|
273
279
|
function resolveConfigPath(configPath) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raystack/chronicle",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Config-driven documentation framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -36,6 +36,11 @@
|
|
|
36
36
|
"@codemirror/theme-one-dark": "^6.1.3",
|
|
37
37
|
"@codemirror/view": "^6.39.14",
|
|
38
38
|
"@heroicons/react": "^2.2.0",
|
|
39
|
+
"@opentelemetry/api": "^1.9.1",
|
|
40
|
+
"@opentelemetry/exporter-prometheus": "^0.214.0",
|
|
41
|
+
"@opentelemetry/resources": "^2.6.1",
|
|
42
|
+
"@opentelemetry/sdk-metrics": "^2.6.1",
|
|
43
|
+
"@opentelemetry/semantic-conventions": "^1.40.0",
|
|
39
44
|
"@raystack/apsara": "0.55.1",
|
|
40
45
|
"@shikijs/rehype": "^4.0.2",
|
|
41
46
|
"@vitejs/plugin-react": "^6.0.1",
|
|
@@ -44,7 +49,7 @@
|
|
|
44
49
|
"codemirror": "^6.0.2",
|
|
45
50
|
"commander": "^14.0.2",
|
|
46
51
|
"fumadocs-core": "16.6.15",
|
|
47
|
-
"fumadocs-mdx": "
|
|
52
|
+
"fumadocs-mdx": "14.2.6",
|
|
48
53
|
"glob": "^11.0.0",
|
|
49
54
|
"gray-matter": "^4.0.3",
|
|
50
55
|
"h3": "^2.0.1-rc.16",
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Link as ApsaraLink } from '@raystack/apsara';
|
|
2
|
-
import type { ComponentProps } from 'react';
|
|
3
|
-
import {
|
|
2
|
+
import type { ComponentProps, MouseEvent } from 'react';
|
|
3
|
+
import { useNavigate } from 'react-router';
|
|
4
4
|
|
|
5
5
|
type LinkProps = ComponentProps<'a'>;
|
|
6
6
|
|
|
7
|
-
export function Link({ href, children, ...props }: LinkProps) {
|
|
7
|
+
export function Link({ href, children, onClick: onClickProp, ...props }: LinkProps) {
|
|
8
|
+
const navigate = useNavigate();
|
|
9
|
+
|
|
8
10
|
if (!href) {
|
|
9
11
|
return <span {...props}>{children}</span>;
|
|
10
12
|
}
|
|
@@ -12,14 +14,6 @@ export function Link({ href, children, ...props }: LinkProps) {
|
|
|
12
14
|
const isExternal = href.startsWith('http://') || href.startsWith('https://');
|
|
13
15
|
const isAnchor = href.startsWith('#');
|
|
14
16
|
|
|
15
|
-
if (isAnchor) {
|
|
16
|
-
return (
|
|
17
|
-
<ApsaraLink href={href} {...props}>
|
|
18
|
-
{children}
|
|
19
|
-
</ApsaraLink>
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
17
|
if (isExternal) {
|
|
24
18
|
return (
|
|
25
19
|
<ApsaraLink
|
|
@@ -33,9 +27,36 @@ export function Link({ href, children, ...props }: LinkProps) {
|
|
|
33
27
|
);
|
|
34
28
|
}
|
|
35
29
|
|
|
30
|
+
if (isAnchor) {
|
|
31
|
+
return (
|
|
32
|
+
<ApsaraLink href={href} {...props}>
|
|
33
|
+
{children}
|
|
34
|
+
</ApsaraLink>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const onClick = (e: MouseEvent<HTMLAnchorElement>) => {
|
|
39
|
+
if (
|
|
40
|
+
e.defaultPrevented ||
|
|
41
|
+
e.button !== 0 ||
|
|
42
|
+
e.metaKey ||
|
|
43
|
+
e.ctrlKey ||
|
|
44
|
+
e.shiftKey ||
|
|
45
|
+
e.altKey
|
|
46
|
+
) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
onClickProp?.(e);
|
|
51
|
+
if (e.defaultPrevented) return;
|
|
52
|
+
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
navigate(href);
|
|
55
|
+
};
|
|
56
|
+
|
|
36
57
|
return (
|
|
37
|
-
<
|
|
58
|
+
<ApsaraLink href={href} {...props} onClick={onClick}>
|
|
38
59
|
{children}
|
|
39
|
-
</
|
|
60
|
+
</ApsaraLink>
|
|
40
61
|
);
|
|
41
62
|
}
|
package/src/lib/page-context.tsx
CHANGED
|
@@ -22,6 +22,7 @@ interface PageContextValue {
|
|
|
22
22
|
config: ChronicleConfig;
|
|
23
23
|
tree: Root;
|
|
24
24
|
page: PageData | null;
|
|
25
|
+
errorStatus: number | null;
|
|
25
26
|
apiSpecs: ApiSpec[];
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -35,6 +36,7 @@ export function usePageContext(): PageContextValue {
|
|
|
35
36
|
config: { title: 'Documentation' },
|
|
36
37
|
tree: { name: 'root', children: [] } as Root,
|
|
37
38
|
page: null,
|
|
39
|
+
errorStatus: null,
|
|
38
40
|
apiSpecs: []
|
|
39
41
|
};
|
|
40
42
|
}
|
|
@@ -50,6 +52,16 @@ interface PageProviderProps {
|
|
|
50
52
|
children: ReactNode;
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
function isApisRoute(pathname: string): boolean {
|
|
56
|
+
return pathname === '/apis' || pathname.startsWith('/apis/');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getInitialErrorStatus(page: PageData | null, pathname: string): number | null {
|
|
60
|
+
if (page) return null;
|
|
61
|
+
if (pathname === '/' || isApisRoute(pathname)) return null;
|
|
62
|
+
return 404;
|
|
63
|
+
}
|
|
64
|
+
|
|
53
65
|
export function PageProvider({
|
|
54
66
|
initialConfig,
|
|
55
67
|
initialTree,
|
|
@@ -61,6 +73,7 @@ export function PageProvider({
|
|
|
61
73
|
const { pathname } = useLocation();
|
|
62
74
|
const [tree] = useState<Root>(initialTree);
|
|
63
75
|
const [page, setPage] = useState<PageData | null>(initialPage);
|
|
76
|
+
const [errorStatus, setErrorStatus] = useState<number | null>(getInitialErrorStatus(initialPage, pathname));
|
|
64
77
|
const [apiSpecs, setApiSpecs] = useState<ApiSpec[]>(initialApiSpecs);
|
|
65
78
|
const [currentPath, setCurrentPath] = useState(pathname);
|
|
66
79
|
|
|
@@ -70,7 +83,7 @@ export function PageProvider({
|
|
|
70
83
|
|
|
71
84
|
const cancelled = { current: false };
|
|
72
85
|
|
|
73
|
-
if (pathname
|
|
86
|
+
if (isApisRoute(pathname)) {
|
|
74
87
|
if (apiSpecs.length === 0) {
|
|
75
88
|
fetch('/api/specs')
|
|
76
89
|
.then(res => res.json())
|
|
@@ -86,24 +99,39 @@ export function PageProvider({
|
|
|
86
99
|
? []
|
|
87
100
|
: pathname.slice(1).split('/').filter(Boolean);
|
|
88
101
|
|
|
89
|
-
const apiPath = slug.length === 0 ? '/api/page
|
|
102
|
+
const apiPath = slug.length === 0 ? '/api/page' : `/api/page?slug=${slug.join(',')}`;
|
|
90
103
|
|
|
91
104
|
fetch(apiPath)
|
|
92
|
-
.then(res =>
|
|
93
|
-
|
|
94
|
-
|
|
105
|
+
.then(res => {
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
if (!cancelled.current) {
|
|
108
|
+
setPage(null);
|
|
109
|
+
setErrorStatus(res.status);
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
return res.json();
|
|
114
|
+
})
|
|
115
|
+
.then(async (data: { frontmatter: Frontmatter; relativePath: string; originalPath?: string } | undefined) => {
|
|
116
|
+
if (cancelled.current || !data) return;
|
|
95
117
|
const { content, toc } = await loadMdx(data.originalPath || data.relativePath);
|
|
96
118
|
if (cancelled.current) return;
|
|
119
|
+
setErrorStatus(null);
|
|
97
120
|
setPage({ slug, frontmatter: data.frontmatter, content, toc });
|
|
98
121
|
})
|
|
99
|
-
.catch(() => {
|
|
122
|
+
.catch(() => {
|
|
123
|
+
if (!cancelled.current) {
|
|
124
|
+
setPage(null);
|
|
125
|
+
setErrorStatus(500);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
100
128
|
|
|
101
129
|
return () => { cancelled.current = true; };
|
|
102
130
|
}, [pathname]);
|
|
103
131
|
|
|
104
132
|
return (
|
|
105
133
|
<PageContext.Provider
|
|
106
|
-
value={{ config: initialConfig, tree, page, apiSpecs }}
|
|
134
|
+
value={{ config: initialConfig, tree, page, errorStatus, apiSpecs }}
|
|
107
135
|
>
|
|
108
136
|
{children}
|
|
109
137
|
</PageContext.Provider>
|
package/src/pages/DocsPage.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Head } from '@/lib/head';
|
|
2
2
|
import { usePageContext } from '@/lib/page-context';
|
|
3
|
+
import { NotFound } from '@/pages/NotFound';
|
|
3
4
|
import { getTheme } from '@/themes/registry';
|
|
4
5
|
|
|
5
6
|
interface DocsPageProps {
|
|
@@ -7,8 +8,10 @@ interface DocsPageProps {
|
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export function DocsPage({ slug }: DocsPageProps) {
|
|
10
|
-
const { config, tree, page } = usePageContext();
|
|
11
|
+
const { config, tree, page, errorStatus } = usePageContext();
|
|
11
12
|
|
|
13
|
+
if (errorStatus === 404) return <NotFound />;
|
|
14
|
+
if (errorStatus) return <NotFound />;
|
|
12
15
|
if (!page) return null;
|
|
13
16
|
|
|
14
17
|
const { Page } = getTheme(config.theme?.name);
|
|
@@ -30,7 +33,7 @@ export function DocsPage({ slug }: DocsPageProps) {
|
|
|
30
33
|
/>
|
|
31
34
|
<Page
|
|
32
35
|
page={{
|
|
33
|
-
slug,
|
|
36
|
+
slug: page.slug,
|
|
34
37
|
frontmatter: page.frontmatter,
|
|
35
38
|
content: page.content,
|
|
36
39
|
toc: page.toc
|
package/src/pages/NotFound.tsx
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { EmptyState } from '@raystack/apsara';
|
|
2
|
+
import styles from './NotFound.module.css';
|
|
2
3
|
|
|
3
4
|
export function NotFound() {
|
|
4
5
|
return (
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
>
|
|
11
|
-
<Headline size='large' as='h1'>
|
|
12
|
-
404
|
|
13
|
-
</Headline>
|
|
14
|
-
<Text size={3}>Page not found</Text>
|
|
15
|
-
</Flex>
|
|
6
|
+
<EmptyState
|
|
7
|
+
heading="404"
|
|
8
|
+
subHeading="Page not found"
|
|
9
|
+
classNames={{ container: styles.emptyState }}
|
|
10
|
+
/>
|
|
16
11
|
);
|
|
17
12
|
}
|
|
@@ -2,8 +2,8 @@ import { defineHandler, HTTPError } from 'nitro';
|
|
|
2
2
|
import { getPage, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source';
|
|
3
3
|
|
|
4
4
|
export default defineHandler(async event => {
|
|
5
|
-
const slugParam = event.
|
|
6
|
-
const slug = slugParam ? slugParam.split('
|
|
5
|
+
const slugParam = event.url.searchParams.get('slug') ?? '';
|
|
6
|
+
const slug = slugParam ? slugParam.split(',').filter(Boolean) : [];
|
|
7
7
|
const page = await getPage(slug);
|
|
8
8
|
|
|
9
9
|
if (!page) {
|
|
@@ -9,6 +9,7 @@ import { loadConfig } from '@/lib/config';
|
|
|
9
9
|
import { loadApiSpecs } from '@/lib/openapi';
|
|
10
10
|
import { PageProvider } from '@/lib/page-context';
|
|
11
11
|
import { getPageTree, getPage, loadPageModule, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source';
|
|
12
|
+
import { useNitroApp } from 'nitro/app';
|
|
12
13
|
import { App } from './App';
|
|
13
14
|
|
|
14
15
|
import clientAssets from './entry-client?assets=client';
|
|
@@ -57,6 +58,7 @@ export default {
|
|
|
57
58
|
|
|
58
59
|
const assets = clientAssets.merge(serverAssets);
|
|
59
60
|
|
|
61
|
+
const renderStart = performance.now();
|
|
60
62
|
const stream = await renderToReadableStream(
|
|
61
63
|
<html lang="en">
|
|
62
64
|
<head>
|
|
@@ -91,9 +93,13 @@ export default {
|
|
|
91
93
|
</html>,
|
|
92
94
|
);
|
|
93
95
|
|
|
96
|
+
const renderDuration = performance.now() - renderStart;
|
|
97
|
+
|
|
94
98
|
const isApiRoute = pathname.startsWith('/apis');
|
|
95
99
|
const status = !page && !isApiRoute && slug.length > 0 ? 404 : 200;
|
|
96
100
|
|
|
101
|
+
useNitroApp().hooks.callHook('chronicle:ssr-rendered', pathname, status, renderDuration);
|
|
102
|
+
|
|
97
103
|
return new Response(stream, {
|
|
98
104
|
status,
|
|
99
105
|
headers: { 'Content-Type': 'text/html;charset=utf-8' },
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Counter, Histogram } from '@opentelemetry/api'
|
|
2
|
+
import { MeterProvider } from '@opentelemetry/sdk-metrics'
|
|
3
|
+
import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'
|
|
4
|
+
import { resourceFromAttributes } from '@opentelemetry/resources'
|
|
5
|
+
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'
|
|
6
|
+
import type { H3Event } from 'h3'
|
|
7
|
+
import { definePlugin } from 'nitro'
|
|
8
|
+
import { loadConfig } from '@/lib/config'
|
|
9
|
+
|
|
10
|
+
declare module 'nitro/types' {
|
|
11
|
+
interface NitroRuntimeHooks {
|
|
12
|
+
'chronicle:ssr-rendered': (route: string, status: number, durationMs: number) => void
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default definePlugin((nitroApp) => {
|
|
17
|
+
const config = loadConfig()
|
|
18
|
+
if (!config.telemetry?.enabled) return
|
|
19
|
+
|
|
20
|
+
const resource = resourceFromAttributes({
|
|
21
|
+
[ATTR_SERVICE_NAME]: config.telemetry?.serviceName ?? 'chronicle',
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const port = config.telemetry?.port ?? 9090
|
|
25
|
+
const exporter = new PrometheusExporter({ port })
|
|
26
|
+
const provider = new MeterProvider({ resource, readers: [exporter] })
|
|
27
|
+
const meter = provider.getMeter('chronicle')
|
|
28
|
+
|
|
29
|
+
const requestCounter: Counter = meter.createCounter('http_server_request_total', {
|
|
30
|
+
description: 'Total HTTP requests',
|
|
31
|
+
})
|
|
32
|
+
const requestDuration: Histogram = meter.createHistogram('http_server_request_duration_ms', {
|
|
33
|
+
description: 'HTTP request duration in ms',
|
|
34
|
+
})
|
|
35
|
+
const ssrRenderDuration: Histogram = meter.createHistogram('http_server_ssr_render_duration_ms', {
|
|
36
|
+
description: 'SSR render duration in ms',
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
nitroApp.hooks.hook('close', async () => {
|
|
40
|
+
await provider.shutdown()
|
|
41
|
+
await exporter.shutdown()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
nitroApp.hooks.hook('chronicle:ssr-rendered', (route, status, durationMs) => {
|
|
45
|
+
ssrRenderDuration.record(durationMs, { route, status })
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
nitroApp.hooks.hook('request', (event) => {
|
|
49
|
+
(event as H3Event).context._requestStart = performance.now()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
nitroApp.hooks.hook('response', (res, event) => {
|
|
53
|
+
const start = (event as H3Event).context._requestStart as number | undefined
|
|
54
|
+
if (start === undefined) return
|
|
55
|
+
const duration = performance.now() - start
|
|
56
|
+
const method = event.req.method
|
|
57
|
+
const route = new URL(event.req.url).pathname
|
|
58
|
+
requestCounter.add(1, { method, route, status: res.status })
|
|
59
|
+
requestDuration.record(duration, { method, route, status: res.status })
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import matter from 'gray-matter';
|
|
3
|
+
import { defineHandler, HTTPError } from 'nitro';
|
|
4
|
+
import { loadConfig } from '@/lib/config';
|
|
5
|
+
import { getPage, getOriginalPath } from '@/lib/source';
|
|
6
|
+
import { safePath } from '@/server/utils/safe-path';
|
|
7
|
+
|
|
8
|
+
export default defineHandler(async event => {
|
|
9
|
+
const pathname = event.path || event.req.url?.split('?')[0] || '';
|
|
10
|
+
if (!pathname.endsWith('.md')) return;
|
|
11
|
+
|
|
12
|
+
const config = loadConfig();
|
|
13
|
+
if (!config.llms?.enabled) {
|
|
14
|
+
throw new HTTPError({ status: 404, message: 'Not Found' });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const stripped = pathname.replace(/\.md$/, '');
|
|
18
|
+
const parts = stripped === '/index' || stripped === '/'
|
|
19
|
+
? []
|
|
20
|
+
: stripped.slice(1).split('/').filter(Boolean);
|
|
21
|
+
const page = await getPage(parts);
|
|
22
|
+
|
|
23
|
+
if (!page) {
|
|
24
|
+
throw new HTTPError({ status: 404, message: 'Not Found' });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const originalPath = getOriginalPath(page);
|
|
28
|
+
if (!originalPath) {
|
|
29
|
+
throw new HTTPError({ status: 404, message: 'Not Found' });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const contentDir = __CHRONICLE_CONTENT_DIR__;
|
|
33
|
+
const filePath = safePath(contentDir, '/' + originalPath);
|
|
34
|
+
if (!filePath) {
|
|
35
|
+
throw new HTTPError({ status: 404, message: 'Not Found' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const raw = await fs.readFile(filePath, 'utf-8').catch(() => null);
|
|
39
|
+
if (!raw) {
|
|
40
|
+
throw new HTTPError({ status: 404, message: 'Not Found' });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
event.res.headers.set('Content-Type', 'text/markdown; charset=utf-8');
|
|
44
|
+
return matter(raw).content;
|
|
45
|
+
});
|
|
@@ -12,7 +12,8 @@ export default defineHandler(async event => {
|
|
|
12
12
|
const pages = await getPages();
|
|
13
13
|
const index = pages.map(p => {
|
|
14
14
|
const fm = extractFrontmatter(p);
|
|
15
|
-
|
|
15
|
+
const mdUrl = p.url === '/' ? '/index.md' : `${p.url}.md`;
|
|
16
|
+
return `- [${fm.title}](${mdUrl})`;
|
|
16
17
|
}).join('\n');
|
|
17
18
|
const body = `# ${config.title}\n\n${config.description ?? ''}\n\n${index}`;
|
|
18
19
|
|
package/src/types/config.ts
CHANGED
|
@@ -67,6 +67,12 @@ const analyticsSchema = z.object({
|
|
|
67
67
|
googleAnalytics: googleAnalyticsSchema.optional(),
|
|
68
68
|
})
|
|
69
69
|
|
|
70
|
+
const telemetrySchema = z.object({
|
|
71
|
+
enabled: z.boolean().optional(),
|
|
72
|
+
serviceName: z.string().optional(),
|
|
73
|
+
port: z.number().int().min(1).max(65535).default(9090),
|
|
74
|
+
})
|
|
75
|
+
|
|
70
76
|
export const chronicleConfigSchema = z.object({
|
|
71
77
|
title: z.string(),
|
|
72
78
|
description: z.string().optional(),
|
|
@@ -81,6 +87,7 @@ export const chronicleConfigSchema = z.object({
|
|
|
81
87
|
api: z.array(apiSchema).optional(),
|
|
82
88
|
llms: llmsSchema.optional(),
|
|
83
89
|
analytics: analyticsSchema.optional(),
|
|
90
|
+
telemetry: telemetrySchema.optional(),
|
|
84
91
|
})
|
|
85
92
|
|
|
86
93
|
export type ChronicleConfig = z.infer<typeof chronicleConfigSchema>
|
|
@@ -97,3 +104,4 @@ export type FooterConfig = z.infer<typeof footerSchema>
|
|
|
97
104
|
export type LlmsConfig = z.infer<typeof llmsSchema>
|
|
98
105
|
export type AnalyticsConfig = z.infer<typeof analyticsSchema>
|
|
99
106
|
export type GoogleAnalyticsConfig = z.infer<typeof googleAnalyticsSchema>
|
|
107
|
+
export type TelemetryConfig = z.infer<typeof telemetrySchema>
|