@clidey/dory 0.34.0 → 0.35.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/index.html +0 -7
- package/package.json +40 -40
- package/src/components/error-boundary.tsx +59 -0
- package/src/components/routes.tsx +20 -3
- package/src/components/search-index.ts +34 -23
- package/src/components/store.ts +22 -22
- package/src/main.tsx +4 -1
- package/src/mdx/latex.tsx +63 -6
- package/src/mdx/mdx.tsx +17 -2
- package/src/plugins/analytics-injector.ts +43 -0
- package/src/plugins/csp-generator.ts +68 -0
- package/src/plugins/docs-assets.ts +1 -6
- package/src/plugins/frontmatter-dev-server.ts +8 -1
- package/src/plugins/frontmatter-generator.ts +12 -7
- package/src/plugins/llm-txt-dev-server.ts +8 -1
- package/src/plugins/llm-txt-generator.ts +8 -1
- package/src/plugins/prerender.ts +119 -0
- package/src/plugins/robots-generator.ts +37 -0
- package/src/plugins/sitemap-generator.ts +73 -0
- package/src/types/config.ts +9 -0
- package/src/ui/header.tsx +3 -8
- package/src/ui/layout.tsx +63 -5
- package/vite.config.ts +23 -3
- package/bin/dist/dory.d.ts +0 -3
- package/bin/dist/dory.d.ts.map +0 -1
- package/bin/dist/dory.js +0 -486
- package/bin/dist/dory.js.map +0 -1
- package/src/plugins/manifest-generator.ts +0 -57
package/index.html
CHANGED
|
@@ -38,13 +38,6 @@
|
|
|
38
38
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
39
39
|
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Inter&display=swap" rel="stylesheet">
|
|
40
40
|
|
|
41
|
-
<script>
|
|
42
|
-
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init be ys Ss me gs ws capture Ne calculateEventProperties xs register register_once register_for_session unregister unregister_for_session Rs getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSurveysLoaded onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey canRenderSurveyAsync identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty Is ks createPersonProfile Ps bs opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing $s debug Es getPageViewId captureTraceFeedback captureTraceMetric".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
|
|
43
|
-
posthog.init('phc_hxwfcMiIgSXYKkbWl9zpQE0HaPPmlRaVXCWODricYu1', {
|
|
44
|
-
api_host: 'https://us.i.posthog.com',
|
|
45
|
-
person_profiles: 'identified_only', // or 'always' to create profiles for anonymous users as well
|
|
46
|
-
})
|
|
47
|
-
</script>
|
|
48
41
|
</head>
|
|
49
42
|
<body>
|
|
50
43
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clidey/dory",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.35.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A CLI tool for building and previewing documentation sites",
|
|
7
7
|
"keywords": [
|
|
@@ -45,47 +45,47 @@
|
|
|
45
45
|
"prepublishOnly": "echo 'Skipping prepublishOnly - build handled by publish script'"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@algolia/autocomplete-core": "
|
|
49
|
-
"@clidey/ux": "
|
|
50
|
-
"@fortawesome/fontawesome-svg-core": "
|
|
51
|
-
"@fortawesome/free-brands-svg-icons": "
|
|
52
|
-
"@fortawesome/free-regular-svg-icons": "
|
|
53
|
-
"@fortawesome/free-solid-svg-icons": "
|
|
54
|
-
"@fortawesome/react-fontawesome": "
|
|
55
|
-
"@headlessui/react": "
|
|
56
|
-
"@heroicons/react": "
|
|
57
|
-
"@mdx-js/mdx": "
|
|
58
|
-
"@mdx-js/preact": "
|
|
59
|
-
"@mdx-js/rollup": "
|
|
60
|
-
"@preact/preset-vite": "
|
|
61
|
-
"@tailwindcss/vite": "
|
|
62
|
-
"@types/react-highlight-words": "
|
|
63
|
-
"classnames": "
|
|
64
|
-
"flexsearch": "
|
|
65
|
-
"gray-matter": "
|
|
66
|
-
"gzip-size-cli": "
|
|
67
|
-
"katex": "
|
|
68
|
-
"lucide-react": "
|
|
69
|
-
"mermaid": "
|
|
70
|
-
"motion": "
|
|
71
|
-
"preact": "
|
|
72
|
-
"prism-react-renderer": "
|
|
73
|
-
"react-highlight-words": "
|
|
74
|
-
"remark-gfm": "
|
|
75
|
-
"saas": "
|
|
76
|
-
"sass-embedded": "
|
|
77
|
-
"sirv": "
|
|
78
|
-
"tailwindcss": "
|
|
79
|
-
"
|
|
80
|
-
"tsx": "^4.20.4",
|
|
48
|
+
"@algolia/autocomplete-core": "1.19.2",
|
|
49
|
+
"@clidey/ux": "0.36.0",
|
|
50
|
+
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
|
51
|
+
"@fortawesome/free-brands-svg-icons": "6.7.2",
|
|
52
|
+
"@fortawesome/free-regular-svg-icons": "6.7.2",
|
|
53
|
+
"@fortawesome/free-solid-svg-icons": "6.7.2",
|
|
54
|
+
"@fortawesome/react-fontawesome": "0.2.2",
|
|
55
|
+
"@headlessui/react": "2.2.9",
|
|
56
|
+
"@heroicons/react": "2.2.0",
|
|
57
|
+
"@mdx-js/mdx": "3.1.0",
|
|
58
|
+
"@mdx-js/preact": "3.1.0",
|
|
59
|
+
"@mdx-js/rollup": "3.1.0",
|
|
60
|
+
"@preact/preset-vite": "2.10.1",
|
|
61
|
+
"@tailwindcss/vite": "4.1.16",
|
|
62
|
+
"@types/react-highlight-words": "0.20.0",
|
|
63
|
+
"classnames": "2.5.1",
|
|
64
|
+
"flexsearch": "0.8.205",
|
|
65
|
+
"gray-matter": "4.0.3",
|
|
66
|
+
"gzip-size-cli": "5.1.0",
|
|
67
|
+
"katex": "0.16.22",
|
|
68
|
+
"lucide-react": "0.513.0",
|
|
69
|
+
"mermaid": "11.6.0",
|
|
70
|
+
"motion": "12.16.0",
|
|
71
|
+
"preact": "10.26.5",
|
|
72
|
+
"prism-react-renderer": "2.4.1",
|
|
73
|
+
"react-highlight-words": "0.21.0",
|
|
74
|
+
"remark-gfm": "4.0.1",
|
|
75
|
+
"saas": "1.0.0",
|
|
76
|
+
"sass-embedded": "1.89.2",
|
|
77
|
+
"sirv": "3.0.1",
|
|
78
|
+
"tailwindcss": "4.1.16",
|
|
79
|
+
"tsx": "4.20.4",
|
|
81
80
|
"typescript": "~5.8.3",
|
|
82
|
-
"unist-util-visit": "
|
|
83
|
-
"vite": "
|
|
84
|
-
"vite-plugin-mdx": "
|
|
85
|
-
"wouter-preact": "
|
|
81
|
+
"unist-util-visit": "5.0.0",
|
|
82
|
+
"vite": "6.3.5",
|
|
83
|
+
"vite-plugin-mdx": "3.6.1",
|
|
84
|
+
"wouter-preact": "3.7.1"
|
|
86
85
|
},
|
|
87
|
-
"packageManager": "pnpm@10.
|
|
86
|
+
"packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a",
|
|
88
87
|
"devDependencies": {
|
|
89
|
-
"@types/node": "
|
|
88
|
+
"@types/node": "24.0.12",
|
|
89
|
+
"terser": "5.44.1"
|
|
90
90
|
}
|
|
91
91
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Component, type ComponentChildren } from 'preact';
|
|
2
|
+
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline';
|
|
3
|
+
|
|
4
|
+
interface ErrorBoundaryProps {
|
|
5
|
+
children: ComponentChildren;
|
|
6
|
+
fallback?: (error: Error) => ComponentChildren;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ErrorBoundaryState {
|
|
10
|
+
hasError: boolean;
|
|
11
|
+
error: Error | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
15
|
+
constructor(props: ErrorBoundaryProps) {
|
|
16
|
+
super(props);
|
|
17
|
+
this.state = { hasError: false, error: null };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
21
|
+
return { hasError: true, error };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
componentDidCatch(error: Error, errorInfo: any) {
|
|
25
|
+
console.error('Error caught by boundary:', error, errorInfo);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
render() {
|
|
29
|
+
if (this.state.hasError && this.state.error) {
|
|
30
|
+
if (this.props.fallback) {
|
|
31
|
+
return this.props.fallback(this.state.error);
|
|
32
|
+
}
|
|
33
|
+
return (
|
|
34
|
+
<div className="flex flex-col items-center justify-center min-h-[50vh] p-8">
|
|
35
|
+
<div className="w-16 h-16 mb-4 flex items-center justify-center bg-red-100 dark:bg-red-900/30 rounded-full">
|
|
36
|
+
<ExclamationTriangleIcon className="w-8 h-8 text-red-500" />
|
|
37
|
+
</div>
|
|
38
|
+
<h2 className="text-2xl font-semibold text-red-700 dark:text-red-300 mb-2">
|
|
39
|
+
Something went wrong
|
|
40
|
+
</h2>
|
|
41
|
+
<p className="text-red-600 dark:text-red-400 mb-4 text-center max-w-md">
|
|
42
|
+
An error occurred while rendering this page.
|
|
43
|
+
</p>
|
|
44
|
+
<pre className="text-sm text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-md p-3 max-w-md mb-4 overflow-auto">
|
|
45
|
+
{this.state.error.message}
|
|
46
|
+
</pre>
|
|
47
|
+
<button
|
|
48
|
+
onClick={() => window.location.reload()}
|
|
49
|
+
className="px-4 py-2 text-sm font-medium rounded-md bg-[var(--brand-primary)] text-white hover:opacity-90 transition-opacity"
|
|
50
|
+
>
|
|
51
|
+
Reload Page
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return this.props.children;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -70,10 +70,27 @@ export default function Routes() {
|
|
|
70
70
|
useEffect(() => {
|
|
71
71
|
const page = ALL_NAVIGATION.find((tab) => tab.groups.find((group) => group.pages.find((page) => page.href === pathname)));
|
|
72
72
|
if (!page && pathname !== '/') {
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
setRoutes([{
|
|
74
|
+
path: pathname,
|
|
75
|
+
component: () => (
|
|
76
|
+
<div className="flex flex-col items-center justify-center min-h-[50vh] p-8">
|
|
77
|
+
<h1 className="text-6xl font-bold text-gray-300 dark:text-gray-700 mb-4">404</h1>
|
|
78
|
+
<h2 className="text-2xl font-semibold mb-4">Page Not Found</h2>
|
|
79
|
+
<p className="text-gray-600 dark:text-gray-400 mb-6 text-center max-w-md">
|
|
80
|
+
The page <code className="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 rounded text-sm">{pathname}</code> doesn't exist.
|
|
81
|
+
</p>
|
|
82
|
+
<button
|
|
83
|
+
onClick={() => navigate(ALL_NAVIGATION[0].groups[0].pages[0].href)}
|
|
84
|
+
className="px-4 py-2 text-sm font-medium rounded-md bg-[var(--brand-primary)] text-white hover:opacity-90 transition-opacity"
|
|
85
|
+
>
|
|
86
|
+
Go to Home
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
)
|
|
90
|
+
}]);
|
|
91
|
+
setLoading(false);
|
|
75
92
|
}
|
|
76
|
-
}, [
|
|
93
|
+
}, [pathname]);
|
|
77
94
|
|
|
78
95
|
useEffect(() => {
|
|
79
96
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
@@ -1,41 +1,52 @@
|
|
|
1
|
-
|
|
1
|
+
let searchIndex: any = null;
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
async function getSearchIndex() {
|
|
4
|
+
if (!searchIndex) {
|
|
5
|
+
const { Document } = await import('flexsearch');
|
|
6
|
+
searchIndex = new Document({
|
|
7
|
+
tokenize: 'full',
|
|
8
|
+
document: {
|
|
9
|
+
id: 'url',
|
|
10
|
+
index: 'content',
|
|
11
|
+
store: ['title', 'pageTitle'],
|
|
12
|
+
},
|
|
13
|
+
context: {
|
|
14
|
+
resolution: 9,
|
|
15
|
+
depth: 2,
|
|
16
|
+
bidirectional: true
|
|
17
|
+
}
|
|
18
|
+
});
|
|
14
19
|
}
|
|
15
|
-
|
|
20
|
+
return searchIndex;
|
|
21
|
+
}
|
|
16
22
|
|
|
17
|
-
export function addToSearchIndex(filename: string, data: any, content: string) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
export async function addToSearchIndex(filename: string, data: any, content: string) {
|
|
24
|
+
try {
|
|
25
|
+
const index = await getSearchIndex();
|
|
26
|
+
index.add({
|
|
27
|
+
url: filename,
|
|
28
|
+
title: data.title || '',
|
|
29
|
+
content: content,
|
|
30
|
+
pageTitle: data.title
|
|
31
|
+
});
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.warn(`Failed to index ${filename}:`, error);
|
|
34
|
+
}
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
export function search(query: string, options = {}) {
|
|
27
38
|
if (!searchIndex) return [];
|
|
28
|
-
|
|
39
|
+
|
|
29
40
|
const result = searchIndex.search(query, {
|
|
30
41
|
...options,
|
|
31
42
|
enrich: true,
|
|
32
|
-
});
|
|
43
|
+
}) as any[];
|
|
33
44
|
|
|
34
45
|
if (result.length === 0) {
|
|
35
46
|
return [];
|
|
36
47
|
}
|
|
37
48
|
|
|
38
|
-
return result[0].result.map((item) => ({
|
|
49
|
+
return result[0].result.map((item: any) => ({
|
|
39
50
|
url: item.id,
|
|
40
51
|
title: item.doc?.title || '',
|
|
41
52
|
pageTitle: item.doc?.pageTitle || '',
|
package/src/components/store.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ComponentType } from 'preact';
|
|
|
2
2
|
import { type RouteComponentProps } from 'wouter-preact';
|
|
3
3
|
import docsConfig from '../../docs/dory.json' with { type: 'json' };
|
|
4
4
|
import type { DoryConfig } from '../types/config';
|
|
5
|
-
import {
|
|
5
|
+
import { addToSearchIndex } from './search-index';
|
|
6
6
|
|
|
7
7
|
const config = docsConfig as DoryConfig;
|
|
8
8
|
|
|
@@ -108,16 +108,26 @@ export async function preloadFrontmatter() {
|
|
|
108
108
|
// Add preloaded content to search index
|
|
109
109
|
async function addPreloadedContentToSearch() {
|
|
110
110
|
if (!preloadedFrontMatter) return;
|
|
111
|
+
|
|
112
|
+
const mdxFiles = import.meta.glob<{ default: string }>('../../docs/**/*.mdx', { query: 'raw' });
|
|
113
|
+
const fileEntries = Object.entries(mdxFiles);
|
|
111
114
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
await Promise.all(
|
|
116
|
+
preloadedFrontMatter.map(async (fm) => {
|
|
117
|
+
const fileEntry = fileEntries.find(([filePath]) => pathFromFilename(filePath) === fm.path);
|
|
118
|
+
if (fileEntry) {
|
|
119
|
+
try {
|
|
120
|
+
const [, loader] = fileEntry;
|
|
121
|
+
const rawContent = (await loader()).default;
|
|
122
|
+
const { content } = parseFrontMatter(rawContent);
|
|
123
|
+
|
|
124
|
+
await addToSearchIndex(fm.path, { title: fm.title }, content);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.warn(`Failed to index ${fm.path}:`, error);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
);
|
|
121
131
|
}
|
|
122
132
|
|
|
123
133
|
// Load frontmatter for a specific pathname (optimized with fallback)
|
|
@@ -143,12 +153,7 @@ export async function loadMDXFrontMatterForPath(pathname: string) {
|
|
|
143
153
|
const filename = pathFromFilename(path);
|
|
144
154
|
|
|
145
155
|
updateNavigationTitle(filename, data.title);
|
|
146
|
-
|
|
147
|
-
url: filename,
|
|
148
|
-
title: data.title || '',
|
|
149
|
-
content,
|
|
150
|
-
pageTitle: data.title || ''
|
|
151
|
-
});
|
|
156
|
+
await addToSearchIndex(filename, data, content);
|
|
152
157
|
|
|
153
158
|
const frontmatterItem = { ...data, path: filename };
|
|
154
159
|
completeFrontMatter = [...completeFrontMatter, frontmatterItem];
|
|
@@ -175,12 +180,7 @@ export async function loadAllMDXFrontMatter(pathname: string) {
|
|
|
175
180
|
const filename = pathFromFilename(path);
|
|
176
181
|
|
|
177
182
|
updateNavigationTitle(filename, data.title);
|
|
178
|
-
|
|
179
|
-
url: filename,
|
|
180
|
-
title: data.title || '',
|
|
181
|
-
content,
|
|
182
|
-
pageTitle: data.title || ''
|
|
183
|
-
});
|
|
183
|
+
await addToSearchIndex(filename, data, content);
|
|
184
184
|
|
|
185
185
|
newFrontmatter.push({ ...data, path: filename });
|
|
186
186
|
})
|
package/src/main.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { render } from 'preact'
|
|
|
2
2
|
import '@clidey/ux/styles.css';
|
|
3
3
|
import { App } from './app.tsx'
|
|
4
4
|
import { withMDX } from './components/mdx-provider.tsx'
|
|
5
|
+
import { ErrorBoundary } from './components/error-boundary.tsx'
|
|
5
6
|
import { loadFonts } from './utils/fonts'
|
|
6
7
|
import { loadColors } from './utils/colors'
|
|
7
8
|
import { Toaster } from '@clidey/ux'
|
|
@@ -16,7 +17,9 @@ const AppWithMDX = withMDX(App)
|
|
|
16
17
|
render(
|
|
17
18
|
<ThemeProvider storageKey="@clidey/dory/theme">
|
|
18
19
|
<Toaster />
|
|
19
|
-
<
|
|
20
|
+
<ErrorBoundary>
|
|
21
|
+
<AppWithMDX />
|
|
22
|
+
</ErrorBoundary>
|
|
20
23
|
</ThemeProvider>,
|
|
21
24
|
document.getElementById('app')!
|
|
22
25
|
)
|
package/src/mdx/latex.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import 'katex/dist/katex.min.css'
|
|
1
|
+
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
|
|
2
|
+
import { useEffect, useRef, useState } from 'react'
|
|
4
3
|
|
|
5
4
|
interface LatexProps {
|
|
6
5
|
children: string
|
|
@@ -8,19 +7,77 @@ interface LatexProps {
|
|
|
8
7
|
|
|
9
8
|
export function Latex({ children }: LatexProps) {
|
|
10
9
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
10
|
+
const [error, setError] = useState<string | null>(null)
|
|
11
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
11
12
|
|
|
12
13
|
useEffect(() => {
|
|
13
|
-
|
|
14
|
+
let mounted = true
|
|
15
|
+
|
|
16
|
+
const renderLatex = async () => {
|
|
14
17
|
try {
|
|
18
|
+
setError(null)
|
|
19
|
+
setIsLoading(true)
|
|
20
|
+
|
|
21
|
+
const [katexModule] = await Promise.all([
|
|
22
|
+
import('katex'),
|
|
23
|
+
import('katex/dist/katex.min.css'),
|
|
24
|
+
])
|
|
25
|
+
const katex = katexModule.default
|
|
26
|
+
|
|
27
|
+
if (!mounted || !containerRef.current) return
|
|
28
|
+
|
|
15
29
|
katex.render(children, containerRef.current, {
|
|
16
30
|
displayMode: true,
|
|
17
31
|
throwOnError: false,
|
|
18
32
|
})
|
|
33
|
+
setIsLoading(false)
|
|
19
34
|
} catch (error) {
|
|
20
|
-
|
|
35
|
+
if (mounted) {
|
|
36
|
+
console.error('Error rendering LaTeX:', error)
|
|
37
|
+
setError(error instanceof Error ? error.message : 'Failed to render LaTeX')
|
|
38
|
+
setIsLoading(false)
|
|
39
|
+
}
|
|
21
40
|
}
|
|
22
41
|
}
|
|
42
|
+
|
|
43
|
+
renderLatex()
|
|
44
|
+
|
|
45
|
+
return () => {
|
|
46
|
+
mounted = false
|
|
47
|
+
}
|
|
23
48
|
}, [children])
|
|
24
49
|
|
|
25
|
-
|
|
50
|
+
if (error) {
|
|
51
|
+
return (
|
|
52
|
+
<div className="my-4">
|
|
53
|
+
<div className="flex flex-col items-center justify-center py-12 px-6 text-center bg-gradient-to-br from-red-50 to-red-100 dark:from-red-900/20 dark:to-red-800/20 border-2 border-dashed border-red-300 dark:border-red-700 rounded-lg">
|
|
54
|
+
<div className="w-16 h-16 mb-4 flex items-center justify-center bg-red-100 dark:bg-red-900/30 rounded-full">
|
|
55
|
+
<ExclamationTriangleIcon className="w-8 h-8 text-red-500" />
|
|
56
|
+
</div>
|
|
57
|
+
<h3 className="text-lg font-semibold text-red-700 dark:text-red-300 mb-2">
|
|
58
|
+
LaTeX Rendering Error
|
|
59
|
+
</h3>
|
|
60
|
+
<p className="text-red-600 dark:text-red-400 mb-4 max-w-md">
|
|
61
|
+
The LaTeX expression could not be rendered.
|
|
62
|
+
</p>
|
|
63
|
+
<div className="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-md p-3 max-w-md">
|
|
64
|
+
<p className="text-sm text-red-700 dark:text-red-300 font-mono break-all">
|
|
65
|
+
{error}
|
|
66
|
+
</p>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="my-4">
|
|
75
|
+
{isLoading && (
|
|
76
|
+
<div className="flex items-center justify-center py-4">
|
|
77
|
+
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500" />
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
<div ref={containerRef} className={isLoading ? 'hidden' : ''} />
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
26
83
|
}
|
package/src/mdx/mdx.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'preact/hooks';
|
|
2
|
+
import { lazy, Suspense } from 'preact/compat';
|
|
2
3
|
import { usePathname } from 'wouter-preact/use-browser-location';
|
|
3
4
|
import { completeFrontMatter } from '../components/store';
|
|
5
|
+
import { Loading } from '../components/loading';
|
|
4
6
|
|
|
5
7
|
export { h1, h2, h3, h4, h5, h6, p, Row, Col, Anchor as a, Image as img } from './text';
|
|
6
8
|
export { Code as code, CodeGroup } from './code';
|
|
@@ -17,11 +19,24 @@ export { ResponseField, Properties, Property } from './open-api';
|
|
|
17
19
|
export { Steps, Step } from './step';
|
|
18
20
|
export { Table as table, Th as th, Td as td } from './table';
|
|
19
21
|
export { UnorderedList as ul, OrderedList as ol, ListItem as li } from './list';
|
|
20
|
-
export { APIPlayground } from './api-playground';
|
|
21
|
-
export { WebSocketPlayground } from './websocket-playground';
|
|
22
22
|
export { AsyncAPI } from './async-api';
|
|
23
23
|
export { Source } from './source';
|
|
24
24
|
|
|
25
|
+
// Lazy-loaded heavy components — only downloaded when a page uses them
|
|
26
|
+
const LazyAPIPlayground = lazy(() => import('./api-playground').then(m => ({ default: m.APIPlayground })));
|
|
27
|
+
export const APIPlayground = (props: any) => (
|
|
28
|
+
<Suspense fallback={<Loading />}>
|
|
29
|
+
<LazyAPIPlayground {...props} />
|
|
30
|
+
</Suspense>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const LazyWebSocketPlayground = lazy(() => import('./websocket-playground').then(m => ({ default: m.WebSocketPlayground })));
|
|
34
|
+
export const WebSocketPlayground = (props: any) => (
|
|
35
|
+
<Suspense fallback={<Loading />}>
|
|
36
|
+
<LazyWebSocketPlayground {...props} />
|
|
37
|
+
</Suspense>
|
|
38
|
+
);
|
|
39
|
+
|
|
25
40
|
export const wrapper = ({ children }: { children: preact.ComponentChildren }) => {
|
|
26
41
|
const ref = useRef<HTMLDivElement>(null);
|
|
27
42
|
const pathname = usePathname();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
import { readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Conditionally injects analytics scripts based on dory.json config.
|
|
7
|
+
* PostHog is only injected if `analytics.posthog.token` is configured.
|
|
8
|
+
*/
|
|
9
|
+
export function analyticsInjector(): Plugin {
|
|
10
|
+
return {
|
|
11
|
+
name: 'analytics-injector',
|
|
12
|
+
transformIndexHtml(html) {
|
|
13
|
+
const doryJsonPath = resolve(process.cwd(), 'docs', 'dory.json');
|
|
14
|
+
if (!existsSync(doryJsonPath)) return html;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const config = JSON.parse(readFileSync(doryJsonPath, 'utf-8'));
|
|
18
|
+
|
|
19
|
+
if (config.analytics?.posthog?.token) {
|
|
20
|
+
const token = config.analytics.posthog.token;
|
|
21
|
+
const apiHost = config.analytics.posthog.apiHost || 'https://us.i.posthog.com';
|
|
22
|
+
|
|
23
|
+
const script = `
|
|
24
|
+
<script>
|
|
25
|
+
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture identify setPersonProperties group resetGroups reset get_distinct_id getGroups get_session_id opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
|
|
26
|
+
posthog.init('${token}', {
|
|
27
|
+
api_host: '${apiHost}',
|
|
28
|
+
person_profiles: 'identified_only',
|
|
29
|
+
})
|
|
30
|
+
</script>`;
|
|
31
|
+
|
|
32
|
+
return html.replace('</head>', `${script}\n </head>`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return html;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
const detail = error instanceof SyntaxError ? error.message : String(error);
|
|
38
|
+
console.error(`Failed to parse dory.json for analytics injection: ${detail}`);
|
|
39
|
+
return html;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
import { readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generates a Content-Security-Policy meta tag based on dory.json config.
|
|
7
|
+
* Dynamically adds font sources and analytics domains as needed.
|
|
8
|
+
*/
|
|
9
|
+
export function cspGenerator(): Plugin {
|
|
10
|
+
return {
|
|
11
|
+
name: 'csp-generator',
|
|
12
|
+
transformIndexHtml(html) {
|
|
13
|
+
const doryJsonPath = resolve(process.cwd(), 'docs', 'dory.json');
|
|
14
|
+
if (!existsSync(doryJsonPath)) return html;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const config = JSON.parse(readFileSync(doryJsonPath, 'utf-8'));
|
|
18
|
+
|
|
19
|
+
const scriptSrc = ["'self'", "'unsafe-inline'", "'unsafe-eval'"];
|
|
20
|
+
const styleSrc = ["'self'", "'unsafe-inline'"];
|
|
21
|
+
const fontSrc = ["'self'", "data:"];
|
|
22
|
+
const connectSrc = ["'self'"];
|
|
23
|
+
const imgSrc = ["'self'", "data:", "https:"];
|
|
24
|
+
|
|
25
|
+
// Add Google Fonts if fonts are configured with external source
|
|
26
|
+
if (config.fonts?.source) {
|
|
27
|
+
try {
|
|
28
|
+
const fontUrl = new URL(config.fonts.source);
|
|
29
|
+
fontSrc.push(fontUrl.origin);
|
|
30
|
+
} catch {
|
|
31
|
+
// Not a URL, skip
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
styleSrc.push("https://fonts.googleapis.com");
|
|
35
|
+
fontSrc.push("https://fonts.gstatic.com");
|
|
36
|
+
|
|
37
|
+
// Add PostHog if analytics configured
|
|
38
|
+
if (config.analytics?.posthog?.token) {
|
|
39
|
+
const apiHost = config.analytics.posthog.apiHost || 'https://us.i.posthog.com';
|
|
40
|
+
const assetsHost = apiHost.replace('.i.posthog.com', '-assets.i.posthog.com');
|
|
41
|
+
scriptSrc.push(apiHost);
|
|
42
|
+
scriptSrc.push(assetsHost);
|
|
43
|
+
connectSrc.push(apiHost);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Note: frame-ancestors is omitted because it's ignored in <meta> CSP tags.
|
|
47
|
+
// Clickjacking protection is handled via X-Frame-Options in nginx config.
|
|
48
|
+
const csp = [
|
|
49
|
+
`default-src 'self'`,
|
|
50
|
+
`script-src ${scriptSrc.join(' ')}`,
|
|
51
|
+
`style-src ${styleSrc.join(' ')}`,
|
|
52
|
+
`font-src ${fontSrc.join(' ')}`,
|
|
53
|
+
`img-src ${imgSrc.join(' ')}`,
|
|
54
|
+
`connect-src ${connectSrc.join(' ')}`,
|
|
55
|
+
].join('; ');
|
|
56
|
+
|
|
57
|
+
return html.replace(
|
|
58
|
+
'</head>',
|
|
59
|
+
` <meta http-equiv="Content-Security-Policy" content="${csp}">\n </head>`
|
|
60
|
+
);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
const detail = error instanceof SyntaxError ? error.message : String(error);
|
|
63
|
+
console.error(`Failed to generate CSP: ${detail}`);
|
|
64
|
+
return html;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
|
-
import { cpSync, existsSync, readdirSync, statSync
|
|
2
|
+
import { cpSync, existsSync, readdirSync, statSync } from 'fs';
|
|
3
3
|
import { resolve, extname } from 'path';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -31,7 +31,6 @@ export function docsAssetsPlugin(): Plugin {
|
|
|
31
31
|
const skipPatterns = new Set([
|
|
32
32
|
'dory.json',
|
|
33
33
|
'node_modules',
|
|
34
|
-
'dist',
|
|
35
34
|
'.DS_Store',
|
|
36
35
|
]);
|
|
37
36
|
|
|
@@ -57,10 +56,6 @@ export function docsAssetsPlugin(): Plugin {
|
|
|
57
56
|
const stats = statSync(srcPath);
|
|
58
57
|
|
|
59
58
|
if (stats.isDirectory()) {
|
|
60
|
-
// Create destination directory if it doesn't exist
|
|
61
|
-
if (!existsSync(destPath)) {
|
|
62
|
-
mkdirSync(destPath, { recursive: true });
|
|
63
|
-
}
|
|
64
59
|
// Recursively copy directories
|
|
65
60
|
copyAssets(srcPath, destPath, itemRelativePath);
|
|
66
61
|
} else if (stats.isFile()) {
|
|
@@ -6,7 +6,14 @@ import path from 'path';
|
|
|
6
6
|
function getNavigationOrder(docsDir: string): string[] {
|
|
7
7
|
const doryJsonPath = path.join(docsDir, 'dory.json');
|
|
8
8
|
if (!fs.existsSync(doryJsonPath)) return [];
|
|
9
|
-
|
|
9
|
+
let doryJson;
|
|
10
|
+
try {
|
|
11
|
+
doryJson = JSON.parse(fs.readFileSync(doryJsonPath, 'utf-8'));
|
|
12
|
+
} catch (error) {
|
|
13
|
+
const detail = error instanceof SyntaxError ? error.message : String(error);
|
|
14
|
+
console.error(`Failed to parse ${doryJsonPath}: ${detail}`);
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
10
17
|
const order: string[] = [];
|
|
11
18
|
|
|
12
19
|
function walkPages(pages: any[]) {
|