@clidey/dory 0.46.0 → 0.48.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/bin/dist/dory.js +103 -45
- package/bin/dist/dory.js.map +1 -1
- package/index.html +11 -15
- package/package.json +35 -29
- package/scripts/render-routes.mjs +55 -15
- package/src/components/routes.tsx +38 -93
- package/src/components/search.tsx +1 -0
- package/src/components/store.ts +64 -88
- package/src/config/mdx.js +7 -3
- package/src/config/mdx.ts +6 -2
- package/src/entry-server.tsx +189 -66
- package/src/main.tsx +27 -5
- package/src/mdx/api-playground.tsx +2 -0
- package/src/mdx/code.tsx +8 -5
- package/src/mdx/fence.tsx +10 -2
- package/src/mdx/icon.tsx +21 -14
- package/src/mdx/latex.tsx +5 -0
- package/src/mdx/mdx.tsx +19 -11
- package/src/mdx/mermaid.tsx +10 -1
- package/src/mdx/processor.test.ts +47 -0
- package/src/mdx/step.tsx +3 -3
- package/src/mdx/websocket-playground.tsx +2 -0
- package/src/plugins/analytics-injector.ts +7 -5
- package/src/plugins/docs-assets.ts +5 -4
- package/src/plugins/frontmatter-dev-server.ts +23 -137
- package/src/plugins/frontmatter-generator.ts +64 -111
- package/src/plugins/html-files-middleware.test.ts +21 -0
- package/src/plugins/html-files-middleware.ts +27 -5
- package/src/plugins/html-metadata-injector.ts +94 -53
- package/src/plugins/llm-txt-dev-server.ts +25 -178
- package/src/plugins/llm-txt-generator.ts +94 -133
- package/src/plugins/prerender.ts +156 -50
- package/src/plugins/robots-generator.ts +15 -16
- package/src/plugins/sanitize.test.ts +104 -0
- package/src/plugins/sanitize.ts +91 -60
- package/src/plugins/shared.ts +139 -0
- package/src/plugins/sitemap-generator.ts +58 -44
- package/src/theme.scss +138 -15
- package/src/types/config.ts +42 -2
- package/src/types/globals.d.ts +7 -0
- package/src/ui/header.tsx +12 -4
- package/src/ui/layout.tsx +72 -52
- package/src/ui/table-of-content.tsx +19 -2
- package/src/utils/colors.ts +38 -18
- package/src/utils/fonts.ts +55 -12
- package/src/utils/theme.ts +29 -0
- package/vite.config.embed-loader.ts +3 -11
- package/vite.config.embed-widget.ts +3 -10
- package/vite.config.ssr.ts +2 -1
- package/vite.config.ts +13 -8
- package/scripts/generate-frontmatter-json.js +0 -154
- package/scripts/generate-llm-txt.js +0 -115
|
@@ -1,115 +1,60 @@
|
|
|
1
|
-
import { useEffect,
|
|
1
|
+
import { useEffect, useMemo } from 'preact/hooks';
|
|
2
2
|
import { Route, Router, useLocation } from 'wouter-preact';
|
|
3
3
|
import { usePathname } from './hooks';
|
|
4
4
|
import { ALL_NAVIGATION, ALL_PAGES, pathFromFilename } from './store';
|
|
5
|
-
import { Loading } from './loading';
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
export default function Routes() {
|
|
9
|
-
const [routes, setRoutes] = useState<any[]>([]);
|
|
10
8
|
const pathname = usePathname();
|
|
11
9
|
const [, navigate] = useLocation();
|
|
12
|
-
const [loading, setLoading] = useState(true);
|
|
13
10
|
|
|
11
|
+
// Redirect root to the first page so the URL reflects the actual content
|
|
14
12
|
useEffect(() => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
links.forEach(link => {
|
|
20
|
-
const href = link.getAttribute('href');
|
|
21
|
-
if (!href) return;
|
|
22
|
-
|
|
23
|
-
const loaderEntry = Object.entries(ALL_PAGES).find(
|
|
24
|
-
([path]) => path === href
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
if (!loaderEntry) return;
|
|
28
|
-
|
|
29
|
-
const [path, loader] = loaderEntry;
|
|
30
|
-
|
|
31
|
-
const handleMouseEnter = () => {
|
|
32
|
-
if (!preloaded.has(path)) {
|
|
33
|
-
loader();
|
|
34
|
-
preloaded.add(path);
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
link.addEventListener('mouseenter', handleMouseEnter);
|
|
39
|
-
listeners.push(() => link.removeEventListener('mouseenter', handleMouseEnter));
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
return () => {
|
|
43
|
-
listeners.forEach(unsub => unsub());
|
|
44
|
-
};
|
|
45
|
-
}, []);
|
|
46
|
-
|
|
47
|
-
useEffect(() => {
|
|
48
|
-
setLoading(true);
|
|
49
|
-
const loadRoutes = async () => {
|
|
50
|
-
try {
|
|
51
|
-
// For root path, load the first navigation page
|
|
52
|
-
const targetPath = pathname === '/' ? ALL_NAVIGATION[0].groups[0].pages[0].href : pathname;
|
|
53
|
-
|
|
54
|
-
const entries = await Promise.all(
|
|
55
|
-
Object.entries(ALL_PAGES).filter(([path]) => path === targetPath).map(async ([path, loader]) => {
|
|
56
|
-
const module = await loader();
|
|
57
|
-
const routePath = pathname === '/' ? '/' : pathFromFilename(path);
|
|
58
|
-
return {
|
|
59
|
-
path: routePath,
|
|
60
|
-
component: module.default,
|
|
61
|
-
};
|
|
62
|
-
})
|
|
63
|
-
);
|
|
64
|
-
setRoutes(entries);
|
|
65
|
-
} catch (error) {
|
|
66
|
-
console.error('Failed to load route:', error);
|
|
67
|
-
setRoutes([]);
|
|
68
|
-
} finally {
|
|
69
|
-
setLoading(false);
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
loadRoutes();
|
|
13
|
+
if (pathname === '/') {
|
|
14
|
+
const firstPage = ALL_NAVIGATION[0]?.groups[0]?.pages[0]?.href;
|
|
15
|
+
if (firstPage) navigate(firstPage, { replace: true });
|
|
16
|
+
}
|
|
74
17
|
}, [pathname]);
|
|
75
18
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
component: () => (
|
|
82
|
-
<div className="flex flex-col items-center justify-center min-h-[50vh] p-8">
|
|
83
|
-
<h1 className="text-6xl font-bold text-gray-300 dark:text-gray-700 mb-4">404</h1>
|
|
84
|
-
<h2 className="text-2xl font-semibold mb-4">Page Not Found</h2>
|
|
85
|
-
<p className="text-gray-600 dark:text-gray-400 mb-6 text-center max-w-md">
|
|
86
|
-
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.
|
|
87
|
-
</p>
|
|
88
|
-
<button
|
|
89
|
-
onClick={() => navigate(ALL_NAVIGATION[0].groups[0].pages[0].href)}
|
|
90
|
-
className="px-4 py-2 text-sm font-medium rounded-md bg-[var(--brand-primary)] text-white hover:opacity-90 transition-opacity"
|
|
91
|
-
>
|
|
92
|
-
Go to Home
|
|
93
|
-
</button>
|
|
94
|
-
</div>
|
|
95
|
-
)
|
|
96
|
-
}]);
|
|
97
|
-
setLoading(false);
|
|
19
|
+
const route = useMemo(() => {
|
|
20
|
+
let targetPath = pathname;
|
|
21
|
+
if (pathname === '/') {
|
|
22
|
+
const ssrRoute = typeof window !== 'undefined' ? window.__DORY_ROUTE__ : undefined;
|
|
23
|
+
targetPath = ssrRoute || ALL_NAVIGATION[0]?.groups[0]?.pages[0]?.href || pathname;
|
|
98
24
|
}
|
|
99
|
-
}, [pathname]);
|
|
100
25
|
|
|
101
|
-
|
|
102
|
-
|
|
26
|
+
const entry = Object.entries(ALL_PAGES).find(([path]) => path === targetPath);
|
|
27
|
+
if (!entry) return null;
|
|
28
|
+
|
|
29
|
+
const [path, mod] = entry;
|
|
30
|
+
return {
|
|
31
|
+
path: pathname === '/' ? '/' : pathFromFilename(path),
|
|
32
|
+
component: (mod as any).default,
|
|
33
|
+
};
|
|
103
34
|
}, [pathname]);
|
|
104
|
-
|
|
105
35
|
|
|
106
|
-
|
|
36
|
+
// 404 — no MDX module for this path (even if it appears in navigation)
|
|
37
|
+
if (!route && pathname !== '/') {
|
|
38
|
+
return (
|
|
39
|
+
<div className="flex flex-col items-center justify-center min-h-[50vh] p-8">
|
|
40
|
+
<h1 className="text-6xl font-bold text-gray-300 dark:text-gray-700 mb-4">404</h1>
|
|
41
|
+
<h2 className="text-2xl font-semibold mb-4">Page Not Found</h2>
|
|
42
|
+
<p className="text-gray-600 dark:text-gray-400 mb-6 text-center max-w-md">
|
|
43
|
+
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.
|
|
44
|
+
</p>
|
|
45
|
+
<button
|
|
46
|
+
onClick={() => navigate(ALL_NAVIGATION[0]?.groups[0]?.pages[0]?.href ?? '/')}
|
|
47
|
+
className="px-4 py-2 text-sm font-medium rounded-md bg-[var(--brand-primary)] text-white hover:opacity-90 transition-opacity"
|
|
48
|
+
>
|
|
49
|
+
Go to Home
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
107
54
|
|
|
108
55
|
return (
|
|
109
56
|
<Router>
|
|
110
|
-
{
|
|
111
|
-
<Route key={path} path={path} component={Component} />
|
|
112
|
-
))}
|
|
57
|
+
{route && <Route key={route.path} path={route.path} component={route.component} />}
|
|
113
58
|
</Router>
|
|
114
59
|
);
|
|
115
60
|
}
|
package/src/components/store.ts
CHANGED
|
@@ -6,9 +6,12 @@ import { addToSearchIndex } from './search-index';
|
|
|
6
6
|
|
|
7
7
|
const config = docsConfig as DoryConfig;
|
|
8
8
|
|
|
9
|
+
// Eager-load all page modules so they're available synchronously.
|
|
10
|
+
// This eliminates the async loading spinner in Routes and enables
|
|
11
|
+
// proper hydrate() — the first render matches the SSR DOM.
|
|
9
12
|
export const ALL_PAGES = Object.fromEntries(
|
|
10
|
-
Object.entries(import.meta.glob<{ default: ComponentType; }>('../../docs/**/*.mdx'))
|
|
11
|
-
.map(([path,
|
|
13
|
+
Object.entries(import.meta.glob<{ default: ComponentType; }>('../../docs/**/*.mdx', { eager: true }))
|
|
14
|
+
.map(([path, mod]) => [pathFromFilename(path), mod])
|
|
12
15
|
);
|
|
13
16
|
export const ALL_OPENAPI = Object.fromEntries(
|
|
14
17
|
Object.entries(import.meta.glob<{ default: string }>('../../docs/**/openapi.json', { query: 'raw', eager: true }))
|
|
@@ -59,30 +62,26 @@ export function pathFromFilename(filename: string): string {
|
|
|
59
62
|
.toLowerCase();
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
function parseFrontMatter(content: string) {
|
|
63
|
-
const frontMatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
64
|
-
if (!frontMatterMatch) return { data: {}, content };
|
|
65
|
-
|
|
66
|
-
const frontMatter = frontMatterMatch[1];
|
|
67
|
-
const data: Record<string, any> = {};
|
|
68
|
-
|
|
69
|
-
frontMatter.split('\n').forEach(line => {
|
|
70
|
-
const [key, ...valueParts] = line.split(':');
|
|
71
|
-
if (key && valueParts.length) {
|
|
72
|
-
const value = valueParts.join(':').trim();
|
|
73
|
-
data[key.trim()] = value.replace(/^["']|["']$/g, ''); // Remove quotes if present
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
return { data, content: content.slice(frontMatterMatch[0].length) };
|
|
78
|
-
}
|
|
79
65
|
|
|
80
66
|
export let completeFrontMatter: Record<string, any>[] = [];
|
|
67
|
+
export function isFrontmatterReady() { return preloadedFrontMatter !== null; }
|
|
81
68
|
let preloadedFrontMatter: Record<string, any>[] | null = null;
|
|
82
69
|
|
|
83
|
-
//
|
|
70
|
+
// Check for inlined frontmatter from SSR (available synchronously, no fetch needed)
|
|
71
|
+
if (typeof window !== 'undefined' && window.__DORY_FRONTMATTER__) {
|
|
72
|
+
preloadedFrontMatter = window.__DORY_FRONTMATTER__;
|
|
73
|
+
completeFrontMatter = [...preloadedFrontMatter];
|
|
74
|
+
for (const fm of preloadedFrontMatter) {
|
|
75
|
+
updateNavigationTitle(fm.path, fm.title);
|
|
76
|
+
}
|
|
77
|
+
delete window.__DORY_FRONTMATTER__;
|
|
78
|
+
// Build search index in the background after the page has fully rendered
|
|
79
|
+
setTimeout(() => addPreloadedContentToSearch(), 2000);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Preload frontmatter from JSON file (fallback when no inline data)
|
|
84
83
|
export async function preloadFrontmatter() {
|
|
85
|
-
if (preloadedFrontMatter) return; // Already loaded
|
|
84
|
+
if (preloadedFrontMatter) return; // Already loaded (inline or previous fetch)
|
|
86
85
|
|
|
87
86
|
try {
|
|
88
87
|
const response = await fetch('/frontmatter.json');
|
|
@@ -106,33 +105,23 @@ export async function preloadFrontmatter() {
|
|
|
106
105
|
}
|
|
107
106
|
}
|
|
108
107
|
|
|
109
|
-
// Add preloaded content to search index (runs in background, never blocks rendering)
|
|
108
|
+
// Add preloaded content to search index (runs in background, never blocks rendering).
|
|
109
|
+
// Fetches pre-built search-content.json generated at build time — no ?raw glob needed.
|
|
110
110
|
async function addPreloadedContentToSearch() {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const [, loader] = fileEntry;
|
|
126
|
-
const rawContent = (await loader()).default;
|
|
127
|
-
const { content } = parseFrontMatter(rawContent);
|
|
128
|
-
|
|
129
|
-
await addToSearchIndex(fm.path, { title: fm.title }, content);
|
|
130
|
-
} catch (error) {
|
|
131
|
-
console.warn(`Failed to index ${fm.path}:`, error);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
})
|
|
135
|
-
);
|
|
111
|
+
try {
|
|
112
|
+
const response = await fetch('/search-content.json');
|
|
113
|
+
if (!response.ok) return;
|
|
114
|
+
const searchContent: Array<{ path: string; title: string; content: string }> = await response.json();
|
|
115
|
+
|
|
116
|
+
for (const entry of searchContent) {
|
|
117
|
+
try {
|
|
118
|
+
await addToSearchIndex(entry.path, { title: entry.title }, entry.content);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.warn(`Failed to index ${entry.path}:`, error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.warn('Failed to load search content:', error);
|
|
136
125
|
}
|
|
137
126
|
}
|
|
138
127
|
|
|
@@ -146,53 +135,40 @@ export async function loadMDXFrontMatterForPath(pathname: string) {
|
|
|
146
135
|
// Check if already loaded in completeFrontMatter
|
|
147
136
|
if (completeFrontMatter.find(fm => fm.path === pathname)) return;
|
|
148
137
|
|
|
149
|
-
// Fallback
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const frontmatterItem = { ...data, path: filename };
|
|
165
|
-
completeFrontMatter = [...completeFrontMatter, frontmatterItem];
|
|
138
|
+
// Fallback: fetch frontmatter.json for this page's metadata
|
|
139
|
+
try {
|
|
140
|
+
const response = await fetch('/frontmatter.json');
|
|
141
|
+
if (response.ok) {
|
|
142
|
+
const allFm: Record<string, any>[] = await response.json();
|
|
143
|
+
const fm = allFm.find(f => f.path === pathname);
|
|
144
|
+
if (fm) {
|
|
145
|
+
updateNavigationTitle(pathname, fm.title);
|
|
146
|
+
completeFrontMatter = [...completeFrontMatter, fm];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
// Non-fatal — page will still render, just without title in nav
|
|
151
|
+
}
|
|
166
152
|
}
|
|
167
153
|
|
|
168
154
|
// Load all remaining frontmatter (after initial load) - optimized with fallback
|
|
169
155
|
export async function loadAllMDXFrontMatter(pathname: string) {
|
|
170
|
-
|
|
171
|
-
if (preloadedFrontMatter) {
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Fallback to dynamic loading
|
|
176
|
-
const mdxFiles = import.meta.glob<{ default: string }>('../../docs/**/*.mdx', { query: '?raw' });
|
|
177
|
-
const fileEntries = Object.entries(mdxFiles);
|
|
156
|
+
if (preloadedFrontMatter) return;
|
|
178
157
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
remainingEntries.map(async ([path, loader]) => {
|
|
184
|
-
const rawContent = (await loader()).default;
|
|
185
|
-
const { data, content } = parseFrontMatter(rawContent);
|
|
186
|
-
const filename = pathFromFilename(path);
|
|
187
|
-
|
|
188
|
-
updateNavigationTitle(filename, data.title);
|
|
189
|
-
await addToSearchIndex(filename, data, content);
|
|
190
|
-
|
|
191
|
-
newFrontmatter.push({ ...data, path: filename });
|
|
192
|
-
})
|
|
193
|
-
);
|
|
158
|
+
try {
|
|
159
|
+
const response = await fetch('/frontmatter.json');
|
|
160
|
+
if (!response.ok) return;
|
|
161
|
+
const allFm: Record<string, any>[] = await response.json();
|
|
194
162
|
|
|
195
|
-
|
|
163
|
+
for (const fm of allFm) {
|
|
164
|
+
if (fm.path !== pathname) {
|
|
165
|
+
updateNavigationTitle(fm.path, fm.title);
|
|
166
|
+
completeFrontMatter = [...completeFrontMatter, fm];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// Non-fatal
|
|
171
|
+
}
|
|
196
172
|
}
|
|
197
173
|
|
|
198
174
|
// Shared helper
|
package/src/config/mdx.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import remarkGfm from 'remark-gfm';
|
|
2
|
+
import rehypeSlug from 'rehype-slug';
|
|
2
3
|
import { remarkSafeVars } from '../plugins/sanitize.ts';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* Shared MDX configuration used by
|
|
6
|
-
* and CLI build:file command
|
|
6
|
+
* Shared MDX configuration used by the main build (vite.config.ts),
|
|
7
|
+
* the SSR build (vite.config.ssr.ts), and the CLI build:file command
|
|
8
|
+
* (bin/dory.js). rehype-slug adds stable ids to headings so anchors
|
|
9
|
+
* match between SSR output and client hydration.
|
|
7
10
|
*/
|
|
8
11
|
export const getMdxConfig = (isDevelopment = false) => ({
|
|
9
12
|
providerImportSource: '@mdx-js/preact',
|
|
10
13
|
remarkPlugins: [remarkGfm, remarkSafeVars],
|
|
14
|
+
rehypePlugins: [rehypeSlug],
|
|
11
15
|
development: isDevelopment,
|
|
12
|
-
});
|
|
16
|
+
});
|
package/src/config/mdx.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import remarkGfm from 'remark-gfm';
|
|
2
|
+
import rehypeSlug from 'rehype-slug';
|
|
2
3
|
import { remarkSafeVars } from '../plugins/sanitize';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* Shared MDX configuration used by
|
|
6
|
-
* and CLI build:file command
|
|
6
|
+
* Shared MDX configuration used by the main build (vite.config.ts),
|
|
7
|
+
* the SSR build (vite.config.ssr.ts), and the CLI build:file command
|
|
8
|
+
* (bin/dory.js). rehype-slug adds stable ids to headings so anchors
|
|
9
|
+
* match between SSR output and client hydration.
|
|
7
10
|
*/
|
|
8
11
|
export const getMdxConfig = (isDevelopment: boolean = false) => ({
|
|
9
12
|
providerImportSource: '@mdx-js/preact',
|
|
10
13
|
remarkPlugins: [remarkGfm, remarkSafeVars],
|
|
14
|
+
rehypePlugins: [rehypeSlug],
|
|
11
15
|
development: isDevelopment,
|
|
12
16
|
});
|