@raystack/chronicle 0.10.1 → 0.10.3
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 +30 -3
- package/package.json +2 -1
- package/src/components/api/api-overview.tsx +2 -2
- package/src/components/api/playground-dialog.tsx +28 -6
- package/src/components/mdx/paragraph.tsx +1 -1
- package/src/components/ui/PrefetchProvider.tsx +72 -0
- package/src/components/ui/search.module.css +6 -0
- package/src/components/ui/search.tsx +4 -1
- package/src/lib/env.ts +9 -0
- package/src/lib/mdx-utils.ts +4 -0
- package/src/lib/openapi.ts +2 -1
- package/src/lib/page-context.tsx +18 -6
- package/src/lib/preload.ts +42 -0
- package/src/lib/remark-resolve-images.ts +21 -2
- package/src/lib/source.ts +32 -3
- package/src/pages/DocsLayout.tsx +11 -8
- package/src/server/App.module.css +4 -0
- package/src/server/App.tsx +32 -15
- package/src/server/api/page.ts +3 -2
- package/src/server/api/search.ts +16 -6
- package/src/server/entry-client.tsx +18 -14
- package/src/server/entry-server.tsx +8 -2
- package/src/server/error.ts +11 -0
- package/src/server/vite-config.ts +2 -1
- package/src/themes/default/Layout.tsx +22 -17
- package/src/themes/default/Page.tsx +6 -2
- package/src/types/content.ts +1 -0
package/dist/cli/index.js
CHANGED
|
@@ -46,6 +46,15 @@ var __export = (target, all) => {
|
|
|
46
46
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
47
47
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
48
48
|
|
|
49
|
+
// src/lib/mdx-utils.ts
|
|
50
|
+
var MdxNodeType;
|
|
51
|
+
var init_mdx_utils = __esm(() => {
|
|
52
|
+
MdxNodeType = {
|
|
53
|
+
JsxFlow: "mdxJsxFlowElement",
|
|
54
|
+
JsxText: "mdxJsxTextElement"
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
|
|
49
58
|
// src/lib/remark-resolve-images.ts
|
|
50
59
|
import path4 from "node:path";
|
|
51
60
|
import { visit } from "unist-util-visit";
|
|
@@ -72,16 +81,29 @@ var remarkResolveImages = () => {
|
|
|
72
81
|
return;
|
|
73
82
|
const relative = filePath.slice(contentIdx + "/content/".length);
|
|
74
83
|
const dir = path4.posix.dirname(relative);
|
|
84
|
+
const seen = new Set;
|
|
85
|
+
const images = [];
|
|
86
|
+
function collect(src) {
|
|
87
|
+
if (!src || seen.has(src) || /^data:/i.test(src))
|
|
88
|
+
return;
|
|
89
|
+
seen.add(src);
|
|
90
|
+
images.push(src);
|
|
91
|
+
}
|
|
75
92
|
visit(tree, "image", (node) => {
|
|
76
93
|
if (!node.url)
|
|
77
94
|
return;
|
|
78
95
|
node.url = resolveUrl(node.url, dir);
|
|
96
|
+
collect(node.url);
|
|
79
97
|
});
|
|
80
98
|
visit(tree, "html", (node) => {
|
|
81
|
-
node.value = node.value.replace(/(<img\b[^>]*\bsrc=["'])([^"']+)(["'])/gi, (_, before, src, after) =>
|
|
99
|
+
node.value = node.value.replace(/(<img\b[^>]*\bsrc=["'])([^"']+)(["'])/gi, (_, before, src, after) => {
|
|
100
|
+
const resolved = resolveUrl(src, dir);
|
|
101
|
+
collect(resolved);
|
|
102
|
+
return `${before}${resolved}${after}`;
|
|
103
|
+
});
|
|
82
104
|
});
|
|
83
105
|
visit(tree, (node) => {
|
|
84
|
-
if (node.type !==
|
|
106
|
+
if (node.type !== MdxNodeType.JsxFlow && node.type !== MdxNodeType.JsxText)
|
|
85
107
|
return;
|
|
86
108
|
const jsx = node;
|
|
87
109
|
if (jsx.name !== "img")
|
|
@@ -90,6 +112,7 @@ var remarkResolveImages = () => {
|
|
|
90
112
|
if (!srcAttr?.value || typeof srcAttr.value !== "string")
|
|
91
113
|
return;
|
|
92
114
|
srcAttr.value = resolveUrl(srcAttr.value, dir);
|
|
115
|
+
collect(srcAttr.value);
|
|
93
116
|
});
|
|
94
117
|
visit(tree, "element", (node) => {
|
|
95
118
|
if (node.tagName !== "img")
|
|
@@ -98,10 +121,13 @@ var remarkResolveImages = () => {
|
|
|
98
121
|
if (typeof src !== "string")
|
|
99
122
|
return;
|
|
100
123
|
node.properties.src = resolveUrl(src, dir);
|
|
124
|
+
collect(node.properties.src);
|
|
101
125
|
});
|
|
126
|
+
file.data.images = images;
|
|
102
127
|
};
|
|
103
128
|
}, remark_resolve_images_default;
|
|
104
129
|
var init_remark_resolve_images = __esm(() => {
|
|
130
|
+
init_mdx_utils();
|
|
105
131
|
remark_resolve_images_default = remarkResolveImages;
|
|
106
132
|
});
|
|
107
133
|
|
|
@@ -360,7 +386,7 @@ async function createViteConfig(options) {
|
|
|
360
386
|
default: defineFumadocsConfig({
|
|
361
387
|
mdxOptions: {
|
|
362
388
|
remarkImageOptions: false,
|
|
363
|
-
valueToExport: ["readingTime"],
|
|
389
|
+
valueToExport: ["readingTime", "images"],
|
|
364
390
|
remarkPlugins: [
|
|
365
391
|
remarkDirective,
|
|
366
392
|
[remarkDirectiveAdmonition, {
|
|
@@ -433,6 +459,7 @@ async function createViteConfig(options) {
|
|
|
433
459
|
},
|
|
434
460
|
nitro: {
|
|
435
461
|
logLevel: 2,
|
|
462
|
+
errorHandler: path6.resolve(packageRoot, "src/server/error.ts"),
|
|
436
463
|
publicAssets: [{ dir: path6.resolve(projectRoot, "public") }],
|
|
437
464
|
output: {
|
|
438
465
|
dir: resolveOutputDir(projectRoot, preset)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raystack/chronicle",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "Config-driven documentation framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"@radix-ui/react-icons": "^1.3.2",
|
|
50
50
|
"@raystack/apsara": "1.0.0-rc.7",
|
|
51
51
|
"@shikijs/rehype": "^4.0.2",
|
|
52
|
+
"@tanstack/react-query": "5.100.10",
|
|
52
53
|
"@vitejs/plugin-react": "^6.0.1",
|
|
53
54
|
"chalk": "^5.6.2",
|
|
54
55
|
"class-variance-authority": "^0.7.1",
|
|
@@ -21,7 +21,7 @@ interface ApiOverviewProps {
|
|
|
21
21
|
auth?: { type: string; header: string; placeholder?: string }
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export function ApiOverview({ method, path, operation, auth }: ApiOverviewProps) {
|
|
24
|
+
export function ApiOverview({ method, path, operation, serverUrl, auth }: ApiOverviewProps) {
|
|
25
25
|
const params = (operation.parameters ?? []) as OpenAPIV3.ParameterObject[]
|
|
26
26
|
const body = getRequestBody(operation.requestBody as OpenAPIV3.RequestBodyObject | undefined)
|
|
27
27
|
|
|
@@ -36,7 +36,7 @@ export function ApiOverview({ method, path, operation, auth }: ApiOverviewProps)
|
|
|
36
36
|
? headerFields
|
|
37
37
|
: []
|
|
38
38
|
|
|
39
|
-
const fullUrl =
|
|
39
|
+
const fullUrl = serverUrl + path
|
|
40
40
|
const snippetHeaders: Record<string, string> = {}
|
|
41
41
|
if (auth) snippetHeaders[auth.header] = auth.placeholder ?? 'YOUR_API_KEY'
|
|
42
42
|
if (body) snippetHeaders['Content-Type'] = body.contentType ?? 'application/json'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState, useCallback, useMemo } from 'react'
|
|
3
|
+
import { useState, useCallback, useMemo, useEffect } from 'react'
|
|
4
4
|
import type { OpenAPIV3 } from 'openapi-types'
|
|
5
5
|
import { Dialog, Button, Badge, IconButton, Input, CopyButton, Select, Menu } from '@raystack/apsara'
|
|
6
6
|
import { Cross2Icon, ChevronDownIcon, ChevronUpIcon, PlayIcon, PlusIcon } from '@radix-ui/react-icons'
|
|
@@ -68,11 +68,21 @@ export function PlaygroundDialog({
|
|
|
68
68
|
|
|
69
69
|
const authSchemes = useMemo(() => getAuthSchemes(document, auth), [document, auth])
|
|
70
70
|
const defaultScheme = authSchemes.find((s) => s.type !== 'none') ?? authSchemes[0]
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
const storageKey = `chronicle:auth:${specName}`
|
|
72
|
+
const savedAuth = useMemo(() => {
|
|
73
|
+
try {
|
|
74
|
+
const raw = sessionStorage.getItem(storageKey)
|
|
75
|
+
return raw ? JSON.parse(raw) : null
|
|
76
|
+
} catch { return null }
|
|
77
|
+
}, [storageKey])
|
|
78
|
+
|
|
79
|
+
const [selectedScheme, setSelectedScheme] = useState(() => {
|
|
80
|
+
if (savedAuth?.scheme && authSchemes.some((s) => s.name === savedAuth.scheme)) return savedAuth.scheme
|
|
81
|
+
return defaultScheme.name
|
|
82
|
+
})
|
|
83
|
+
const [authToken, setAuthToken] = useState(savedAuth?.token ?? '')
|
|
84
|
+
const [basicUser, setBasicUser] = useState(savedAuth?.basicUser ?? '')
|
|
85
|
+
const [basicPass, setBasicPass] = useState(savedAuth?.basicPass ?? '')
|
|
76
86
|
const [headerValues, setHeaderValues] = useState<Record<string, string>>({})
|
|
77
87
|
const [pathValues, setPathValues] = useState<Record<string, string>>({})
|
|
78
88
|
const [queryValues, setQueryValues] = useState<Record<string, string>>({})
|
|
@@ -89,6 +99,17 @@ export function PlaygroundDialog({
|
|
|
89
99
|
})
|
|
90
100
|
const [bodyJsonStr, setBodyJsonStr] = useState(() => body ? body.jsonExample : '{}')
|
|
91
101
|
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
try {
|
|
104
|
+
sessionStorage.setItem(storageKey, JSON.stringify({
|
|
105
|
+
scheme: selectedScheme,
|
|
106
|
+
token: authToken,
|
|
107
|
+
basicUser,
|
|
108
|
+
basicPass,
|
|
109
|
+
}))
|
|
110
|
+
} catch { /* ignore */ }
|
|
111
|
+
}, [storageKey, selectedScheme, authToken, basicUser, basicPass])
|
|
112
|
+
|
|
92
113
|
const [responseData, setResponseData] = useState<{
|
|
93
114
|
status: number; statusText: string; body: unknown; headers?: Record<string, string>; time: number
|
|
94
115
|
} | null>(null)
|
|
@@ -119,6 +140,7 @@ export function PlaygroundDialog({
|
|
|
119
140
|
setAuthToken('')
|
|
120
141
|
setBasicUser('')
|
|
121
142
|
setBasicPass('')
|
|
143
|
+
try { sessionStorage.removeItem(storageKey) } catch { /* ignore */ }
|
|
122
144
|
setHeaderValues({})
|
|
123
145
|
setPathValues({})
|
|
124
146
|
setQueryValues({})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Children, isValidElement, type ComponentProps } from 'react'
|
|
2
2
|
import styles from './paragraph.module.css'
|
|
3
3
|
|
|
4
|
-
const BLOCK_ELEMENTS = new Set(['summary', 'details', 'div', 'table', 'ul', 'ol'])
|
|
4
|
+
const BLOCK_ELEMENTS = new Set(['summary', 'details', 'div', 'table', 'ul', 'ol', 'p'])
|
|
5
5
|
|
|
6
6
|
function hasBlockChild(children: React.ReactNode): boolean {
|
|
7
7
|
return Children.toArray(children).some(
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { prefetchPageData } from '@/lib/preload';
|
|
3
|
+
|
|
4
|
+
const NO_PREFETCH_ATTR = 'data-no-prefetch';
|
|
5
|
+
|
|
6
|
+
function resolvePathname(href: string | null): string | null {
|
|
7
|
+
if (!href) return null;
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(href, location.href);
|
|
10
|
+
if (url.origin !== location.origin) return null;
|
|
11
|
+
return url.pathname;
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function PrefetchProvider({ children }: { children: React.ReactNode }) {
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const handleMouseOver = (e: MouseEvent) => {
|
|
20
|
+
const anchor = (e.target as HTMLElement).closest?.('a[href]');
|
|
21
|
+
if (!anchor || anchor.hasAttribute(NO_PREFETCH_ATTR)) return;
|
|
22
|
+
const pathname = resolvePathname(anchor.getAttribute('href'));
|
|
23
|
+
if (pathname) prefetchPageData(pathname);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const handleFocusIn = (e: FocusEvent) => {
|
|
27
|
+
const anchor = (e.target as HTMLElement).closest?.('a[href]');
|
|
28
|
+
if (!anchor || anchor.hasAttribute(NO_PREFETCH_ATTR)) return;
|
|
29
|
+
const pathname = resolvePathname(anchor.getAttribute('href'));
|
|
30
|
+
if (pathname) prefetchPageData(pathname);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
document.addEventListener('mouseover', handleMouseOver);
|
|
34
|
+
document.addEventListener('focusin', handleFocusIn);
|
|
35
|
+
|
|
36
|
+
const observer = new IntersectionObserver(
|
|
37
|
+
(entries) => {
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
if (entry.isIntersecting) {
|
|
40
|
+
const pathname = resolvePathname((entry.target as HTMLAnchorElement).getAttribute('href'));
|
|
41
|
+
if (pathname) prefetchPageData(pathname);
|
|
42
|
+
observer.unobserve(entry.target);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{ rootMargin: '200px' },
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const observeLinks = () => {
|
|
50
|
+
document.querySelectorAll(`a[href]:not([data-prefetch-observed]):not([${NO_PREFETCH_ATTR}])`).forEach((link) => {
|
|
51
|
+
const pathname = resolvePathname(link.getAttribute('href'));
|
|
52
|
+
if (pathname) {
|
|
53
|
+
link.setAttribute('data-prefetch-observed', '');
|
|
54
|
+
observer.observe(link);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const mutationObserver = new MutationObserver(observeLinks);
|
|
60
|
+
mutationObserver.observe(document.body, { childList: true, subtree: true });
|
|
61
|
+
observeLinks();
|
|
62
|
+
|
|
63
|
+
return () => {
|
|
64
|
+
document.removeEventListener('mouseover', handleMouseOver);
|
|
65
|
+
document.removeEventListener('focusin', handleFocusIn);
|
|
66
|
+
observer.disconnect();
|
|
67
|
+
mutationObserver.disconnect();
|
|
68
|
+
};
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
return children;
|
|
72
|
+
}
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
HashtagIcon,
|
|
4
4
|
MagnifyingGlassIcon
|
|
5
5
|
} from '@heroicons/react/24/outline';
|
|
6
|
-
import { Command, IconButton, Text } from '@raystack/apsara';
|
|
6
|
+
import { Badge, Command, IconButton, Text } from '@raystack/apsara';
|
|
7
7
|
import { debounce } from 'lodash-es';
|
|
8
8
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
9
9
|
import { useNavigate } from 'react-router';
|
|
@@ -18,6 +18,7 @@ interface SearchResult {
|
|
|
18
18
|
content: string;
|
|
19
19
|
match?: 'title' | 'heading' | 'body';
|
|
20
20
|
snippet?: string;
|
|
21
|
+
section?: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
interface SearchProps {
|
|
@@ -157,6 +158,7 @@ export function Search({ classNames }: SearchProps) {
|
|
|
157
158
|
html={stripMethod(result.content)}
|
|
158
159
|
/>
|
|
159
160
|
</Text>
|
|
161
|
+
{result.section && <Badge size="small" className={styles.sectionBadge}>{result.section}</Badge>}
|
|
160
162
|
</div>
|
|
161
163
|
</Command.Item>
|
|
162
164
|
))}
|
|
@@ -187,6 +189,7 @@ export function Search({ classNames }: SearchProps) {
|
|
|
187
189
|
</Text>
|
|
188
190
|
)}
|
|
189
191
|
</div>
|
|
192
|
+
{result.section && <Badge size="small" className={styles.sectionBadge}>{result.section}</Badge>}
|
|
190
193
|
</div>
|
|
191
194
|
</Command.Item>
|
|
192
195
|
))}
|
package/src/lib/env.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function substituteEnvVars(value: string): string {
|
|
2
|
+
return value.replace(/\$\{(\w+)\}/g, (_, name) => {
|
|
3
|
+
const val = process.env[name];
|
|
4
|
+
if (val === undefined) {
|
|
5
|
+
throw new Error(`Environment variable '${name}' is not set`);
|
|
6
|
+
}
|
|
7
|
+
return val;
|
|
8
|
+
});
|
|
9
|
+
}
|
package/src/lib/openapi.ts
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path'
|
|
|
3
3
|
import { parse as parseYaml } from 'yaml'
|
|
4
4
|
import type { OpenAPIV2, OpenAPIV3 } from 'openapi-types'
|
|
5
5
|
import type { ApiConfig, ApiServerConfig, ApiAuthConfig } from '@/types/config'
|
|
6
|
+
import { substituteEnvVars } from '@/lib/env'
|
|
6
7
|
|
|
7
8
|
type JsonObject = Record<string, unknown>
|
|
8
9
|
|
|
@@ -41,7 +42,7 @@ export async function loadApiSpec(config: ApiConfig, projectRoot: string): Promi
|
|
|
41
42
|
return {
|
|
42
43
|
name: config.name,
|
|
43
44
|
basePath: config.basePath,
|
|
44
|
-
server: config.server,
|
|
45
|
+
server: { ...config.server, url: substituteEnvVars(config.server.url) },
|
|
45
46
|
auth: config.auth,
|
|
46
47
|
document: v3Doc,
|
|
47
48
|
}
|
package/src/lib/page-context.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import { resolveRoute, RouteType } from '@/lib/route-resolver';
|
|
|
13
13
|
import type { VersionContext } from '@/lib/version-source';
|
|
14
14
|
import { LATEST_CONTEXT } from '@/lib/version-source';
|
|
15
15
|
import type { ChronicleConfig, Frontmatter, Page, PageNavLink, Root, TableOfContents } from '@/types';
|
|
16
|
+
import { queryClient } from '@/lib/preload';
|
|
16
17
|
|
|
17
18
|
export type MdxLoader = (relativePath: string) => Promise<{ content: ReactNode; toc: TableOfContents }>;
|
|
18
19
|
|
|
@@ -109,17 +110,22 @@ export function PageProvider({
|
|
|
109
110
|
frontmatter: Frontmatter;
|
|
110
111
|
relativePath: string;
|
|
111
112
|
originalPath?: string;
|
|
113
|
+
images?: string[];
|
|
112
114
|
prev?: PageNavLink | null;
|
|
113
115
|
next?: PageNavLink | null;
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
const fetchPageData = useCallback(async (slug: string[]): Promise<PageData> => {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
const key = slug.length === 0 ? '' : slug.map(s => encodeURIComponent(s)).join(',');
|
|
120
|
+
const apiPath = key ? `/api/page?slug=${key}` : '/api/page';
|
|
121
|
+
return queryClient.fetchQuery({
|
|
122
|
+
queryKey: ['pageData', key],
|
|
123
|
+
queryFn: async () => {
|
|
124
|
+
const res = await fetch(apiPath);
|
|
125
|
+
if (!res.ok) throw new Error(String(res.status));
|
|
126
|
+
return res.json();
|
|
127
|
+
},
|
|
128
|
+
});
|
|
123
129
|
}, []);
|
|
124
130
|
|
|
125
131
|
const loadDocsPage = useCallback(async (slug: string[], cancelled: { current: boolean }) => {
|
|
@@ -127,6 +133,12 @@ export function PageProvider({
|
|
|
127
133
|
try {
|
|
128
134
|
const data = await fetchPageData(slug);
|
|
129
135
|
if (cancelled.current) return;
|
|
136
|
+
if (data.images?.length) {
|
|
137
|
+
for (const src of data.images) {
|
|
138
|
+
const img = new Image();
|
|
139
|
+
img.src = src;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
130
142
|
const { content, toc } = await loadMdx(data.originalPath || data.relativePath);
|
|
131
143
|
if (cancelled.current) return;
|
|
132
144
|
setErrorStatus(null);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
2
|
+
|
|
3
|
+
export const queryClient = new QueryClient({
|
|
4
|
+
defaultOptions: {
|
|
5
|
+
queries: {
|
|
6
|
+
staleTime: Infinity,
|
|
7
|
+
refetchOnWindowFocus: false,
|
|
8
|
+
},
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export function pageDataQueryKey(pathname: string) {
|
|
13
|
+
const slug = pathname.split('/').filter(Boolean);
|
|
14
|
+
const key = slug.length === 0 ? '' : slug.map(s => encodeURIComponent(s)).join(',');
|
|
15
|
+
return ['pageData', key] as const;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function fetchPageDataByPathname(pathname: string) {
|
|
19
|
+
const slug = pathname.split('/').filter(Boolean);
|
|
20
|
+
const key = slug.length === 0 ? '' : slug.map(s => encodeURIComponent(s)).join(',');
|
|
21
|
+
const apiPath = key ? `/api/page?slug=${key}` : '/api/page';
|
|
22
|
+
const res = await fetch(apiPath);
|
|
23
|
+
if (!res.ok) throw new Error(String(res.status));
|
|
24
|
+
return res.json();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isApisRoute(pathname: string): boolean {
|
|
28
|
+
return pathname === '/apis' || pathname.startsWith('/apis/');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hasFileExtension(pathname: string): boolean {
|
|
32
|
+
const lastSegment = pathname.split('/').pop() ?? '';
|
|
33
|
+
return lastSegment.includes('.');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function prefetchPageData(pathname: string) {
|
|
37
|
+
if (isApisRoute(pathname) || hasFileExtension(pathname)) return;
|
|
38
|
+
queryClient.prefetchQuery({
|
|
39
|
+
queryKey: pageDataQueryKey(pathname),
|
|
40
|
+
queryFn: () => fetchPageDataByPathname(pathname),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { Plugin } from 'unified'
|
|
|
4
4
|
import type { Image, Html } from 'mdast'
|
|
5
5
|
import type { Element } from 'hast'
|
|
6
6
|
import type { MdxJsxFlowElement, MdxJsxTextElement, MdxJsxAttribute } from 'mdast-util-mdx-jsx'
|
|
7
|
+
import { MdxNodeType } from './mdx-utils'
|
|
7
8
|
|
|
8
9
|
function resolveUrl(src: string, dir: string): string {
|
|
9
10
|
if (/^[a-z][a-z0-9+\-.]*:/i.test(src)) return src
|
|
@@ -26,25 +27,40 @@ const remarkResolveImages: Plugin = () => {
|
|
|
26
27
|
const relative = filePath.slice(contentIdx + '/content/'.length)
|
|
27
28
|
const dir = path.posix.dirname(relative)
|
|
28
29
|
|
|
30
|
+
const seen = new Set<string>()
|
|
31
|
+
const images: string[] = []
|
|
32
|
+
|
|
33
|
+
function collect(src: string) {
|
|
34
|
+
if (!src || seen.has(src) || /^data:/i.test(src)) return
|
|
35
|
+
seen.add(src)
|
|
36
|
+
images.push(src)
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
visit(tree, 'image', (node: Image) => {
|
|
30
40
|
if (!node.url) return
|
|
31
41
|
node.url = resolveUrl(node.url, dir)
|
|
42
|
+
collect(node.url)
|
|
32
43
|
})
|
|
33
44
|
|
|
34
45
|
visit(tree, 'html', (node: Html) => {
|
|
35
46
|
node.value = node.value.replace(
|
|
36
47
|
/(<img\b[^>]*\bsrc=["'])([^"']+)(["'])/gi,
|
|
37
|
-
(_, before, src, after) =>
|
|
48
|
+
(_, before, src, after) => {
|
|
49
|
+
const resolved = resolveUrl(src, dir)
|
|
50
|
+
collect(resolved)
|
|
51
|
+
return `${before}${resolved}${after}`
|
|
52
|
+
}
|
|
38
53
|
)
|
|
39
54
|
})
|
|
40
55
|
|
|
41
56
|
visit(tree, (node) => {
|
|
42
|
-
if (node.type !==
|
|
57
|
+
if (node.type !== MdxNodeType.JsxFlow && node.type !== MdxNodeType.JsxText) return
|
|
43
58
|
const jsx = node as MdxJsxFlowElement | MdxJsxTextElement
|
|
44
59
|
if (jsx.name !== 'img') return
|
|
45
60
|
const srcAttr = jsx.attributes.find((a): a is MdxJsxAttribute => a.type === 'mdxJsxAttribute' && a.name === 'src')
|
|
46
61
|
if (!srcAttr?.value || typeof srcAttr.value !== 'string') return
|
|
47
62
|
srcAttr.value = resolveUrl(srcAttr.value, dir)
|
|
63
|
+
collect(srcAttr.value)
|
|
48
64
|
})
|
|
49
65
|
|
|
50
66
|
visit(tree, 'element', (node: Element) => {
|
|
@@ -52,7 +68,10 @@ const remarkResolveImages: Plugin = () => {
|
|
|
52
68
|
const src = node.properties?.src
|
|
53
69
|
if (typeof src !== 'string') return
|
|
54
70
|
node.properties.src = resolveUrl(src, dir)
|
|
71
|
+
collect(node.properties.src as string)
|
|
55
72
|
})
|
|
73
|
+
|
|
74
|
+
file.data.images = images
|
|
56
75
|
}
|
|
57
76
|
}
|
|
58
77
|
|
package/src/lib/source.ts
CHANGED
|
@@ -37,6 +37,11 @@ const readingTimeGlob: Record<string, { text: string; minutes: number; words: nu
|
|
|
37
37
|
{ eager: true, import: 'readingTime' }
|
|
38
38
|
);
|
|
39
39
|
|
|
40
|
+
const imagesGlob: Record<string, string[] | undefined> = import.meta.glob(
|
|
41
|
+
'../../.content/**/*.{mdx,md}',
|
|
42
|
+
{ eager: true, import: 'images' }
|
|
43
|
+
);
|
|
44
|
+
|
|
40
45
|
const metaGlob: Record<string, Record<string, unknown>> = import.meta.glob(
|
|
41
46
|
'../../.content/**/meta.json',
|
|
42
47
|
{ eager: true }
|
|
@@ -54,10 +59,11 @@ function buildFiles() {
|
|
|
54
59
|
const relativePath = originalPath.replace(/readme\.(mdx?)$/i, 'index.$1');
|
|
55
60
|
const rt = readingTimeGlob[key];
|
|
56
61
|
const _readingTime = rt?.minutes != null ? Math.max(1, Math.round(rt.minutes)) : undefined;
|
|
62
|
+
const _images = imagesGlob[key] ?? [];
|
|
57
63
|
files.push({
|
|
58
64
|
type: 'page',
|
|
59
65
|
path: relativePath,
|
|
60
|
-
data: { ...data, _readingTime, _relativePath: relativePath, _originalPath: originalPath }
|
|
66
|
+
data: { ...data, _readingTime, _images, _relativePath: relativePath, _originalPath: originalPath }
|
|
61
67
|
});
|
|
62
68
|
}
|
|
63
69
|
|
|
@@ -174,17 +180,31 @@ function sortTreeByOrder(tree: Root, pages: { url: string; data: unknown }[], me
|
|
|
174
180
|
return { ...tree, children: sortNodes(tree.children, pageOrderMap, folderOrderMap) };
|
|
175
181
|
}
|
|
176
182
|
|
|
183
|
+
function filterDraftsFromTree(tree: Root, draftUrls: Set<string>): Root {
|
|
184
|
+
function filterNodes(nodes: Node[]): Node[] {
|
|
185
|
+
return nodes
|
|
186
|
+
.filter(n => n.type !== NodeType.Page || !draftUrls.has(n.url))
|
|
187
|
+
.map(n => n.type === NodeType.Folder
|
|
188
|
+
? { ...n, children: filterNodes(n.children) } as Folder
|
|
189
|
+
: n
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return { ...tree, children: filterNodes(tree.children) };
|
|
193
|
+
}
|
|
194
|
+
|
|
177
195
|
export async function getPageTree(): Promise<Root> {
|
|
178
196
|
if (cachedTree) return cachedTree;
|
|
179
197
|
const s = await getSource();
|
|
180
198
|
const metaFiles = buildFiles().filter(f => f.type === 'meta') as { path: string; data: Record<string, unknown> }[];
|
|
181
|
-
|
|
199
|
+
const sorted = sortTreeByOrder(s.pageTree as Root, s.getPages(), metaFiles);
|
|
200
|
+
const draftUrls = new Set(s.getPages().filter(p => isDraft(p)).map(p => p.url));
|
|
201
|
+
cachedTree = draftUrls.size > 0 ? filterDraftsFromTree(sorted, draftUrls) : sorted;
|
|
182
202
|
return cachedTree;
|
|
183
203
|
}
|
|
184
204
|
|
|
185
205
|
export async function getPages() {
|
|
186
206
|
const s = await getSource();
|
|
187
|
-
return s.getPages();
|
|
207
|
+
return s.getPages().filter(p => !isDraft(p));
|
|
188
208
|
}
|
|
189
209
|
|
|
190
210
|
export async function getPage(slugs?: string[]) {
|
|
@@ -254,10 +274,15 @@ export function extractFrontmatter(page: { data: unknown }, fallbackTitle?: stri
|
|
|
254
274
|
order: d.order as number | undefined,
|
|
255
275
|
icon: d.icon as string | undefined,
|
|
256
276
|
lastModified: d.lastModified as string | undefined,
|
|
277
|
+
draft: d.draft as boolean | undefined,
|
|
257
278
|
_readingTime: d._readingTime as number | undefined,
|
|
258
279
|
};
|
|
259
280
|
}
|
|
260
281
|
|
|
282
|
+
export function isDraft(page: { data: unknown }): boolean {
|
|
283
|
+
return (page.data as Record<string, unknown>).draft === true;
|
|
284
|
+
}
|
|
285
|
+
|
|
261
286
|
export function getRelativePath(page: { data: unknown }): string {
|
|
262
287
|
return ((page.data as Record<string, unknown>)._relativePath as string) ?? '';
|
|
263
288
|
}
|
|
@@ -266,6 +291,10 @@ export function getOriginalPath(page: { data: unknown }): string {
|
|
|
266
291
|
return ((page.data as Record<string, unknown>)._originalPath as string) ?? '';
|
|
267
292
|
}
|
|
268
293
|
|
|
294
|
+
export function getPageImages(page: { data: unknown }): string[] {
|
|
295
|
+
return ((page.data as Record<string, unknown>)._images as string[]) ?? [];
|
|
296
|
+
}
|
|
297
|
+
|
|
269
298
|
export async function getPageSearchContent(page: { data: unknown }): Promise<{ headings: string; body: string }> {
|
|
270
299
|
const originalPath = getOriginalPath(page);
|
|
271
300
|
if (!originalPath) return { headings: '', body: '' };
|
package/src/pages/DocsLayout.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import { useLocation } from 'react-router';
|
|
3
|
+
import { PrefetchProvider } from '@/components/ui/PrefetchProvider';
|
|
3
4
|
import { usePageContext } from '@/lib/page-context';
|
|
4
5
|
import { getActiveContentDir } from '@/lib/navigation';
|
|
5
6
|
import {
|
|
@@ -27,13 +28,15 @@ export function DocsLayout({ children, hideSidebar }: DocsLayoutProps) {
|
|
|
27
28
|
);
|
|
28
29
|
|
|
29
30
|
return (
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
<PrefetchProvider>
|
|
32
|
+
<Layout
|
|
33
|
+
config={config}
|
|
34
|
+
tree={scopedTree}
|
|
35
|
+
hideSidebar={hideSidebar}
|
|
36
|
+
classNames={{ layout: className }}
|
|
37
|
+
>
|
|
38
|
+
{children}
|
|
39
|
+
</Layout>
|
|
40
|
+
</PrefetchProvider>
|
|
38
41
|
);
|
|
39
42
|
}
|
package/src/server/App.tsx
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import '@raystack/apsara/normalize.css';
|
|
2
2
|
import '@raystack/apsara/style.css';
|
|
3
|
-
import { ThemeProvider } from '@raystack/apsara';
|
|
3
|
+
import { ThemeProvider, Skeleton, Flex } from '@raystack/apsara';
|
|
4
|
+
import { lazy, Suspense } from 'react';
|
|
4
5
|
import { Navigate, useLocation } from 'react-router';
|
|
5
6
|
import { Head } from '@/lib/head';
|
|
6
7
|
import { usePageContext } from '@/lib/page-context';
|
|
7
8
|
import { resolveRoute, RouteType } from '@/lib/route-resolver';
|
|
8
|
-
import { ApiLayout } from '@/pages/ApiLayout';
|
|
9
|
-
import { ApiPage } from '@/pages/ApiPage';
|
|
10
|
-
import { DocsLayout } from '@/pages/DocsLayout';
|
|
11
|
-
import { DocsPage } from '@/pages/DocsPage';
|
|
12
|
-
import { LandingPage } from '@/pages/LandingPage';
|
|
13
9
|
import type { ChronicleConfig } from '@/types';
|
|
14
10
|
import { getThemeConfig } from '@/themes/registry';
|
|
11
|
+
import styles from './App.module.css';
|
|
12
|
+
|
|
13
|
+
const ApiLayout = lazy(() => import('@/pages/ApiLayout').then(m => ({ default: m.ApiLayout })));
|
|
14
|
+
const ApiPage = lazy(() => import('@/pages/ApiPage').then(m => ({ default: m.ApiPage })));
|
|
15
|
+
const DocsLayout = lazy(() => import('@/pages/DocsLayout').then(m => ({ default: m.DocsLayout })));
|
|
16
|
+
const DocsPage = lazy(() => import('@/pages/DocsPage').then(m => ({ default: m.DocsPage })));
|
|
17
|
+
const LandingPage = lazy(() => import('@/pages/LandingPage').then(m => ({ default: m.LandingPage })));
|
|
15
18
|
|
|
16
19
|
export function App() {
|
|
17
20
|
const { pathname } = useLocation();
|
|
@@ -35,19 +38,33 @@ export function App() {
|
|
|
35
38
|
forcedTheme={themeConfig.forcedTheme}
|
|
36
39
|
>
|
|
37
40
|
<RootHead config={config} />
|
|
38
|
-
{
|
|
39
|
-
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
<Suspense fallback={<PageFallback />}>
|
|
42
|
+
{isApi ? (
|
|
43
|
+
<ApiLayout>
|
|
44
|
+
<ApiPage slug={apiSlug} />
|
|
45
|
+
</ApiLayout>
|
|
46
|
+
) : (
|
|
47
|
+
<DocsLayout hideSidebar={isLanding}>
|
|
48
|
+
{isLanding ? <LandingPage /> : <DocsPage slug={docsSlug} />}
|
|
49
|
+
</DocsLayout>
|
|
50
|
+
)}
|
|
51
|
+
</Suspense>
|
|
47
52
|
</ThemeProvider>
|
|
48
53
|
);
|
|
49
54
|
}
|
|
50
55
|
|
|
56
|
+
function PageFallback() {
|
|
57
|
+
return (
|
|
58
|
+
<Flex direction="column" gap={4} className={styles.fallback}>
|
|
59
|
+
<Skeleton width="40%" height="var(--rs-line-height-t2)" />
|
|
60
|
+
<Skeleton width="60%" height="var(--rs-line-height-regular)" />
|
|
61
|
+
{[...new Array(12)].map((_, i) => (
|
|
62
|
+
<Skeleton key={i} width="100%" height="var(--rs-line-height-regular)" />
|
|
63
|
+
))}
|
|
64
|
+
</Flex>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
51
68
|
function RootHead({ config }: { config: ChronicleConfig }) {
|
|
52
69
|
return (
|
|
53
70
|
<Head
|
package/src/server/api/page.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { defineHandler, HTTPError } from 'nitro';
|
|
2
|
-
import { getPage, getPageNav, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source';
|
|
2
|
+
import { getPage, getPageNav, extractFrontmatter, getRelativePath, getOriginalPath, getPageImages, isDraft } from '@/lib/source';
|
|
3
3
|
|
|
4
4
|
export default defineHandler(async event => {
|
|
5
5
|
const slugParam = event.url.searchParams.get('slug') ?? '';
|
|
6
6
|
const slug = slugParam ? slugParam.split(',').filter(Boolean) : [];
|
|
7
7
|
const page = await getPage(slug);
|
|
8
8
|
|
|
9
|
-
if (!page) {
|
|
9
|
+
if (!page || isDraft(page)) {
|
|
10
10
|
throw new HTTPError({ status: 404, message: 'Page not found' });
|
|
11
11
|
}
|
|
12
12
|
|
|
@@ -16,6 +16,7 @@ export default defineHandler(async event => {
|
|
|
16
16
|
frontmatter: extractFrontmatter(page, slug[slug.length - 1]),
|
|
17
17
|
relativePath: getRelativePath(page),
|
|
18
18
|
originalPath: getOriginalPath(page),
|
|
19
|
+
images: getPageImages(page),
|
|
19
20
|
prev: nav.prev,
|
|
20
21
|
next: nav.next,
|
|
21
22
|
});
|
package/src/server/api/search.ts
CHANGED
|
@@ -14,6 +14,7 @@ interface SearchDocument {
|
|
|
14
14
|
headings: string;
|
|
15
15
|
body: string;
|
|
16
16
|
type: 'page' | 'api';
|
|
17
|
+
section: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
import fs from 'node:fs/promises';
|
|
@@ -61,7 +62,8 @@ async function buildIndex(ctx: VersionContext, key: string) {
|
|
|
61
62
|
headings TEXT NOT NULL,
|
|
62
63
|
body TEXT NOT NULL,
|
|
63
64
|
type TEXT NOT NULL,
|
|
64
|
-
version TEXT NOT NULL
|
|
65
|
+
version TEXT NOT NULL,
|
|
66
|
+
section TEXT NOT NULL DEFAULT ''
|
|
65
67
|
)`);
|
|
66
68
|
|
|
67
69
|
await db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS search_fts USING fts5(
|
|
@@ -74,8 +76,8 @@ async function buildIndex(ctx: VersionContext, key: string) {
|
|
|
74
76
|
|
|
75
77
|
const docs = await buildDocs(ctx);
|
|
76
78
|
for (const doc of docs) {
|
|
77
|
-
await db.sql`INSERT INTO search_docs (id, url, title, headings, body, type, version)
|
|
78
|
-
VALUES (${doc.id}, ${doc.url}, ${doc.title}, ${doc.headings}, ${doc.body}, ${doc.type}, ${key})`;
|
|
79
|
+
await db.sql`INSERT INTO search_docs (id, url, title, headings, body, type, version, section)
|
|
80
|
+
VALUES (${doc.id}, ${doc.url}, ${doc.title}, ${doc.headings}, ${doc.body}, ${doc.type}, ${key}, ${doc.section})`;
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
await db.sql`INSERT INTO search_fts (rowid, title, headings, body)
|
|
@@ -86,11 +88,15 @@ async function buildIndex(ctx: VersionContext, key: string) {
|
|
|
86
88
|
|
|
87
89
|
async function buildDocs(ctx: VersionContext): Promise<SearchDocument[]> {
|
|
88
90
|
const docs: SearchDocument[] = [];
|
|
91
|
+
const config = loadConfig();
|
|
92
|
+
const contentEntries = config.content ?? [];
|
|
89
93
|
|
|
90
94
|
const pages = await getPagesForVersion(ctx);
|
|
91
95
|
for (const p of pages) {
|
|
92
96
|
const fm = extractFrontmatter(p);
|
|
93
97
|
const { headings, body } = await getPageSearchContent(p);
|
|
98
|
+
const dir = p.url.replace(/^\//, '').split('/')[0];
|
|
99
|
+
const entry = contentEntries.find(c => c.dir === dir);
|
|
94
100
|
docs.push({
|
|
95
101
|
id: p.url,
|
|
96
102
|
url: p.url,
|
|
@@ -98,10 +104,10 @@ async function buildDocs(ctx: VersionContext): Promise<SearchDocument[]> {
|
|
|
98
104
|
headings,
|
|
99
105
|
body: [fm.description ?? '', body].join(' '),
|
|
100
106
|
type: 'page',
|
|
107
|
+
section: entry?.label ?? dir ?? '',
|
|
101
108
|
});
|
|
102
109
|
}
|
|
103
110
|
|
|
104
|
-
const config = loadConfig();
|
|
105
111
|
const apiConfigs = getApiConfigsForVersion(config, ctx.dir);
|
|
106
112
|
if (apiConfigs.length) {
|
|
107
113
|
const specs = await loadApiSpecs(apiConfigs);
|
|
@@ -122,6 +128,7 @@ async function buildDocs(ctx: VersionContext): Promise<SearchDocument[]> {
|
|
|
122
128
|
headings: op.summary ?? opId,
|
|
123
129
|
body: [op.description ?? '', pathStr, method.toUpperCase()].join(' '),
|
|
124
130
|
type: 'api',
|
|
131
|
+
section: spec.name,
|
|
125
132
|
});
|
|
126
133
|
}
|
|
127
134
|
}
|
|
@@ -183,7 +190,7 @@ export default defineHandler(async event => {
|
|
|
183
190
|
const key = versionKey(ctx);
|
|
184
191
|
|
|
185
192
|
if (!query) {
|
|
186
|
-
const result = await db.sql`SELECT id, url, title, type FROM search_docs
|
|
193
|
+
const result = await db.sql`SELECT id, url, title, type, section FROM search_docs
|
|
187
194
|
WHERE version = ${key} AND type = 'page'
|
|
188
195
|
LIMIT 8`;
|
|
189
196
|
return Response.json((result.rows ?? []).map(r => ({
|
|
@@ -191,11 +198,12 @@ export default defineHandler(async event => {
|
|
|
191
198
|
url: r.url,
|
|
192
199
|
type: r.type,
|
|
193
200
|
content: r.title,
|
|
201
|
+
section: r.section || null,
|
|
194
202
|
})));
|
|
195
203
|
}
|
|
196
204
|
|
|
197
205
|
const searchTerm = query.split(/\s+/).map(t => `"${t}"*`).join(' ');
|
|
198
|
-
const result = await db.sql`SELECT s.id, s.url, s.title, s.headings, s.body, s.type,
|
|
206
|
+
const result = await db.sql`SELECT s.id, s.url, s.title, s.headings, s.body, s.type, s.section,
|
|
199
207
|
bm25(search_fts, 10.0, 5.0, 1.0) AS score
|
|
200
208
|
FROM search_fts f
|
|
201
209
|
JOIN search_docs s ON s.rowid = f.rowid
|
|
@@ -214,6 +222,8 @@ export default defineHandler(async event => {
|
|
|
214
222
|
content: r.title,
|
|
215
223
|
match,
|
|
216
224
|
snippet,
|
|
225
|
+
section: r.section || null,
|
|
217
226
|
};
|
|
218
227
|
}));
|
|
219
228
|
});
|
|
229
|
+
|
|
@@ -3,9 +3,11 @@ import React from 'react';
|
|
|
3
3
|
import { hydrateRoot } from 'react-dom/client';
|
|
4
4
|
import { BrowserRouter } from 'react-router';
|
|
5
5
|
import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
|
|
6
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
6
7
|
import { mdxComponents } from '@/components/mdx';
|
|
7
8
|
import { getApiConfigsForVersion } from '@/lib/config';
|
|
8
9
|
import { PageProvider } from '@/lib/page-context';
|
|
10
|
+
import { queryClient } from '@/lib/preload';
|
|
9
11
|
import { resolveRoute, RouteType } from '@/lib/route-resolver';
|
|
10
12
|
import { resolveVersionFromUrl, type VersionContext } from '@/lib/version-source';
|
|
11
13
|
import type { ChronicleConfig, Frontmatter, PageNavLink, Root, TableOfContents } from '@/types';
|
|
@@ -93,20 +95,22 @@ async function hydrate() {
|
|
|
93
95
|
|
|
94
96
|
hydrateRoot(
|
|
95
97
|
document.getElementById('root') as HTMLElement,
|
|
96
|
-
<
|
|
97
|
-
<
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
98
|
+
<QueryClientProvider client={queryClient}>
|
|
99
|
+
<BrowserRouter>
|
|
100
|
+
<ReactRouterProvider>
|
|
101
|
+
<PageProvider
|
|
102
|
+
initialConfig={config}
|
|
103
|
+
initialTree={tree}
|
|
104
|
+
initialPage={page}
|
|
105
|
+
initialApiSpecs={apiSpecs}
|
|
106
|
+
initialVersion={version}
|
|
107
|
+
loadMdx={loadMdxModule}
|
|
108
|
+
>
|
|
109
|
+
<App />
|
|
110
|
+
</PageProvider>
|
|
111
|
+
</ReactRouterProvider>
|
|
112
|
+
</BrowserRouter>
|
|
113
|
+
</QueryClientProvider>
|
|
110
114
|
);
|
|
111
115
|
} catch (err) {
|
|
112
116
|
console.error('Hydration failed:', err);
|
|
@@ -9,7 +9,7 @@ import { getApiConfigsForVersion, loadConfig } from '@/lib/config';
|
|
|
9
9
|
import { loadApiSpecs } from '@/lib/openapi';
|
|
10
10
|
import { PageProvider } from '@/lib/page-context';
|
|
11
11
|
import { resolveRoute, RouteType } from '@/lib/route-resolver';
|
|
12
|
-
import { getPageTree, getPage, getPageNav, loadPageModule, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source';
|
|
12
|
+
import { getPageTree, getPage, getPageNav, loadPageModule, extractFrontmatter, getRelativePath, getOriginalPath, getPageImages, isDraft } from '@/lib/source';
|
|
13
13
|
import { getFirstApiUrl } from '@/lib/api-routes';
|
|
14
14
|
import { StatusCodes } from 'http-status-codes';
|
|
15
15
|
import { resolveDocsRedirect } from '@/lib/tree-utils';
|
|
@@ -44,10 +44,12 @@ export default {
|
|
|
44
44
|
: [];
|
|
45
45
|
const apiSpecs = apiConfigs.length ? await loadApiSpecs(apiConfigs) : [];
|
|
46
46
|
|
|
47
|
-
const [tree,
|
|
47
|
+
const [tree, rawPage] = await Promise.all([
|
|
48
48
|
getPageTree(),
|
|
49
49
|
route.type === RouteType.DocsPage ? getPage(route.slug) : Promise.resolve(null),
|
|
50
50
|
]);
|
|
51
|
+
const page = rawPage && isDraft(rawPage) ? null : rawPage;
|
|
52
|
+
|
|
51
53
|
// SSR redirects for index pages
|
|
52
54
|
if (route.type === RouteType.ApiIndex) {
|
|
53
55
|
const firstUrl = getFirstApiUrl(apiSpecs);
|
|
@@ -77,6 +79,7 @@ export default {
|
|
|
77
79
|
const relativePath = page ? getRelativePath(page) : null;
|
|
78
80
|
const originalPath = page ? getOriginalPath(page) : null;
|
|
79
81
|
const mdxModule = (originalPath || relativePath) ? await loadPageModule(originalPath || relativePath!) : null;
|
|
82
|
+
const pageImages = page ? getPageImages(page) : [];
|
|
80
83
|
|
|
81
84
|
const pageData = page
|
|
82
85
|
? {
|
|
@@ -123,6 +126,9 @@ export default {
|
|
|
123
126
|
{assets.js.map((attr: { href: string }) => (
|
|
124
127
|
<link key={attr.href} rel="modulepreload" {...attr} />
|
|
125
128
|
))}
|
|
129
|
+
{pageImages.map((src: string) => (
|
|
130
|
+
<link key={src} rel="preload" as="image" href={src} />
|
|
131
|
+
))}
|
|
126
132
|
<script type="module" src={assets.entry} />
|
|
127
133
|
<script dangerouslySetInnerHTML={{ __html: `window.__PAGE_DATA__ = ${safeJson}` }} />
|
|
128
134
|
</head>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineErrorHandler, HTTPError } from 'nitro';
|
|
2
|
+
|
|
3
|
+
export default defineErrorHandler((error, _event) => {
|
|
4
|
+
const status = HTTPError.isError(error) ? error.status : 500;
|
|
5
|
+
const message = error.message || 'Internal Server Error';
|
|
6
|
+
|
|
7
|
+
return new Response(JSON.stringify({ error: true, status, message }), {
|
|
8
|
+
status,
|
|
9
|
+
headers: { 'Content-Type': 'application/json' },
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -72,7 +72,7 @@ export async function createViteConfig(
|
|
|
72
72
|
default: defineFumadocsConfig({
|
|
73
73
|
mdxOptions: {
|
|
74
74
|
remarkImageOptions: false,
|
|
75
|
-
valueToExport: ['readingTime'],
|
|
75
|
+
valueToExport: ['readingTime', 'images'],
|
|
76
76
|
remarkPlugins: [
|
|
77
77
|
remarkDirective,
|
|
78
78
|
[remarkDirectiveAdmonition, {
|
|
@@ -145,6 +145,7 @@ export async function createViteConfig(
|
|
|
145
145
|
},
|
|
146
146
|
nitro: {
|
|
147
147
|
logLevel: 2,
|
|
148
|
+
errorHandler: path.resolve(packageRoot, 'src/server/error.ts'),
|
|
148
149
|
publicAssets: [{ dir: path.resolve(projectRoot, 'public') }],
|
|
149
150
|
output: {
|
|
150
151
|
dir: resolveOutputDir(projectRoot, preset),
|
|
@@ -9,12 +9,13 @@ import {
|
|
|
9
9
|
import { Flex, IconButton, Button, Sidebar } from '@raystack/apsara';
|
|
10
10
|
import { PlayIcon } from '@radix-ui/react-icons';
|
|
11
11
|
import { cx } from 'class-variance-authority';
|
|
12
|
-
import { useState, useEffect, useMemo, useRef } from 'react';
|
|
12
|
+
import { useState, useEffect, useMemo, useRef, lazy, Suspense } from 'react';
|
|
13
13
|
import { Link as RouterLink, useLocation, useNavigate } from 'react-router';
|
|
14
14
|
import type { OpenAPIV3 } from 'openapi-types';
|
|
15
15
|
import { MethodBadge } from '@/components/api/method-badge';
|
|
16
16
|
import { useApiOperation } from '@/lib/use-api-operation';
|
|
17
|
-
|
|
17
|
+
|
|
18
|
+
const PlaygroundDialog = lazy(() => import('@/components/api/playground-dialog').then(m => ({ default: m.PlaygroundDialog })));
|
|
18
19
|
import { ClientThemeSwitcher } from '@/components/ui/client-theme-switcher';
|
|
19
20
|
import { Search } from '@/components/ui/search';
|
|
20
21
|
import { Breadcrumbs } from '@/components/ui/breadcrumbs';
|
|
@@ -137,14 +138,14 @@ export function Layout({
|
|
|
137
138
|
<DocumentTextIcon width={16} height={16} />
|
|
138
139
|
)}
|
|
139
140
|
classNames={{ root: styles.topLinkItem, text: styles.topLinkText }}
|
|
140
|
-
render={<RouterLink to={entry.href} />}
|
|
141
|
+
render={<RouterLink to={entry.href} data-no-prefetch />}
|
|
141
142
|
>
|
|
142
143
|
{entry.label}
|
|
143
144
|
</Sidebar.Item>
|
|
144
145
|
))}
|
|
145
146
|
{apiEntries.map(api => (
|
|
146
147
|
<Sidebar.Item
|
|
147
|
-
key={api.basePath}
|
|
148
|
+
key={`${api.basePath}-${api.name}`}
|
|
148
149
|
href={api.basePath}
|
|
149
150
|
active={isApiBase(api.basePath)}
|
|
150
151
|
leadingIcon={renderConfigIcon(
|
|
@@ -153,7 +154,7 @@ export function Layout({
|
|
|
153
154
|
<CodeBracketSquareIcon width={16} height={16} />
|
|
154
155
|
)}
|
|
155
156
|
classNames={{ root: styles.topLinkItem, text: styles.topLinkText }}
|
|
156
|
-
render={<RouterLink to={api.basePath} />}
|
|
157
|
+
render={<RouterLink to={api.basePath} data-no-prefetch />}
|
|
157
158
|
>
|
|
158
159
|
{api.name} API
|
|
159
160
|
</Sidebar.Item>
|
|
@@ -371,18 +372,22 @@ function TestRequestButton() {
|
|
|
371
372
|
>
|
|
372
373
|
Test request
|
|
373
374
|
</Button>
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
375
|
+
{open && (
|
|
376
|
+
<Suspense fallback={null}>
|
|
377
|
+
<PlaygroundDialog
|
|
378
|
+
key={`${match.spec.name}-${match.path}-${match.method}`}
|
|
379
|
+
open={open}
|
|
380
|
+
onOpenChange={setOpen}
|
|
381
|
+
method={match.method}
|
|
382
|
+
path={match.path}
|
|
383
|
+
operation={match.operation}
|
|
384
|
+
serverUrl={match.spec.server.url}
|
|
385
|
+
specName={match.spec.name}
|
|
386
|
+
auth={match.spec.auth}
|
|
387
|
+
document={match.spec.document}
|
|
388
|
+
/>
|
|
389
|
+
</Suspense>
|
|
390
|
+
)}
|
|
386
391
|
</>
|
|
387
392
|
);
|
|
388
393
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Flex, Headline } from '@raystack/apsara';
|
|
4
|
+
import { lazy, Suspense } from 'react';
|
|
4
5
|
import type { ThemePageProps } from '@/types';
|
|
5
6
|
import styles from './Page.module.css';
|
|
6
|
-
|
|
7
|
+
|
|
8
|
+
const Toc = lazy(() => import('./Toc').then(m => ({ default: m.Toc })));
|
|
7
9
|
|
|
8
10
|
export function Page({ page }: ThemePageProps) {
|
|
9
11
|
return (
|
|
@@ -16,7 +18,9 @@ export function Page({ page }: ThemePageProps) {
|
|
|
16
18
|
)}
|
|
17
19
|
<div className={styles.content}>{page.content}</div>
|
|
18
20
|
</article>
|
|
19
|
-
<
|
|
21
|
+
<Suspense fallback={null}>
|
|
22
|
+
<Toc items={page.toc} />
|
|
23
|
+
</Suspense>
|
|
20
24
|
</Flex>
|
|
21
25
|
);
|
|
22
26
|
}
|