@clidey/dory 0.34.1 → 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 +41 -41
- 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/embed/loader.ts +2 -1
- package/src/embed/widget-iframe.ts +0 -3
- 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.embed-widget.ts +11 -8
- package/vite.config.ts +23 -4
- 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
|
@@ -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[]) {
|
|
@@ -11,7 +11,14 @@ interface FrontmatterGeneratorOptions {
|
|
|
11
11
|
function getNavigationOrder(docsDir: string): string[] {
|
|
12
12
|
const doryJsonPath = path.join(docsDir, 'dory.json');
|
|
13
13
|
if (!fs.existsSync(doryJsonPath)) return [];
|
|
14
|
-
|
|
14
|
+
let doryJson;
|
|
15
|
+
try {
|
|
16
|
+
doryJson = JSON.parse(fs.readFileSync(doryJsonPath, 'utf-8'));
|
|
17
|
+
} catch (error) {
|
|
18
|
+
const detail = error instanceof SyntaxError ? error.message : String(error);
|
|
19
|
+
console.error(`Failed to parse ${doryJsonPath}: ${detail}`);
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
15
22
|
const order: string[] = [];
|
|
16
23
|
|
|
17
24
|
function walkPages(pages: any[]) {
|
|
@@ -109,13 +116,12 @@ function generateFrontmatterJson(docsDir: string): Array<Record<string, any>> {
|
|
|
109
116
|
|
|
110
117
|
const filePath = mdxFilesMap[fileKey];
|
|
111
118
|
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
112
|
-
const { data
|
|
119
|
+
const { data } = parseFrontmatter(rawContent);
|
|
113
120
|
const pathname = pathFromFilename(filePath, docsDir);
|
|
114
121
|
|
|
115
122
|
frontmatterArray.push({
|
|
116
123
|
...data,
|
|
117
|
-
path: pathname
|
|
118
|
-
content: content.trim()
|
|
124
|
+
path: pathname
|
|
119
125
|
});
|
|
120
126
|
}
|
|
121
127
|
|
|
@@ -124,13 +130,12 @@ function generateFrontmatterJson(docsDir: string): Array<Record<string, any>> {
|
|
|
124
130
|
for (const fileKey of extraKeys) {
|
|
125
131
|
const filePath = mdxFilesMap[fileKey];
|
|
126
132
|
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
127
|
-
const { data
|
|
133
|
+
const { data } = parseFrontmatter(rawContent);
|
|
128
134
|
const pathname = pathFromFilename(filePath, docsDir);
|
|
129
135
|
|
|
130
136
|
frontmatterArray.push({
|
|
131
137
|
...data,
|
|
132
|
-
path: pathname
|
|
133
|
-
content: content.trim()
|
|
138
|
+
path: pathname
|
|
134
139
|
});
|
|
135
140
|
}
|
|
136
141
|
|
|
@@ -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[]) {
|
|
@@ -11,7 +11,14 @@ interface LlmTxtGeneratorOptions {
|
|
|
11
11
|
function getNavigationOrder(docsDir: string): string[] {
|
|
12
12
|
const doryJsonPath = path.join(docsDir, 'dory.json');
|
|
13
13
|
if (!fs.existsSync(doryJsonPath)) return [];
|
|
14
|
-
|
|
14
|
+
let doryJson;
|
|
15
|
+
try {
|
|
16
|
+
doryJson = JSON.parse(fs.readFileSync(doryJsonPath, 'utf-8'));
|
|
17
|
+
} catch (error) {
|
|
18
|
+
const detail = error instanceof SyntaxError ? error.message : String(error);
|
|
19
|
+
console.error(`Failed to parse ${doryJsonPath}: ${detail}`);
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
15
22
|
const order: string[] = [];
|
|
16
23
|
|
|
17
24
|
function walkPages(pages: any[]) {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { Plugin, ResolvedConfig } from 'vite';
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generates per-route HTML files with page-specific meta tags at build time.
|
|
7
|
+
* This is NOT full SSR — the Preact app still hydrates client-side.
|
|
8
|
+
* It provides correct <title>, <meta>, canonical, and JSON-LD per route
|
|
9
|
+
* so crawlers see unique, meaningful HTML for each documentation page.
|
|
10
|
+
*/
|
|
11
|
+
export function prerender(): Plugin {
|
|
12
|
+
let config: ResolvedConfig;
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
name: 'prerender',
|
|
16
|
+
configResolved(resolvedConfig) {
|
|
17
|
+
config = resolvedConfig;
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
closeBundle() {
|
|
21
|
+
const outDir = config.build.outDir;
|
|
22
|
+
const frontmatterPath = resolve(outDir, 'frontmatter.json');
|
|
23
|
+
const indexHtmlPath = resolve(outDir, 'index.html');
|
|
24
|
+
|
|
25
|
+
if (!existsSync(frontmatterPath) || !existsSync(indexHtmlPath)) {
|
|
26
|
+
console.warn('prerender: frontmatter.json or index.html not found, skipping');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const frontmatter: Array<Record<string, any>> = JSON.parse(readFileSync(frontmatterPath, 'utf-8'));
|
|
32
|
+
const baseHtml = readFileSync(indexHtmlPath, 'utf-8');
|
|
33
|
+
|
|
34
|
+
// Read dory.json for site-level config
|
|
35
|
+
const doryJsonPath = resolve(process.cwd(), 'docs', 'dory.json');
|
|
36
|
+
let siteConfig: Record<string, any> = {};
|
|
37
|
+
if (existsSync(doryJsonPath)) {
|
|
38
|
+
siteConfig = JSON.parse(readFileSync(doryJsonPath, 'utf-8'));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const baseUrl = siteConfig.url || '';
|
|
42
|
+
const siteName = siteConfig.name || 'Documentation';
|
|
43
|
+
let count = 0;
|
|
44
|
+
|
|
45
|
+
for (const page of frontmatter) {
|
|
46
|
+
const routePath = page.path;
|
|
47
|
+
if (!routePath || routePath === '/') continue;
|
|
48
|
+
|
|
49
|
+
const title = page.title || siteName;
|
|
50
|
+
const description = page.description || `${siteName} - ${title}`;
|
|
51
|
+
const fullUrl = `${baseUrl}${routePath}`;
|
|
52
|
+
|
|
53
|
+
let html = baseHtml;
|
|
54
|
+
|
|
55
|
+
// Replace title
|
|
56
|
+
html = html.replace(/<title>.*?<\/title>/, `<title>${escapeHtml(title)}</title>`);
|
|
57
|
+
|
|
58
|
+
// Replace meta description
|
|
59
|
+
html = html.replace(
|
|
60
|
+
/<meta name="description" content=".*?" \/>/,
|
|
61
|
+
`<meta name="description" content="${escapeAttr(description)}" />`
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Replace OG tags
|
|
65
|
+
html = html.replace(
|
|
66
|
+
/<meta property="og:title" content=".*?" \/>/,
|
|
67
|
+
`<meta property="og:title" content="${escapeAttr(title)}" />`
|
|
68
|
+
);
|
|
69
|
+
html = html.replace(
|
|
70
|
+
/<meta property="og:description" content=".*?" \/>/,
|
|
71
|
+
`<meta property="og:description" content="${escapeAttr(description)}" />`
|
|
72
|
+
);
|
|
73
|
+
if (fullUrl) {
|
|
74
|
+
html = html.replace(
|
|
75
|
+
/<meta property="og:url" content=".*?" \/>/,
|
|
76
|
+
`<meta property="og:url" content="${escapeAttr(fullUrl)}" />`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Replace Twitter tags
|
|
81
|
+
html = html.replace(
|
|
82
|
+
/<meta name="twitter:title" content=".*?" \/>/,
|
|
83
|
+
`<meta name="twitter:title" content="${escapeAttr(title)}" />`
|
|
84
|
+
);
|
|
85
|
+
html = html.replace(
|
|
86
|
+
/<meta name="twitter:description" content=".*?" \/>/,
|
|
87
|
+
`<meta name="twitter:description" content="${escapeAttr(description)}" />`
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Add canonical link
|
|
91
|
+
if (!html.includes('rel="canonical"')) {
|
|
92
|
+
html = html.replace(
|
|
93
|
+
'</head>',
|
|
94
|
+
` <link rel="canonical" href="${escapeAttr(fullUrl)}" />\n </head>`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Write to route directory
|
|
99
|
+
const routeDir = resolve(outDir, routePath.replace(/^\//, ''));
|
|
100
|
+
mkdirSync(routeDir, { recursive: true });
|
|
101
|
+
writeFileSync(resolve(routeDir, 'index.html'), html, 'utf-8');
|
|
102
|
+
count++;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log(`✅ Prerendered ${count} routes with page-specific meta tags`);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error('prerender failed:', error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function escapeHtml(str: string): string {
|
|
114
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function escapeAttr(str: string): string {
|
|
118
|
+
return str.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
119
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
import { readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generates robots.txt with a sitemap reference at build time.
|
|
7
|
+
*/
|
|
8
|
+
export function robotsGenerator(): Plugin {
|
|
9
|
+
return {
|
|
10
|
+
name: 'robots-generator',
|
|
11
|
+
generateBundle() {
|
|
12
|
+
const doryJsonPath = resolve(process.cwd(), 'docs', 'dory.json');
|
|
13
|
+
|
|
14
|
+
let sitemapLine = '';
|
|
15
|
+
if (existsSync(doryJsonPath)) {
|
|
16
|
+
try {
|
|
17
|
+
const config = JSON.parse(readFileSync(doryJsonPath, 'utf-8'));
|
|
18
|
+
if (config.url) {
|
|
19
|
+
sitemapLine = `\nSitemap: ${config.url}/sitemap.xml\n`;
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
// Non-critical, continue without sitemap reference
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const robots = `User-agent: *\nAllow: /\n${sitemapLine}`;
|
|
27
|
+
|
|
28
|
+
this.emitFile({
|
|
29
|
+
type: 'asset',
|
|
30
|
+
fileName: 'robots.txt',
|
|
31
|
+
source: robots
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
console.log('✅ Generated robots.txt');
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
import { readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generates sitemap.xml from dory.json navigation structure at build time.
|
|
7
|
+
* Uses the `url` field from dory.json as the base URL.
|
|
8
|
+
*/
|
|
9
|
+
export function sitemapGenerator(): Plugin {
|
|
10
|
+
return {
|
|
11
|
+
name: 'sitemap-generator',
|
|
12
|
+
generateBundle() {
|
|
13
|
+
const doryJsonPath = resolve(process.cwd(), 'docs', 'dory.json');
|
|
14
|
+
if (!existsSync(doryJsonPath)) {
|
|
15
|
+
console.warn('sitemap-generator: dory.json not found, skipping');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const config = JSON.parse(readFileSync(doryJsonPath, 'utf-8'));
|
|
21
|
+
const baseUrl = config.url || '';
|
|
22
|
+
|
|
23
|
+
// Extract all routes from navigation
|
|
24
|
+
const routes: string[] = [];
|
|
25
|
+
function walkPages(pages: any[]) {
|
|
26
|
+
for (const page of pages) {
|
|
27
|
+
if (typeof page === 'string') {
|
|
28
|
+
routes.push(`/${page}`);
|
|
29
|
+
} else if (typeof page === 'object' && page.pages) {
|
|
30
|
+
walkPages(page.pages);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (config.navigation?.tabs) {
|
|
36
|
+
for (const tab of config.navigation.tabs) {
|
|
37
|
+
if (tab.groups) {
|
|
38
|
+
for (const group of tab.groups) {
|
|
39
|
+
if (group.pages) {
|
|
40
|
+
walkPages(group.pages);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const now = new Date().toISOString().split('T')[0];
|
|
48
|
+
let sitemap = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
|
49
|
+
sitemap += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';
|
|
50
|
+
|
|
51
|
+
for (const route of routes) {
|
|
52
|
+
sitemap += ' <url>\n';
|
|
53
|
+
sitemap += ` <loc>${baseUrl}${route}</loc>\n`;
|
|
54
|
+
sitemap += ` <lastmod>${now}</lastmod>\n`;
|
|
55
|
+
sitemap += ' </url>\n';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
sitemap += '</urlset>';
|
|
59
|
+
|
|
60
|
+
this.emitFile({
|
|
61
|
+
type: 'asset',
|
|
62
|
+
fileName: 'sitemap.xml',
|
|
63
|
+
source: sitemap
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
console.log(`✅ Generated sitemap.xml with ${routes.length} URLs`);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const detail = error instanceof SyntaxError ? error.message : String(error);
|
|
69
|
+
console.error(`Failed to generate sitemap: ${detail}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
package/src/types/config.ts
CHANGED
|
@@ -78,12 +78,21 @@ export interface DoryEmbedConfig {
|
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
export interface DoryAnalytics {
|
|
82
|
+
posthog?: {
|
|
83
|
+
token: string;
|
|
84
|
+
apiHost?: string;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
81
88
|
export interface DoryConfig {
|
|
82
89
|
name: string;
|
|
83
90
|
favicon: string;
|
|
91
|
+
url?: string;
|
|
84
92
|
colors?: DoryColors;
|
|
85
93
|
fonts?: DoryFonts;
|
|
86
94
|
navigation: DoryNavigation;
|
|
87
95
|
logo: DoryLogo;
|
|
88
96
|
embed?: DoryEmbedConfig;
|
|
97
|
+
analytics?: DoryAnalytics;
|
|
89
98
|
}
|
package/src/ui/header.tsx
CHANGED
|
@@ -36,12 +36,7 @@ export function Header({ className }: { className?: string }) {
|
|
|
36
36
|
const isEmbedded = useIsEmbedded();
|
|
37
37
|
|
|
38
38
|
const { light, dark } = useMemo(() => {
|
|
39
|
-
|
|
40
|
-
const normalizePath = (path: string) => path.replace(/^\.\//, '');
|
|
41
|
-
return {
|
|
42
|
-
light: normalizePath(config.logo.light),
|
|
43
|
-
dark: normalizePath(config.logo.dark)
|
|
44
|
-
};
|
|
39
|
+
return { light: config.logo.light, dark: config.logo.dark };
|
|
45
40
|
}, []);
|
|
46
41
|
|
|
47
42
|
useEffect(() => {
|
|
@@ -68,8 +63,8 @@ export function Header({ className }: { className?: string }) {
|
|
|
68
63
|
>
|
|
69
64
|
<div className="relative flex justify-between gap-6 sm:gap-8 md:grow items-center w-full px-4">
|
|
70
65
|
<div className="flex items-center grow gap-2">
|
|
71
|
-
<img src={
|
|
72
|
-
<img src={
|
|
66
|
+
<img src={`/${light}`} alt="logo" className="w-8 hidden dark:block" />
|
|
67
|
+
<img src={`/${dark}`} alt="logo" className="w-8 block dark:hidden" />
|
|
73
68
|
<h1 className="text-base lg:text-2xl font-bold">{config.name}</h1>
|
|
74
69
|
</div>
|
|
75
70
|
<nav className="hidden md:block">
|
package/src/ui/layout.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import Dropdown from '../components/dropdown';
|
|
|
13
13
|
import { useNotification } from '../components/notification';
|
|
14
14
|
import { SparkleIcon } from 'lucide-react';
|
|
15
15
|
import { useIsEmbedded } from '../components/hooks';
|
|
16
|
+
import { ErrorBoundary } from '../components/error-boundary';
|
|
16
17
|
import { cn } from '@clidey/ux';
|
|
17
18
|
|
|
18
19
|
interface LayoutProps {
|
|
@@ -140,6 +141,52 @@ export default function Layout({ children }: LayoutProps) {
|
|
|
140
141
|
if (ogUrl) {
|
|
141
142
|
ogUrl.setAttribute('content', window.location.href);
|
|
142
143
|
}
|
|
144
|
+
|
|
145
|
+
// Add/update canonical link
|
|
146
|
+
let canonical = document.querySelector('link[rel="canonical"]') as HTMLLinkElement | null;
|
|
147
|
+
if (!canonical) {
|
|
148
|
+
canonical = document.createElement('link');
|
|
149
|
+
canonical.setAttribute('rel', 'canonical');
|
|
150
|
+
document.head.appendChild(canonical);
|
|
151
|
+
}
|
|
152
|
+
canonical.setAttribute('href', `${window.location.origin}${window.location.pathname}`);
|
|
153
|
+
|
|
154
|
+
// Add/update JSON-LD structured data
|
|
155
|
+
const structuredData = {
|
|
156
|
+
'@context': 'https://schema.org',
|
|
157
|
+
'@graph': [
|
|
158
|
+
{
|
|
159
|
+
'@type': 'Article',
|
|
160
|
+
'headline': page?.title || document.title,
|
|
161
|
+
'description': page?.description || '',
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
'@type': 'BreadcrumbList',
|
|
165
|
+
'itemListElement': [
|
|
166
|
+
{
|
|
167
|
+
'@type': 'ListItem',
|
|
168
|
+
'position': 1,
|
|
169
|
+
'name': 'Home',
|
|
170
|
+
'item': window.location.origin
|
|
171
|
+
},
|
|
172
|
+
...(page?.title ? [{
|
|
173
|
+
'@type': 'ListItem',
|
|
174
|
+
'position': 2,
|
|
175
|
+
'name': page.title,
|
|
176
|
+
'item': window.location.href
|
|
177
|
+
}] : [])
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
let ldScript = document.querySelector('script[type="application/ld+json"]');
|
|
184
|
+
if (!ldScript) {
|
|
185
|
+
ldScript = document.createElement('script');
|
|
186
|
+
ldScript.setAttribute('type', 'application/ld+json');
|
|
187
|
+
document.head.appendChild(ldScript);
|
|
188
|
+
}
|
|
189
|
+
ldScript.textContent = JSON.stringify(structuredData);
|
|
143
190
|
}, [page]);
|
|
144
191
|
|
|
145
192
|
const tab = useMemo(() => {
|
|
@@ -217,11 +264,22 @@ export default function Layout({ children }: LayoutProps) {
|
|
|
217
264
|
}
|
|
218
265
|
</div>
|
|
219
266
|
<div className="flex-1 min-h-[calc(100vh-2rem)]">
|
|
220
|
-
{
|
|
221
|
-
<
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
267
|
+
<ErrorBoundary fallback={(error) => (
|
|
268
|
+
<div className="flex flex-col items-center justify-center py-16 px-6 text-center">
|
|
269
|
+
<p className="text-lg font-semibold text-red-700 dark:text-red-300 mb-2">
|
|
270
|
+
This page failed to render
|
|
271
|
+
</p>
|
|
272
|
+
<pre className="text-sm text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-md p-3 max-w-md overflow-auto">
|
|
273
|
+
{error.message}
|
|
274
|
+
</pre>
|
|
275
|
+
</div>
|
|
276
|
+
)}>
|
|
277
|
+
{openAPIJSON && tab && "openapi" in tab && method && path ?
|
|
278
|
+
<OpenAPI openAPIJson={JSON.parse(openAPIJSON)} method={method} path={path} />
|
|
279
|
+
: asyncAPIJSON && tab && "asyncapi" in tab && operation && channel ?
|
|
280
|
+
<AsyncAPI asyncAPIJson={JSON.parse(asyncAPIJSON)} operation={operation} channel={channel} />
|
|
281
|
+
: children}
|
|
282
|
+
</ErrorBoundary>
|
|
225
283
|
</div>
|
|
226
284
|
<PrevNextLinks />
|
|
227
285
|
</article>
|
|
@@ -3,14 +3,11 @@ import { resolve } from 'path';
|
|
|
3
3
|
|
|
4
4
|
export default defineConfig({
|
|
5
5
|
build: {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
name: '__DoryWidget__',
|
|
12
|
-
inlineDynamicImports: true,
|
|
13
|
-
},
|
|
6
|
+
lib: {
|
|
7
|
+
entry: resolve(__dirname, 'src/embed/widget-iframe.ts'),
|
|
8
|
+
name: '__DoryWidget__',
|
|
9
|
+
fileName: () => 'embed-widget.js',
|
|
10
|
+
formats: ['es'],
|
|
14
11
|
},
|
|
15
12
|
outDir: 'dist',
|
|
16
13
|
emptyOutDir: false,
|
|
@@ -24,5 +21,11 @@ export default defineConfig({
|
|
|
24
21
|
safari10: true,
|
|
25
22
|
},
|
|
26
23
|
},
|
|
24
|
+
rollupOptions: {
|
|
25
|
+
output: {
|
|
26
|
+
format: 'es',
|
|
27
|
+
inlineDynamicImports: false,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
27
30
|
},
|
|
28
31
|
});
|
package/vite.config.ts
CHANGED
|
@@ -10,11 +10,14 @@ import { frontmatterDevServer } from './src/plugins/frontmatter-dev-server';
|
|
|
10
10
|
import { docsAssetsPlugin } from './src/plugins/docs-assets';
|
|
11
11
|
import { htmlMetadataInjector } from './src/plugins/html-metadata-injector';
|
|
12
12
|
import { htmlFilesMiddleware } from './src/plugins/html-files-middleware';
|
|
13
|
-
import {
|
|
13
|
+
import { analyticsInjector } from './src/plugins/analytics-injector';
|
|
14
|
+
import { cspGenerator } from './src/plugins/csp-generator';
|
|
15
|
+
import { sitemapGenerator } from './src/plugins/sitemap-generator';
|
|
16
|
+
import { robotsGenerator } from './src/plugins/robots-generator';
|
|
17
|
+
import { prerender } from './src/plugins/prerender';
|
|
14
18
|
import { getMdxConfig } from './src/config/mdx';
|
|
15
19
|
|
|
16
20
|
export default defineConfig(({ command }) => ({
|
|
17
|
-
base: process.env.VITE_BASE || '/',
|
|
18
21
|
plugins: [
|
|
19
22
|
// SINGLE SOURCE OF TRUTH: All MDX preprocessing happens in src/mdx/processor.ts
|
|
20
23
|
htmlFilesMiddleware(), // Must be first to intercept HTML file requests
|
|
@@ -26,9 +29,13 @@ export default defineConfig(({ command }) => ({
|
|
|
26
29
|
llmTxtDevServer(),
|
|
27
30
|
frontmatterGenerator(),
|
|
28
31
|
frontmatterDevServer(),
|
|
29
|
-
manifestGenerator(),
|
|
30
32
|
docsAssetsPlugin(),
|
|
31
33
|
htmlMetadataInjector(),
|
|
34
|
+
analyticsInjector(),
|
|
35
|
+
cspGenerator(),
|
|
36
|
+
sitemapGenerator(),
|
|
37
|
+
robotsGenerator(),
|
|
38
|
+
prerender(),
|
|
32
39
|
],
|
|
33
40
|
resolve: {
|
|
34
41
|
alias: {
|
|
@@ -37,11 +44,23 @@ export default defineConfig(({ command }) => ({
|
|
|
37
44
|
}
|
|
38
45
|
},
|
|
39
46
|
publicDir: false, // Don't use a public directory since we need to import from docs
|
|
47
|
+
build: {
|
|
48
|
+
rollupOptions: {
|
|
49
|
+
output: {
|
|
50
|
+
manualChunks: {
|
|
51
|
+
'vendor-preact': ['preact', 'preact/compat', 'preact/hooks'],
|
|
52
|
+
'vendor-ui': ['@clidey/ux', '@headlessui/react'],
|
|
53
|
+
'vendor-icons': ['lucide-react'],
|
|
54
|
+
'katex': ['katex'],
|
|
55
|
+
'flexsearch': ['flexsearch'],
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
40
60
|
server: {
|
|
41
61
|
allowedHosts: true,
|
|
42
62
|
port: 3000,
|
|
43
63
|
strictPort: false,
|
|
44
|
-
cors: true,
|
|
45
64
|
},
|
|
46
65
|
json: {
|
|
47
66
|
stringify: false
|
package/bin/dist/dory.d.ts
DELETED
package/bin/dist/dory.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"dory.d.ts","sourceRoot":"","sources":["../dory.ts"],"names":[],"mappings":""}
|