@raystack/chronicle 0.5.3 → 0.6.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/dist/cli/index.js +260 -81
- package/package.json +8 -6
- package/src/cli/commands/build.ts +5 -8
- package/src/cli/commands/dev.ts +5 -6
- package/src/cli/commands/init.test.ts +77 -0
- package/src/cli/commands/init.ts +73 -40
- package/src/cli/commands/serve.ts +6 -9
- package/src/cli/commands/start.ts +5 -5
- package/src/cli/utils/config.ts +6 -12
- package/src/cli/utils/scaffold.test.ts +179 -0
- package/src/cli/utils/scaffold.ts +70 -9
- package/src/components/api/field-row.tsx +1 -1
- package/src/components/api/field-section.tsx +2 -2
- package/src/components/mdx/index.tsx +1 -1
- package/src/components/mdx/mermaid.tsx +24 -21
- package/src/components/ui/breadcrumbs.tsx +4 -2
- package/src/components/ui/client-theme-switcher.tsx +21 -4
- package/src/components/ui/search.module.css +16 -41
- package/src/components/ui/search.tsx +30 -41
- package/src/lib/config.test.ts +493 -0
- package/src/lib/config.ts +123 -22
- package/src/lib/head.tsx +23 -5
- package/src/lib/llms.test.ts +94 -0
- package/src/lib/llms.ts +41 -0
- package/src/lib/navigation.test.ts +94 -0
- package/src/lib/navigation.ts +51 -0
- package/src/lib/page-context.tsx +79 -32
- package/src/lib/route-resolver.test.ts +173 -0
- package/src/lib/route-resolver.ts +73 -0
- package/src/lib/source.ts +94 -1
- package/src/lib/version-source.test.ts +163 -0
- package/src/lib/version-source.ts +101 -0
- package/src/pages/ApiPage.tsx +1 -1
- package/src/pages/DocsLayout.tsx +24 -3
- package/src/pages/DocsPage.tsx +7 -7
- package/src/pages/LandingPage.module.css +56 -0
- package/src/pages/LandingPage.tsx +39 -0
- package/src/pages/NotFound.module.css +3 -0
- package/src/pages/NotFound.tsx +9 -12
- package/src/server/App.tsx +21 -23
- package/src/server/api/{page/[...slug].ts → page.ts} +7 -3
- package/src/server/api/search.ts +51 -24
- package/src/server/api/specs.ts +17 -5
- package/src/server/entry-client.tsx +42 -14
- package/src/server/entry-server.tsx +35 -13
- package/src/server/plugins/telemetry.ts +47 -7
- package/src/server/routes/[...slug].md.ts +0 -6
- package/src/server/routes/[version]/llms.txt.ts +26 -0
- package/src/server/routes/llms.txt.ts +10 -13
- package/src/server/routes/og.tsx +2 -2
- package/src/server/routes/sitemap.xml.ts +14 -6
- package/src/server/vite-config.ts +5 -5
- package/src/themes/default/ContentDirButtons.tsx +66 -0
- package/src/themes/default/Layout.module.css +187 -40
- package/src/themes/default/Layout.tsx +166 -65
- package/src/themes/default/OpenInAI.tsx +112 -0
- package/src/themes/default/Page.module.css +30 -0
- package/src/themes/default/Page.tsx +1 -3
- package/src/themes/default/SidebarLogo.tsx +26 -0
- package/src/themes/default/Toc.module.css +102 -25
- package/src/themes/default/Toc.tsx +56 -10
- package/src/themes/default/VersionSwitcher.tsx +59 -0
- package/src/themes/paper/ContentDirDropdown.tsx +47 -0
- package/src/themes/paper/Layout.module.css +7 -0
- package/src/themes/paper/Layout.tsx +20 -13
- package/src/themes/paper/VersionSwitcher.tsx +60 -0
- package/src/types/config.ts +146 -23
- package/src/types/content.ts +11 -1
- package/src/types/theme.ts +1 -0
- package/src/components/ui/footer.module.css +0 -27
- package/src/components/ui/footer.tsx +0 -30
- package/src/server/api/metrics.ts +0 -23
- package/src/server/api/page/index.ts +0 -1
- package/src/server/telemetry.ts +0 -49
package/src/server/App.tsx
CHANGED
|
@@ -1,49 +1,47 @@
|
|
|
1
1
|
import '@raystack/apsara/normalize.css';
|
|
2
2
|
import '@raystack/apsara/style.css';
|
|
3
3
|
import { ThemeProvider } from '@raystack/apsara';
|
|
4
|
-
import { useLocation } from 'react-router';
|
|
4
|
+
import { Navigate, useLocation } from 'react-router';
|
|
5
5
|
import { Head } from '@/lib/head';
|
|
6
6
|
import { usePageContext } from '@/lib/page-context';
|
|
7
|
+
import { resolveRoute, RouteType } from '@/lib/route-resolver';
|
|
7
8
|
import { ApiLayout } from '@/pages/ApiLayout';
|
|
8
9
|
import { ApiPage } from '@/pages/ApiPage';
|
|
9
10
|
import { DocsLayout } from '@/pages/DocsLayout';
|
|
10
11
|
import { DocsPage } from '@/pages/DocsPage';
|
|
12
|
+
import { LandingPage } from '@/pages/LandingPage';
|
|
11
13
|
import type { ChronicleConfig } from '@/types';
|
|
12
14
|
import { getThemeConfig } from '@/themes/registry';
|
|
13
15
|
|
|
14
|
-
function resolveRoute(pathname: string) {
|
|
15
|
-
if (pathname.startsWith('/apis')) {
|
|
16
|
-
const slug = pathname
|
|
17
|
-
.replace(/^\/apis\/?/, '')
|
|
18
|
-
.split('/')
|
|
19
|
-
.filter(Boolean);
|
|
20
|
-
return { type: 'api' as const, slug };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const slug =
|
|
24
|
-
pathname === '/' ? [] : pathname.slice(1).split('/').filter(Boolean);
|
|
25
|
-
return { type: 'docs' as const, slug };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
16
|
export function App() {
|
|
29
17
|
const { pathname } = useLocation();
|
|
30
18
|
const { config } = usePageContext();
|
|
31
|
-
const route = resolveRoute(pathname);
|
|
19
|
+
const route = resolveRoute(pathname, config);
|
|
32
20
|
const themeConfig = getThemeConfig(config.theme?.name);
|
|
33
21
|
|
|
22
|
+
if (route.type === RouteType.Redirect) {
|
|
23
|
+
return <Navigate to={route.to} replace />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const isApi =
|
|
27
|
+
route.type === RouteType.ApiIndex || route.type === RouteType.ApiPage;
|
|
28
|
+
const apiSlug = route.type === RouteType.ApiPage ? route.slug : [];
|
|
29
|
+
const docsSlug = route.type === RouteType.DocsPage ? route.slug : [];
|
|
30
|
+
const isLanding = route.type === RouteType.DocsIndex;
|
|
31
|
+
|
|
34
32
|
return (
|
|
35
33
|
<ThemeProvider
|
|
36
34
|
enableSystem={themeConfig.enableSystem}
|
|
37
35
|
forcedTheme={themeConfig.forcedTheme}
|
|
38
36
|
>
|
|
39
37
|
<RootHead config={config} />
|
|
40
|
-
{
|
|
38
|
+
{isApi ? (
|
|
41
39
|
<ApiLayout>
|
|
42
|
-
<ApiPage slug={
|
|
40
|
+
<ApiPage slug={apiSlug} />
|
|
43
41
|
</ApiLayout>
|
|
44
42
|
) : (
|
|
45
43
|
<DocsLayout>
|
|
46
|
-
<DocsPage slug={
|
|
44
|
+
{isLanding ? <LandingPage /> : <DocsPage slug={docsSlug} />}
|
|
47
45
|
</DocsLayout>
|
|
48
46
|
)}
|
|
49
47
|
</ThemeProvider>
|
|
@@ -53,16 +51,16 @@ export function App() {
|
|
|
53
51
|
function RootHead({ config }: { config: ChronicleConfig }) {
|
|
54
52
|
return (
|
|
55
53
|
<Head
|
|
56
|
-
title={config.title}
|
|
57
|
-
description={config.description}
|
|
54
|
+
title={config.site.title}
|
|
55
|
+
description={config.site.description}
|
|
58
56
|
config={config}
|
|
59
57
|
jsonLd={
|
|
60
58
|
config.url
|
|
61
59
|
? {
|
|
62
60
|
'@context': 'https://schema.org',
|
|
63
61
|
'@type': 'WebSite',
|
|
64
|
-
name: config.title,
|
|
65
|
-
description: config.description,
|
|
62
|
+
name: config.site.title,
|
|
63
|
+
description: config.site.description,
|
|
66
64
|
url: config.url
|
|
67
65
|
}
|
|
68
66
|
: undefined
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { defineHandler, HTTPError } from 'nitro';
|
|
2
|
-
import { getPage, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source';
|
|
2
|
+
import { getPage, getPageNav, 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) {
|
|
10
10
|
throw new HTTPError({ status: 404, message: 'Page not found' });
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
const nav = await getPageNav(slug);
|
|
14
|
+
|
|
13
15
|
return {
|
|
14
16
|
frontmatter: extractFrontmatter(page, slug[slug.length - 1]),
|
|
15
17
|
relativePath: getRelativePath(page),
|
|
16
18
|
originalPath: getOriginalPath(page),
|
|
19
|
+
prev: nav.prev,
|
|
20
|
+
next: nav.next,
|
|
17
21
|
};
|
|
18
22
|
});
|
package/src/server/api/search.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import MiniSearch from 'minisearch';
|
|
2
|
-
import { defineHandler } from 'nitro';
|
|
2
|
+
import { defineHandler, HTTPError } from 'nitro';
|
|
3
3
|
import type { OpenAPIV3 } from 'openapi-types';
|
|
4
4
|
import { getSpecSlug } from '@/lib/api-routes';
|
|
5
|
-
import { loadConfig } from '@/lib/config';
|
|
5
|
+
import { getApiConfigsForVersion, loadConfig } from '@/lib/config';
|
|
6
6
|
import { loadApiSpecs } from '@/lib/openapi';
|
|
7
|
-
import {
|
|
7
|
+
import { extractFrontmatter, getPagesForVersion } from '@/lib/source';
|
|
8
|
+
import { LATEST_CONTEXT, type VersionContext } from '@/lib/version-source';
|
|
8
9
|
|
|
9
10
|
interface SearchDocument {
|
|
10
11
|
id: string;
|
|
@@ -14,8 +15,12 @@ interface SearchDocument {
|
|
|
14
15
|
type: 'page' | 'api';
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
const indexCache = new Map<string, MiniSearch<SearchDocument>>();
|
|
19
|
+
const docsCache = new Map<string, SearchDocument[]>();
|
|
20
|
+
|
|
21
|
+
function keyFor(ctx: VersionContext): string {
|
|
22
|
+
return ctx.dir ?? '__latest__';
|
|
23
|
+
}
|
|
19
24
|
|
|
20
25
|
function createIndex(docs: SearchDocument[]): MiniSearch<SearchDocument> {
|
|
21
26
|
const index = new MiniSearch<SearchDocument>({
|
|
@@ -31,8 +36,8 @@ function createIndex(docs: SearchDocument[]): MiniSearch<SearchDocument> {
|
|
|
31
36
|
return index;
|
|
32
37
|
}
|
|
33
38
|
|
|
34
|
-
async function scanContent(): Promise<SearchDocument[]> {
|
|
35
|
-
const pages = await
|
|
39
|
+
async function scanContent(ctx: VersionContext): Promise<SearchDocument[]> {
|
|
40
|
+
const pages = await getPagesForVersion(ctx);
|
|
36
41
|
return pages.map(p => {
|
|
37
42
|
const fm = extractFrontmatter(p);
|
|
38
43
|
return {
|
|
@@ -45,12 +50,13 @@ async function scanContent(): Promise<SearchDocument[]> {
|
|
|
45
50
|
});
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
async function buildApiDocs(): Promise<SearchDocument[]> {
|
|
53
|
+
async function buildApiDocs(ctx: VersionContext): Promise<SearchDocument[]> {
|
|
49
54
|
const config = loadConfig();
|
|
50
|
-
|
|
55
|
+
const apiConfigs = getApiConfigsForVersion(config, ctx.dir);
|
|
56
|
+
if (!apiConfigs.length) return [];
|
|
51
57
|
|
|
52
58
|
const docs: SearchDocument[] = [];
|
|
53
|
-
const specs = await loadApiSpecs(
|
|
59
|
+
const specs = await loadApiSpecs(apiConfigs);
|
|
54
60
|
|
|
55
61
|
for (const spec of specs) {
|
|
56
62
|
const specSlug = getSpecSlug(spec);
|
|
@@ -60,7 +66,7 @@ async function buildApiDocs(): Promise<SearchDocument[]> {
|
|
|
60
66
|
for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) {
|
|
61
67
|
const op = pathItem[method] as OpenAPIV3.OperationObject | undefined;
|
|
62
68
|
if (!op?.operationId) continue;
|
|
63
|
-
const url =
|
|
69
|
+
const url = `${ctx.urlPrefix}/apis/${specSlug}/${encodeURIComponent(op.operationId)}`;
|
|
64
70
|
docs.push({
|
|
65
71
|
id: url,
|
|
66
72
|
url,
|
|
@@ -75,29 +81,50 @@ async function buildApiDocs(): Promise<SearchDocument[]> {
|
|
|
75
81
|
return docs;
|
|
76
82
|
}
|
|
77
83
|
|
|
78
|
-
async function getDocs(): Promise<SearchDocument[]> {
|
|
79
|
-
|
|
84
|
+
async function getDocs(ctx: VersionContext): Promise<SearchDocument[]> {
|
|
85
|
+
const key = keyFor(ctx);
|
|
86
|
+
const cached = docsCache.get(key);
|
|
87
|
+
if (cached) return cached;
|
|
80
88
|
const [contentDocs, apiDocs] = await Promise.all([
|
|
81
|
-
scanContent(),
|
|
82
|
-
buildApiDocs()
|
|
89
|
+
scanContent(ctx),
|
|
90
|
+
buildApiDocs(ctx)
|
|
83
91
|
]);
|
|
84
|
-
|
|
85
|
-
|
|
92
|
+
const docs = [...contentDocs, ...apiDocs];
|
|
93
|
+
docsCache.set(key, docs);
|
|
94
|
+
return docs;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function getIndex(ctx: VersionContext): Promise<MiniSearch<SearchDocument>> {
|
|
98
|
+
const key = keyFor(ctx);
|
|
99
|
+
const cached = indexCache.get(key);
|
|
100
|
+
if (cached) return cached;
|
|
101
|
+
const docs = await getDocs(ctx);
|
|
102
|
+
const index = createIndex(docs);
|
|
103
|
+
indexCache.set(key, index);
|
|
104
|
+
return index;
|
|
86
105
|
}
|
|
87
106
|
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
107
|
+
function resolveCtx(tag: string | null): VersionContext {
|
|
108
|
+
if (!tag) return LATEST_CONTEXT;
|
|
109
|
+
const config = loadConfig();
|
|
110
|
+
const version = config.versions?.find(v => v.dir === tag);
|
|
111
|
+
if (!version) {
|
|
112
|
+
throw new HTTPError({
|
|
113
|
+
status: 400,
|
|
114
|
+
message: `Unknown version tag: ${tag}`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return { dir: version.dir, urlPrefix: `/${version.dir}` };
|
|
93
118
|
}
|
|
94
119
|
|
|
95
120
|
export default defineHandler(async event => {
|
|
96
121
|
const query = event.url.searchParams.get('query') ?? '';
|
|
97
|
-
const
|
|
122
|
+
const tag = event.url.searchParams.get('tag');
|
|
123
|
+
const ctx = resolveCtx(tag);
|
|
124
|
+
const index = await getIndex(ctx);
|
|
98
125
|
|
|
99
126
|
if (!query) {
|
|
100
|
-
const docs = await getDocs();
|
|
127
|
+
const docs = await getDocs(ctx);
|
|
101
128
|
return docs
|
|
102
129
|
.filter(d => d.type === 'page')
|
|
103
130
|
.slice(0, 8)
|
package/src/server/api/specs.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
import { defineHandler } from 'nitro';
|
|
2
|
-
import { loadConfig } from '@/lib/config';
|
|
1
|
+
import { defineHandler, HTTPError } from 'nitro';
|
|
2
|
+
import { getApiConfigsForVersion, loadConfig } from '@/lib/config';
|
|
3
3
|
import { loadApiSpecs } from '@/lib/openapi';
|
|
4
4
|
|
|
5
|
-
export default defineHandler(async
|
|
5
|
+
export default defineHandler(async event => {
|
|
6
|
+
const versionParam = event.url.searchParams.get('version');
|
|
7
|
+
const versionDir = versionParam === null || versionParam === '' ? null : versionParam;
|
|
8
|
+
|
|
6
9
|
const config = loadConfig();
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
if (versionDir && !config.versions?.some(v => v.dir === versionDir)) {
|
|
11
|
+
throw new HTTPError({
|
|
12
|
+
status: 400,
|
|
13
|
+
message: `Unknown version: ${versionDir}`,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const apiConfigs = getApiConfigsForVersion(config, versionDir);
|
|
18
|
+
if (!apiConfigs.length) return [];
|
|
19
|
+
|
|
20
|
+
return loadApiSpecs(apiConfigs);
|
|
9
21
|
});
|
|
@@ -4,8 +4,11 @@ import { hydrateRoot } from 'react-dom/client';
|
|
|
4
4
|
import { BrowserRouter } from 'react-router';
|
|
5
5
|
import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
|
|
6
6
|
import { mdxComponents } from '@/components/mdx';
|
|
7
|
+
import { getApiConfigsForVersion } from '@/lib/config';
|
|
7
8
|
import { PageProvider } from '@/lib/page-context';
|
|
8
|
-
import
|
|
9
|
+
import { resolveRoute, RouteType } from '@/lib/route-resolver';
|
|
10
|
+
import { resolveVersionFromUrl, type VersionContext } from '@/lib/version-source';
|
|
11
|
+
import type { ChronicleConfig, Frontmatter, PageNavLink, Root, TableOfContents } from '@/types';
|
|
9
12
|
import type { ApiSpec } from '@/lib/openapi';
|
|
10
13
|
import type { ReactNode } from 'react';
|
|
11
14
|
import { App } from './App';
|
|
@@ -14,11 +17,19 @@ interface EmbeddedData {
|
|
|
14
17
|
config: ChronicleConfig;
|
|
15
18
|
tree: Root;
|
|
16
19
|
slug: string[];
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
version: VersionContext;
|
|
21
|
+
frontmatter: Frontmatter | null;
|
|
22
|
+
relativePath: string | null;
|
|
23
|
+
originalPath: string | null;
|
|
24
|
+
prev: PageNavLink | null;
|
|
25
|
+
next: PageNavLink | null;
|
|
20
26
|
}
|
|
21
27
|
|
|
28
|
+
const defaultConfig: ChronicleConfig = {
|
|
29
|
+
site: { title: 'Documentation' },
|
|
30
|
+
content: [{ dir: 'docs', label: 'Docs' }],
|
|
31
|
+
};
|
|
32
|
+
|
|
22
33
|
const contentModules = import.meta.glob<{ default?: React.ComponentType<any>; toc?: TableOfContents }>(
|
|
23
34
|
'../../.content/**/*.{mdx,md}'
|
|
24
35
|
);
|
|
@@ -43,23 +54,39 @@ async function hydrate() {
|
|
|
43
54
|
window as unknown as { __PAGE_DATA__?: EmbeddedData }
|
|
44
55
|
).__PAGE_DATA__;
|
|
45
56
|
|
|
46
|
-
const config: ChronicleConfig = embedded?.config ??
|
|
47
|
-
title: 'Documentation'
|
|
48
|
-
};
|
|
57
|
+
const config: ChronicleConfig = embedded?.config ?? defaultConfig;
|
|
49
58
|
const tree: Root = embedded?.tree ?? { name: 'root', children: [] };
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
|
|
60
|
+
const route = resolveRoute(window.location.pathname, config);
|
|
61
|
+
// resolveVersionFromUrl always returns a valid context — even for redirect
|
|
62
|
+
// targets (e.g. /v1 -> /v1/docs) where route.version isn't on the union.
|
|
63
|
+
const routeVersion: VersionContext = resolveVersionFromUrl(
|
|
64
|
+
window.location.pathname,
|
|
65
|
+
config,
|
|
66
|
+
);
|
|
67
|
+
const version: VersionContext = embedded?.version ?? routeVersion;
|
|
68
|
+
|
|
69
|
+
const isApiRoute =
|
|
70
|
+
route.type === RouteType.ApiIndex || route.type === RouteType.ApiPage;
|
|
71
|
+
const apiConfigs = isApiRoute
|
|
72
|
+
? getApiConfigsForVersion(config, routeVersion.dir)
|
|
73
|
+
: [];
|
|
74
|
+
const specsUrl = routeVersion.dir
|
|
75
|
+
? `/api/specs?version=${encodeURIComponent(routeVersion.dir)}`
|
|
76
|
+
: '/api/specs';
|
|
77
|
+
const apiSpecs: ApiSpec[] = apiConfigs.length
|
|
78
|
+
? await fetch(specsUrl)
|
|
54
79
|
.then(r => r.json())
|
|
55
80
|
.catch(() => [])
|
|
56
81
|
: [];
|
|
57
82
|
|
|
58
83
|
const mdxPath = embedded?.originalPath || embedded?.relativePath;
|
|
59
|
-
const page = mdxPath
|
|
84
|
+
const page = embedded && mdxPath && embedded.frontmatter
|
|
60
85
|
? {
|
|
61
|
-
slug: embedded
|
|
62
|
-
frontmatter: embedded
|
|
86
|
+
slug: embedded.slug,
|
|
87
|
+
frontmatter: embedded.frontmatter,
|
|
88
|
+
prev: embedded.prev,
|
|
89
|
+
next: embedded.next,
|
|
63
90
|
...(await loadMdxModule(mdxPath)),
|
|
64
91
|
}
|
|
65
92
|
: null;
|
|
@@ -73,6 +100,7 @@ async function hydrate() {
|
|
|
73
100
|
initialTree={tree}
|
|
74
101
|
initialPage={page}
|
|
75
102
|
initialApiSpecs={apiSpecs}
|
|
103
|
+
initialVersion={version}
|
|
76
104
|
loadMdx={loadMdxModule}
|
|
77
105
|
>
|
|
78
106
|
<App />
|
|
@@ -5,12 +5,13 @@ import { renderToReadableStream } from 'react-dom/server.edge';
|
|
|
5
5
|
import { StaticRouter } from 'react-router';
|
|
6
6
|
import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
|
|
7
7
|
import { mdxComponents } from '@/components/mdx';
|
|
8
|
-
import { loadConfig } from '@/lib/config';
|
|
8
|
+
import { getApiConfigsForVersion, loadConfig } from '@/lib/config';
|
|
9
9
|
import { loadApiSpecs } from '@/lib/openapi';
|
|
10
10
|
import { PageProvider } from '@/lib/page-context';
|
|
11
|
-
import {
|
|
11
|
+
import { resolveRoute, RouteType } from '@/lib/route-resolver';
|
|
12
|
+
import { getPageTree, getPage, getPageNav, loadPageModule, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source';
|
|
13
|
+
import { useNitroApp } from 'nitro/app';
|
|
12
14
|
import { App } from './App';
|
|
13
|
-
import { recordSSRRender } from './telemetry';
|
|
14
15
|
|
|
15
16
|
import clientAssets from './entry-client?assets=client';
|
|
16
17
|
import serverAssets from './entry-server?assets=ssr';
|
|
@@ -19,17 +20,32 @@ export default {
|
|
|
19
20
|
async fetch(req: Request) {
|
|
20
21
|
const url = new URL(req.url);
|
|
21
22
|
const pathname = url.pathname;
|
|
22
|
-
const slug = pathname === '/' ? [] : pathname.slice(1).split('/').filter(Boolean);
|
|
23
23
|
|
|
24
24
|
const config = loadConfig();
|
|
25
|
-
const
|
|
26
|
-
|
|
25
|
+
const route = resolveRoute(pathname, config);
|
|
26
|
+
|
|
27
|
+
if (route.type === RouteType.Redirect) {
|
|
28
|
+
// biome-ignore lint/correctness/useHookAtTopLevel: useNitroApp is a Nitro DI accessor, not a React hook
|
|
29
|
+
useNitroApp().hooks.callHook('chronicle:ssr-rendered', pathname, route.status, 0);
|
|
30
|
+
return new Response(null, {
|
|
31
|
+
status: route.status,
|
|
32
|
+
headers: { Location: route.to },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const isApiRoute = route.type === RouteType.ApiIndex || route.type === RouteType.ApiPage;
|
|
37
|
+
const pageSlug = route.type === RouteType.DocsPage ? route.slug : [];
|
|
38
|
+
|
|
39
|
+
const apiConfigs = isApiRoute
|
|
40
|
+
? getApiConfigsForVersion(config, route.version.dir)
|
|
27
41
|
: [];
|
|
42
|
+
const apiSpecs = apiConfigs.length ? await loadApiSpecs(apiConfigs) : [];
|
|
28
43
|
|
|
29
44
|
const [tree, page] = await Promise.all([
|
|
30
45
|
getPageTree(),
|
|
31
|
-
getPage(slug),
|
|
46
|
+
route.type === RouteType.DocsPage ? getPage(route.slug) : Promise.resolve(null),
|
|
32
47
|
]);
|
|
48
|
+
const nav = page ? await getPageNav(pageSlug, tree) : { prev: null, next: null };
|
|
33
49
|
|
|
34
50
|
const relativePath = page ? getRelativePath(page) : null;
|
|
35
51
|
const originalPath = page ? getOriginalPath(page) : null;
|
|
@@ -37,22 +53,27 @@ export default {
|
|
|
37
53
|
|
|
38
54
|
const pageData = page
|
|
39
55
|
? {
|
|
40
|
-
slug,
|
|
41
|
-
frontmatter: extractFrontmatter(page,
|
|
56
|
+
slug: pageSlug,
|
|
57
|
+
frontmatter: extractFrontmatter(page, pageSlug[pageSlug.length - 1]),
|
|
42
58
|
content: mdxModule?.default
|
|
43
59
|
? React.createElement(mdxModule.default, { components: mdxComponents })
|
|
44
60
|
: null,
|
|
45
61
|
toc: mdxModule?.toc ?? [],
|
|
62
|
+
prev: nav.prev,
|
|
63
|
+
next: nav.next,
|
|
46
64
|
}
|
|
47
65
|
: null;
|
|
48
66
|
|
|
49
67
|
const embeddedData = {
|
|
50
68
|
config,
|
|
51
69
|
tree,
|
|
52
|
-
slug,
|
|
70
|
+
slug: pageSlug,
|
|
71
|
+
version: route.version,
|
|
53
72
|
frontmatter: pageData?.frontmatter ?? null,
|
|
54
73
|
relativePath,
|
|
55
74
|
originalPath,
|
|
75
|
+
prev: pageData?.prev ?? null,
|
|
76
|
+
next: pageData?.next ?? null,
|
|
56
77
|
};
|
|
57
78
|
const safeJson = JSON.stringify(embeddedData).replace(/</g, '\\u003c');
|
|
58
79
|
|
|
@@ -82,6 +103,7 @@ export default {
|
|
|
82
103
|
initialTree={tree}
|
|
83
104
|
initialPage={pageData}
|
|
84
105
|
initialApiSpecs={apiSpecs}
|
|
106
|
+
initialVersion={route.version}
|
|
85
107
|
loadMdx={async () => ({ content: null, toc: [] })}
|
|
86
108
|
>
|
|
87
109
|
<App />
|
|
@@ -95,10 +117,10 @@ export default {
|
|
|
95
117
|
|
|
96
118
|
const renderDuration = performance.now() - renderStart;
|
|
97
119
|
|
|
98
|
-
const
|
|
99
|
-
const status = !page && !isApiRoute && slug.length > 0 ? 404 : 200;
|
|
120
|
+
const status = route.type === RouteType.DocsPage && !page ? 404 : 200;
|
|
100
121
|
|
|
101
|
-
|
|
122
|
+
// biome-ignore lint/correctness/useHookAtTopLevel: useNitroApp is a Nitro DI accessor, not a React hook
|
|
123
|
+
useNitroApp().hooks.callHook('chronicle:ssr-rendered', pathname, status, renderDuration);
|
|
102
124
|
|
|
103
125
|
return new Response(stream, {
|
|
104
126
|
status,
|
|
@@ -1,21 +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'
|
|
1
7
|
import { definePlugin } from 'nitro'
|
|
2
8
|
import { loadConfig } from '@/lib/config'
|
|
3
|
-
|
|
9
|
+
|
|
10
|
+
declare module 'nitro/types' {
|
|
11
|
+
interface NitroRuntimeHooks {
|
|
12
|
+
'chronicle:ssr-rendered': (route: string, status: number, durationMs: number) => void
|
|
13
|
+
}
|
|
14
|
+
}
|
|
4
15
|
|
|
5
16
|
export default definePlugin((nitroApp) => {
|
|
6
17
|
const config = loadConfig()
|
|
7
18
|
if (!config.telemetry?.enabled) return
|
|
8
19
|
|
|
9
|
-
|
|
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
|
+
})
|
|
10
47
|
|
|
11
48
|
nitroApp.hooks.hook('request', (event) => {
|
|
12
|
-
|
|
13
|
-
event.context._requestStart = performance.now()
|
|
49
|
+
(event as H3Event).context._requestStart = performance.now()
|
|
14
50
|
})
|
|
15
51
|
|
|
16
52
|
nitroApp.hooks.hook('response', (res, event) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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 })
|
|
20
60
|
})
|
|
21
61
|
})
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import matter from 'gray-matter';
|
|
3
3
|
import { defineHandler, HTTPError } from 'nitro';
|
|
4
|
-
import { loadConfig } from '@/lib/config';
|
|
5
4
|
import { getPage, getOriginalPath } from '@/lib/source';
|
|
6
5
|
import { safePath } from '@/server/utils/safe-path';
|
|
7
6
|
|
|
@@ -9,11 +8,6 @@ export default defineHandler(async event => {
|
|
|
9
8
|
const pathname = event.path || event.req.url?.split('?')[0] || '';
|
|
10
9
|
if (!pathname.endsWith('.md')) return;
|
|
11
10
|
|
|
12
|
-
const config = loadConfig();
|
|
13
|
-
if (!config.llms?.enabled) {
|
|
14
|
-
throw new HTTPError({ status: 404, message: 'Not Found' });
|
|
15
|
-
}
|
|
16
|
-
|
|
17
11
|
const stripped = pathname.replace(/\.md$/, '');
|
|
18
12
|
const parts = stripped === '/index' || stripped === '/'
|
|
19
13
|
? []
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { getRouterParam } from 'h3';
|
|
2
|
+
import { defineHandler, HTTPError } from 'nitro';
|
|
3
|
+
import { loadConfig } from '@/lib/config';
|
|
4
|
+
import { buildLlmsTxt } from '@/lib/llms';
|
|
5
|
+
import { extractFrontmatter, getPagesForVersion } from '@/lib/source';
|
|
6
|
+
|
|
7
|
+
export default defineHandler(async event => {
|
|
8
|
+
const config = loadConfig();
|
|
9
|
+
|
|
10
|
+
const versionDir = getRouterParam(event, 'version');
|
|
11
|
+
const version = config.versions?.find(v => v.dir === versionDir);
|
|
12
|
+
if (!version) {
|
|
13
|
+
throw new HTTPError({ status: 404, message: 'Not Found' });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ctx = { dir: version.dir, urlPrefix: `/${version.dir}` };
|
|
17
|
+
const pages = await getPagesForVersion(ctx);
|
|
18
|
+
const body = buildLlmsTxt(
|
|
19
|
+
config,
|
|
20
|
+
pages.map(p => ({ url: p.url, title: extractFrontmatter(p).title })),
|
|
21
|
+
ctx,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
event.res.headers.set('Content-Type', 'text/plain');
|
|
25
|
+
return body;
|
|
26
|
+
});
|
|
@@ -1,21 +1,18 @@
|
|
|
1
|
-
import { defineHandler
|
|
1
|
+
import { defineHandler } from 'nitro';
|
|
2
2
|
import { loadConfig } from '@/lib/config';
|
|
3
|
-
import {
|
|
3
|
+
import { buildLlmsTxt } from '@/lib/llms';
|
|
4
|
+
import { extractFrontmatter, getPagesForVersion } from '@/lib/source';
|
|
5
|
+
import { LATEST_CONTEXT } from '@/lib/version-source';
|
|
4
6
|
|
|
5
7
|
export default defineHandler(async event => {
|
|
6
8
|
const config = loadConfig();
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const fm = extractFrontmatter(p);
|
|
15
|
-
const mdUrl = p.url === '/' ? '/index.md' : `${p.url}.md`;
|
|
16
|
-
return `- [${fm.title}](${mdUrl})`;
|
|
17
|
-
}).join('\n');
|
|
18
|
-
const body = `# ${config.title}\n\n${config.description ?? ''}\n\n${index}`;
|
|
10
|
+
const pages = await getPagesForVersion(LATEST_CONTEXT);
|
|
11
|
+
const body = buildLlmsTxt(
|
|
12
|
+
config,
|
|
13
|
+
pages.map(p => ({ url: p.url, title: extractFrontmatter(p).title })),
|
|
14
|
+
LATEST_CONTEXT,
|
|
15
|
+
);
|
|
19
16
|
|
|
20
17
|
event.res.headers.set('Content-Type', 'text/plain');
|
|
21
18
|
return body;
|
package/src/server/routes/og.tsx
CHANGED
|
@@ -22,9 +22,9 @@ async function loadFont(): Promise<ArrayBuffer> {
|
|
|
22
22
|
|
|
23
23
|
export default defineHandler(async event => {
|
|
24
24
|
const config = loadConfig();
|
|
25
|
-
const title = event.url.searchParams.get('title') ?? config.title;
|
|
25
|
+
const title = event.url.searchParams.get('title') ?? config.site.title;
|
|
26
26
|
const description = event.url.searchParams.get('description') ?? '';
|
|
27
|
-
const siteName = config.title;
|
|
27
|
+
const siteName = config.site.title;
|
|
28
28
|
|
|
29
29
|
const font = await loadFont();
|
|
30
30
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineHandler } from 'nitro';
|
|
2
2
|
import { buildApiRoutes } from '@/lib/api-routes';
|
|
3
|
-
import { loadConfig } from '@/lib/config';
|
|
3
|
+
import { getAllVersions, getApiConfigsForVersion, loadConfig } from '@/lib/config';
|
|
4
4
|
import { loadApiSpecs } from '@/lib/openapi';
|
|
5
5
|
import { getPages } from '@/lib/source';
|
|
6
6
|
|
|
@@ -23,11 +23,19 @@ export default defineHandler(async event => {
|
|
|
23
23
|
return `<url><loc>${baseUrl}/${page.slugs.join('/')}</loc>${lastmod}</url>`;
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
const apiPages =
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
const apiPages: string[] = [];
|
|
27
|
+
for (const v of getAllVersions(config)) {
|
|
28
|
+
const versionDir = v.isLatest ? null : v.dir;
|
|
29
|
+
const apiConfigs = getApiConfigsForVersion(config, versionDir);
|
|
30
|
+
if (!apiConfigs.length) continue;
|
|
31
|
+
const prefix = versionDir ? `/${versionDir}` : '';
|
|
32
|
+
const routes = buildApiRoutes(await loadApiSpecs(apiConfigs));
|
|
33
|
+
for (const route of routes) {
|
|
34
|
+
apiPages.push(
|
|
35
|
+
`<url><loc>${baseUrl}${prefix}/apis/${route.slug.join('/')}</loc></url>`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
31
39
|
|
|
32
40
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
33
41
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|