@clidey/dory 0.41.0 → 0.43.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 +4 -3
- package/package.json +6 -3
- package/scripts/generate-frontmatter-json.js +154 -0
- package/scripts/generate-llm-txt.js +115 -0
- package/scripts/render-routes.mjs +94 -0
- package/src/components/hooks.tsx +23 -3
- package/src/entry-server.tsx +106 -0
- package/src/mdx/fence.tsx +1 -1
- package/src/mdx/mdx.tsx +1 -1
- package/src/mdx/processor.ts +18 -2
- package/src/plugins/html-metadata-injector.ts +37 -24
- package/src/plugins/prerender.ts +51 -13
- package/src/plugins/sitemap-generator.ts +21 -7
- package/src/plugins/unified-mdx-plugin.ts +1 -1
- package/src/types/config.ts +1 -0
- package/src/ui/header.tsx +1 -1
- package/src/ui/layout.tsx +2 -2
- package/vite.config.ssr.ts +32 -0
- package/vite.config.ts +0 -2
- package/src/plugins/csp-generator.ts +0 -68
package/index.html
CHANGED
|
@@ -26,12 +26,13 @@
|
|
|
26
26
|
<meta name="twitter:title" content="Docucod: Generate and Maintain Documentation With No Work" />
|
|
27
27
|
<meta name="twitter:description" content="Docucod auto-generates and maintains up-to-date documentation from your codebase so your team can move fast, onboard quickly, and stay focused on building." />
|
|
28
28
|
<meta name="twitter:image" content="./docs/favicon.svg" />
|
|
29
|
-
<meta name="twitter:site" content="
|
|
30
|
-
<meta name="twitter:creator" content="
|
|
29
|
+
<meta name="twitter:site" content="" />
|
|
30
|
+
<meta name="twitter:creator" content="" />
|
|
31
31
|
|
|
32
|
+
<link rel="canonical" href="" />
|
|
32
33
|
<link rel="apple-touch-icon" href="./docs/favicon.svg" />
|
|
33
34
|
<link rel="manifest" href="/manifest.json" />
|
|
34
|
-
|
|
35
|
+
|
|
35
36
|
<title>Docucod - Instantly Generate and Maintain Your Documentation</title>
|
|
36
37
|
|
|
37
38
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
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.43.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A CLI tool for building and previewing documentation sites",
|
|
7
7
|
"keywords": [
|
|
@@ -17,8 +17,10 @@
|
|
|
17
17
|
"files": [
|
|
18
18
|
"bin/dist",
|
|
19
19
|
"src",
|
|
20
|
+
"scripts",
|
|
20
21
|
"index.html",
|
|
21
22
|
"vite.config.ts",
|
|
23
|
+
"vite.config.ssr.ts",
|
|
22
24
|
"vite.config.embed-loader.ts",
|
|
23
25
|
"vite.config.embed-widget.ts"
|
|
24
26
|
],
|
|
@@ -34,9 +36,9 @@
|
|
|
34
36
|
},
|
|
35
37
|
"scripts": {
|
|
36
38
|
"dev": "vite",
|
|
37
|
-
"build": "npm run build:cli && tsc -b &&
|
|
39
|
+
"build": "npm run build:cli && tsc -b && npm run build:docs",
|
|
38
40
|
"build:cli": "tsc -p tsconfig.bin.json",
|
|
39
|
-
"build:docs": "vite build",
|
|
41
|
+
"build:docs": "vite build && vite build --config vite.config.ssr.ts && node scripts/render-routes.mjs",
|
|
40
42
|
"build:embed": "npm run build:embed:loader && npm run build:embed:widget",
|
|
41
43
|
"build:embed:loader": "vite build -c vite.config.embed-loader.ts",
|
|
42
44
|
"build:embed:widget": "vite build -c vite.config.embed-widget.ts",
|
|
@@ -69,6 +71,7 @@
|
|
|
69
71
|
"mermaid": "11.12.2",
|
|
70
72
|
"motion": "12.34.0",
|
|
71
73
|
"preact": "10.28.3",
|
|
74
|
+
"preact-render-to-string": "6.6.5",
|
|
72
75
|
"prism-react-renderer": "2.4.1",
|
|
73
76
|
"react-highlight-words": "0.21.0",
|
|
74
77
|
"remark-gfm": "4.0.1",
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
// Read dory.json to get navigation order (flat list, in order)
|
|
11
|
+
function getNavigationOrder(docsDir) {
|
|
12
|
+
const doryJsonPath = path.join(docsDir, 'dory.json');
|
|
13
|
+
if (!fs.existsSync(doryJsonPath)) return [];
|
|
14
|
+
const doryJson = JSON.parse(fs.readFileSync(doryJsonPath, 'utf-8'));
|
|
15
|
+
const order = [];
|
|
16
|
+
|
|
17
|
+
function walkPages(pages) {
|
|
18
|
+
for (const page of pages) {
|
|
19
|
+
if (typeof page === 'string') {
|
|
20
|
+
order.push(page);
|
|
21
|
+
} else if (typeof page === 'object' && page.pages) {
|
|
22
|
+
walkPages(page.pages);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (doryJson.navigation && Array.isArray(doryJson.navigation.tabs)) {
|
|
28
|
+
for (const tab of doryJson.navigation.tabs) {
|
|
29
|
+
if (tab.groups && Array.isArray(tab.groups)) {
|
|
30
|
+
for (const group of tab.groups) {
|
|
31
|
+
if (group.pages && Array.isArray(group.pages)) {
|
|
32
|
+
walkPages(group.pages);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return order;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Function to parse frontmatter from MDX content
|
|
42
|
+
function parseFrontmatter(content) {
|
|
43
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
44
|
+
if (!frontmatterMatch) return { data: {}, content };
|
|
45
|
+
|
|
46
|
+
const frontmatter = frontmatterMatch[1];
|
|
47
|
+
const data = {};
|
|
48
|
+
|
|
49
|
+
frontmatter.split('\n').forEach(line => {
|
|
50
|
+
const [key, ...valueParts] = line.split(':');
|
|
51
|
+
if (key && valueParts.length) {
|
|
52
|
+
const value = valueParts.join(':').trim();
|
|
53
|
+
data[key.trim()] = value.replace(/^["']|["']$/g, '');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return { data, content: content.slice(frontmatterMatch[0].length) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Convert filename to path format used by the store
|
|
61
|
+
function pathFromFilename(filename, docsDir) {
|
|
62
|
+
return filename
|
|
63
|
+
.replace(docsDir, '')
|
|
64
|
+
.replace(/\/?index\.mdx$/, '/')
|
|
65
|
+
.replace(/\.mdx$/, '')
|
|
66
|
+
.replace(/\\/g, '/')
|
|
67
|
+
.toLowerCase();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Function to recursively find all MDX files and map by relative path (without .mdx)
|
|
71
|
+
function findMdxFilesMap(dir, baseDir) {
|
|
72
|
+
const files = {};
|
|
73
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
74
|
+
|
|
75
|
+
for (const item of items) {
|
|
76
|
+
const fullPath = path.join(dir, item.name);
|
|
77
|
+
if (item.isDirectory()) {
|
|
78
|
+
Object.assign(files, findMdxFilesMap(fullPath, baseDir));
|
|
79
|
+
} else if (item.isFile() && item.name.endsWith('.mdx')) {
|
|
80
|
+
// Key: relative path without .mdx, with forward slashes
|
|
81
|
+
let rel = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
|
82
|
+
if (rel.endsWith('.mdx')) rel = rel.slice(0, -4);
|
|
83
|
+
files[rel] = fullPath;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return files;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Main function to generate frontmatter JSON
|
|
91
|
+
async function generateFrontmatterJson() {
|
|
92
|
+
const docsDir = path.join(__dirname, '..', 'docs');
|
|
93
|
+
const outputDir = path.join(__dirname, '..', 'dist');
|
|
94
|
+
|
|
95
|
+
// Ensure output directory exists
|
|
96
|
+
if (!fs.existsSync(outputDir)) {
|
|
97
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const navOrder = getNavigationOrder(docsDir);
|
|
101
|
+
const mdxFilesMap = findMdxFilesMap(docsDir, docsDir);
|
|
102
|
+
const usedKeys = new Set();
|
|
103
|
+
const frontmatterArray = [];
|
|
104
|
+
|
|
105
|
+
// Process files in navigation order first
|
|
106
|
+
for (const navKey of navOrder) {
|
|
107
|
+
// Try both with and without "index"
|
|
108
|
+
let fileKey = navKey;
|
|
109
|
+
if (mdxFilesMap[fileKey]) {
|
|
110
|
+
usedKeys.add(fileKey);
|
|
111
|
+
} else if (mdxFilesMap[`${fileKey}/index`]) {
|
|
112
|
+
fileKey = `${fileKey}/index`;
|
|
113
|
+
usedKeys.add(fileKey);
|
|
114
|
+
} else {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const filePath = mdxFilesMap[fileKey];
|
|
119
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
120
|
+
const { data } = parseFrontmatter(rawContent);
|
|
121
|
+
const pathname = pathFromFilename(filePath, docsDir);
|
|
122
|
+
|
|
123
|
+
frontmatterArray.push({
|
|
124
|
+
...data,
|
|
125
|
+
path: pathname
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Process any extra files not in navigation order, sorted
|
|
130
|
+
const extraKeys = Object.keys(mdxFilesMap).filter(k => !usedKeys.has(k)).sort();
|
|
131
|
+
for (const fileKey of extraKeys) {
|
|
132
|
+
const filePath = mdxFilesMap[fileKey];
|
|
133
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
134
|
+
const { data } = parseFrontmatter(rawContent);
|
|
135
|
+
const pathname = pathFromFilename(filePath, docsDir);
|
|
136
|
+
|
|
137
|
+
frontmatterArray.push({
|
|
138
|
+
...data,
|
|
139
|
+
path: pathname
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Write to output file
|
|
144
|
+
const jsonContent = JSON.stringify(frontmatterArray, null, 2);
|
|
145
|
+
const outputPath = path.join(outputDir, 'frontmatter.json');
|
|
146
|
+
fs.writeFileSync(outputPath, jsonContent);
|
|
147
|
+
|
|
148
|
+
console.log(`✅ Generated frontmatter JSON file: ${outputPath}`);
|
|
149
|
+
console.log(`📄 Processed ${frontmatterArray.length} MDX files`);
|
|
150
|
+
console.log(`📝 Total JSON size: ${jsonContent.length} characters`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Run the script
|
|
154
|
+
generateFrontmatterJson().catch(console.error);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
// Function to parse frontmatter from MDX content
|
|
11
|
+
function parseFrontmatter(content) {
|
|
12
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
|
|
13
|
+
if (!frontmatterMatch) return { data: {}, content };
|
|
14
|
+
|
|
15
|
+
const frontmatter = frontmatterMatch[1];
|
|
16
|
+
const data = {};
|
|
17
|
+
|
|
18
|
+
frontmatter.split('\n').forEach(line => {
|
|
19
|
+
const [key, ...valueParts] = line.split(':');
|
|
20
|
+
if (key && valueParts.length) {
|
|
21
|
+
const value = valueParts.join(':').trim();
|
|
22
|
+
data[key.trim()] = value.replace(/^["']|["']$/g, '');
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return { data, content: content.slice(frontmatterMatch[0].length) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Function to recursively find all MDX files
|
|
30
|
+
function findMdxFiles(dir) {
|
|
31
|
+
const files = [];
|
|
32
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
33
|
+
|
|
34
|
+
for (const item of items) {
|
|
35
|
+
const fullPath = path.join(dir, item.name);
|
|
36
|
+
if (item.isDirectory()) {
|
|
37
|
+
files.push(...findMdxFiles(fullPath));
|
|
38
|
+
} else if (item.isFile() && item.name.endsWith('.mdx')) {
|
|
39
|
+
files.push(fullPath);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return files;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Function to clean MDX content for LLM consumption
|
|
47
|
+
function cleanMdxContent(content) {
|
|
48
|
+
// Remove JSX components but keep their content
|
|
49
|
+
let cleaned = content
|
|
50
|
+
// Remove JSX opening/closing tags but keep content
|
|
51
|
+
.replace(/<(\w+)([^>]*?)>/g, '')
|
|
52
|
+
.replace(/<\/\w+>/g, '')
|
|
53
|
+
// Remove code block language specifiers
|
|
54
|
+
.replace(/```(\w+)/g, '```')
|
|
55
|
+
// Remove HTML comments
|
|
56
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
57
|
+
// Remove excessive whitespace
|
|
58
|
+
.replace(/\n\s*\n\s*\n/g, '\n\n')
|
|
59
|
+
// Remove leading/trailing whitespace
|
|
60
|
+
.trim();
|
|
61
|
+
|
|
62
|
+
return cleaned;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Main function to generate LLM text
|
|
66
|
+
async function generateLlmTxt() {
|
|
67
|
+
const docsDir = path.join(__dirname, '..', 'docs');
|
|
68
|
+
const outputDir = path.join(__dirname, '..', 'dist');
|
|
69
|
+
|
|
70
|
+
// Ensure output directory exists
|
|
71
|
+
if (!fs.existsSync(outputDir)) {
|
|
72
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const mdxFiles = findMdxFiles(docsDir);
|
|
76
|
+
let aggregatedContent = '';
|
|
77
|
+
|
|
78
|
+
// Sort files for consistent output
|
|
79
|
+
mdxFiles.sort();
|
|
80
|
+
|
|
81
|
+
for (const filePath of mdxFiles) {
|
|
82
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
83
|
+
const { data, content } = parseFrontmatter(rawContent);
|
|
84
|
+
const cleanedContent = cleanMdxContent(content);
|
|
85
|
+
|
|
86
|
+
// Add file header
|
|
87
|
+
const relativePath = path.relative(docsDir, filePath);
|
|
88
|
+
aggregatedContent += `\n# ${relativePath}\n`;
|
|
89
|
+
|
|
90
|
+
// Add title if available
|
|
91
|
+
if (data.title) {
|
|
92
|
+
aggregatedContent += `## ${data.title}\n\n`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Add description if available
|
|
96
|
+
if (data.description) {
|
|
97
|
+
aggregatedContent += `${data.description}\n\n`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Add cleaned content
|
|
101
|
+
aggregatedContent += cleanedContent + '\n\n';
|
|
102
|
+
aggregatedContent += '---\n\n';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Write to output file
|
|
106
|
+
const outputPath = path.join(outputDir, 'llms.txt');
|
|
107
|
+
fs.writeFileSync(outputPath, aggregatedContent.trim());
|
|
108
|
+
|
|
109
|
+
console.log(`✅ Generated LLM text file: ${outputPath}`);
|
|
110
|
+
console.log(`📄 Processed ${mdxFiles.length} MDX files`);
|
|
111
|
+
console.log(`📝 Total content size: ${aggregatedContent.length} characters`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Run the script
|
|
115
|
+
generateLlmTxt().catch(console.error);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-build SSR rendering script.
|
|
3
|
+
* Reads the SSR bundle from dist-ssr/, renders each route to HTML,
|
|
4
|
+
* and injects the content into the prerendered HTML files in dist/.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync, rmSync } from 'fs';
|
|
7
|
+
import { resolve, dirname } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const distDir = resolve(__dirname, '..', 'dist');
|
|
12
|
+
const ssrDir = resolve(__dirname, '..', 'dist-ssr');
|
|
13
|
+
|
|
14
|
+
// Check prerequisites
|
|
15
|
+
const frontmatterPath = resolve(distDir, 'frontmatter.json');
|
|
16
|
+
if (!existsSync(frontmatterPath)) {
|
|
17
|
+
console.warn('⚠️ frontmatter.json not found, skipping SSR rendering');
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ssrEntryPath = resolve(ssrDir, 'entry-server.js');
|
|
22
|
+
if (!existsSync(ssrEntryPath)) {
|
|
23
|
+
console.warn('⚠️ SSR bundle not found, skipping SSR rendering');
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Import SSR bundle
|
|
28
|
+
const { render } = await import(ssrEntryPath);
|
|
29
|
+
const frontmatter = JSON.parse(readFileSync(frontmatterPath, 'utf-8'));
|
|
30
|
+
|
|
31
|
+
let count = 0;
|
|
32
|
+
let failed = 0;
|
|
33
|
+
|
|
34
|
+
function injectSSR(htmlPath, ssrHtml) {
|
|
35
|
+
let html = readFileSync(htmlPath, 'utf-8');
|
|
36
|
+
|
|
37
|
+
html = html.replace(
|
|
38
|
+
'<div id="app"></div>',
|
|
39
|
+
`<div id="app">${ssrHtml}</div>`
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
html = html.replace(
|
|
43
|
+
'<noscript>You need to enable JavaScript to run this app.</noscript>',
|
|
44
|
+
'<noscript>JavaScript enhances this page with interactive features.</noscript>'
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
writeFileSync(htmlPath, html, 'utf-8');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Render each route
|
|
51
|
+
for (const page of frontmatter) {
|
|
52
|
+
const routePath = page.path;
|
|
53
|
+
if (!routePath || routePath === '/') continue;
|
|
54
|
+
|
|
55
|
+
const htmlPath = resolve(distDir, routePath.replace(/^\//, ''), 'index.html');
|
|
56
|
+
if (!existsSync(htmlPath)) continue;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const ssrHtml = await render(routePath);
|
|
60
|
+
if (!ssrHtml) continue;
|
|
61
|
+
|
|
62
|
+
injectSSR(htmlPath, ssrHtml);
|
|
63
|
+
count++;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.warn(`⚠️ SSR failed for ${routePath}: ${error.message}`);
|
|
66
|
+
failed++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Also render the root index.html with the first page's content
|
|
71
|
+
try {
|
|
72
|
+
const rootHtmlPath = resolve(distDir, 'index.html');
|
|
73
|
+
if (existsSync(rootHtmlPath) && frontmatter.length > 0) {
|
|
74
|
+
const firstRoute = frontmatter[0].path;
|
|
75
|
+
if (firstRoute) {
|
|
76
|
+
const ssrHtml = await render(firstRoute);
|
|
77
|
+
if (ssrHtml) {
|
|
78
|
+
injectSSR(rootHtmlPath, ssrHtml);
|
|
79
|
+
count++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.warn(`⚠️ SSR failed for root: ${error.message}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Cleanup SSR build artifacts
|
|
88
|
+
try {
|
|
89
|
+
rmSync(ssrDir, { recursive: true, force: true });
|
|
90
|
+
} catch {
|
|
91
|
+
// Non-fatal: dist-ssr will be overwritten on next build
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log(`✅ SSR rendered ${count} routes${failed > 0 ? ` (${failed} failed)` : ''}`);
|
package/src/components/hooks.tsx
CHANGED
|
@@ -2,18 +2,38 @@ import { useEffect, useState } from 'preact/hooks';
|
|
|
2
2
|
import { useLocation } from 'wouter-preact';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Strips trailing slash from a pathname (except root "/").
|
|
6
|
+
*/
|
|
7
|
+
function normalizePathname(pathname: string): string {
|
|
8
|
+
if (pathname !== '/' && pathname.endsWith('/')) {
|
|
9
|
+
return pathname.slice(0, -1);
|
|
10
|
+
}
|
|
11
|
+
return pathname;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Custom hook to get the current pathname, normalized to strip trailing slashes.
|
|
6
16
|
*/
|
|
7
17
|
export const usePathname = () => {
|
|
8
18
|
const [pathname] = useLocation();
|
|
9
|
-
|
|
19
|
+
const normalized = normalizePathname(pathname);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (pathname !== normalized) {
|
|
23
|
+
window.history.replaceState(null, '', normalized + window.location.search + window.location.hash);
|
|
24
|
+
}
|
|
25
|
+
}, [pathname, normalized]);
|
|
26
|
+
|
|
27
|
+
return normalized;
|
|
10
28
|
};
|
|
11
29
|
|
|
12
30
|
/**
|
|
13
31
|
* Custom hook to get the URL search params as a URLSearchParams instance
|
|
14
32
|
*/
|
|
15
33
|
export const useSearchParams = () => {
|
|
16
|
-
const [searchParams, setSearchParams] = useState(
|
|
34
|
+
const [searchParams, setSearchParams] = useState(
|
|
35
|
+
typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : new URLSearchParams()
|
|
36
|
+
);
|
|
17
37
|
|
|
18
38
|
useEffect(() => {
|
|
19
39
|
const handlePopState = () => {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {renderToString} from 'preact-render-to-string';
|
|
2
|
+
import {Router} from 'wouter-preact';
|
|
3
|
+
import {MDXProvider} from '@mdx-js/preact';
|
|
4
|
+
import * as mdxComponents from './mdx/mdx';
|
|
5
|
+
|
|
6
|
+
function pathFromFilename(filename: string): string {
|
|
7
|
+
return filename
|
|
8
|
+
.replace(/^.*?docs/, '')
|
|
9
|
+
.replace(/\/?index\.mdx$/, '/')
|
|
10
|
+
.replace(/\.mdx$/, '')
|
|
11
|
+
.toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const pages = import.meta.glob('../docs/**/*.mdx');
|
|
15
|
+
const allPages = Object.fromEntries(
|
|
16
|
+
Object.entries(pages).map(([path, loader]) => [pathFromFilename(path), loader])
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
// SSR-safe Pre: renders code blocks without syntax highlighting.
|
|
20
|
+
// The client-side Fence component uses useTheme() from @clidey/ux which
|
|
21
|
+
// requires ThemeProvider (localStorage access). Plain pre/code is sufficient
|
|
22
|
+
// for crawlers — syntax highlighting is a visual enhancement.
|
|
23
|
+
function SSRPre(props: any) {
|
|
24
|
+
const child = props.children?.props;
|
|
25
|
+
const className = child?.className || '';
|
|
26
|
+
const code = child?.children || '';
|
|
27
|
+
return (
|
|
28
|
+
<pre class="overflow-x-auto p-4 text-xs">
|
|
29
|
+
<code class={className}>{code}</code>
|
|
30
|
+
</pre>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// SSR-safe Latex: renders the raw expression as text.
|
|
35
|
+
// The client-side Latex component lazy-loads KaTeX in useEffect.
|
|
36
|
+
function SSRLatex({ children }: { children: any }) {
|
|
37
|
+
return (
|
|
38
|
+
<div class="my-4">
|
|
39
|
+
<code>{children}</code>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// SSR-safe table: @clidey/ux's Table component renders a vaul Drawer
|
|
45
|
+
// (Preview drawer) alongside every table. vaul's useScaleBackground
|
|
46
|
+
// accesses document.body at render time, crashing in Node.js.
|
|
47
|
+
function SSRTable(props: any) {
|
|
48
|
+
return (
|
|
49
|
+
<div class="overflow-x-auto rounded-lg my-8">
|
|
50
|
+
<table class="table-auto border-collapse min-w-full" {...props} />
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
function SSRTh(props: any) {
|
|
55
|
+
return <th class="px-4 py-2 text-left text-sm font-medium" {...props} />;
|
|
56
|
+
}
|
|
57
|
+
function SSRTd(props: any) {
|
|
58
|
+
return <td class="px-4 py-2 text-sm" {...props} />;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// SSR-safe placeholders for interactive components that use Suspense/lazy.
|
|
62
|
+
// renderToString cannot handle Suspense boundaries — these components are
|
|
63
|
+
// interactive-only anyway (playgrounds, diagrams) and not meaningful for crawlers.
|
|
64
|
+
function SSRPlaceholder() {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const ssrComponents: Record<string, any> = {
|
|
69
|
+
...mdxComponents,
|
|
70
|
+
pre: SSRPre,
|
|
71
|
+
Latex: SSRLatex,
|
|
72
|
+
table: SSRTable,
|
|
73
|
+
th: SSRTh,
|
|
74
|
+
td: SSRTd,
|
|
75
|
+
APIPlayground: SSRPlaceholder,
|
|
76
|
+
WebSocketPlayground: SSRPlaceholder,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export async function render(routePath: string): Promise<string> {
|
|
80
|
+
const loader = allPages[routePath];
|
|
81
|
+
if (!loader) return '';
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const module = await loader() as any;
|
|
85
|
+
const Content = module.default;
|
|
86
|
+
|
|
87
|
+
return renderToString(
|
|
88
|
+
<Router ssrPath={routePath}>
|
|
89
|
+
<MDXProvider components={ssrComponents}>
|
|
90
|
+
<div class="flex w-full flex-col">
|
|
91
|
+
<main class="flex grow max-w-full px-4 py-16 sm:px-6 lg:px-8">
|
|
92
|
+
<div class="min-w-0 w-full">
|
|
93
|
+
<article class="h-full flex flex-1 flex-col">
|
|
94
|
+
<Content/>
|
|
95
|
+
</article>
|
|
96
|
+
</div>
|
|
97
|
+
</main>
|
|
98
|
+
</div>
|
|
99
|
+
</MDXProvider>
|
|
100
|
+
</Router>
|
|
101
|
+
);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.warn(`SSR render failed for ${routePath}:`, error);
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
}
|
package/src/mdx/fence.tsx
CHANGED
|
@@ -15,7 +15,7 @@ export function Fence({
|
|
|
15
15
|
language: string
|
|
16
16
|
}) {
|
|
17
17
|
const { theme } = useTheme();
|
|
18
|
-
const isDark = theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
18
|
+
const isDark = theme === 'dark' || (theme === 'system' && typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
19
19
|
|
|
20
20
|
if (language === 'mermaid') {
|
|
21
21
|
return (
|
package/src/mdx/mdx.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'preact/hooks';
|
|
2
2
|
import { lazy, Suspense } from 'preact/compat';
|
|
3
|
-
import { usePathname } from '
|
|
3
|
+
import { usePathname } from '../components/hooks';
|
|
4
4
|
import { completeFrontMatter } from '../components/store';
|
|
5
5
|
import { Loading } from '../components/loading';
|
|
6
6
|
|
package/src/mdx/processor.ts
CHANGED
|
@@ -25,12 +25,28 @@ export async function preprocessMdxContent(
|
|
|
25
25
|
error?: Error;
|
|
26
26
|
}> {
|
|
27
27
|
try {
|
|
28
|
+
// Step 0: Strip YAML frontmatter before any processing.
|
|
29
|
+
// Frontmatter is parsed separately by the frontmatter-generator plugin.
|
|
30
|
+
let processedContent = content;
|
|
31
|
+
const frontMatterMatch = processedContent.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
32
|
+
if (frontMatterMatch) {
|
|
33
|
+
processedContent = processedContent.slice(frontMatterMatch[0].length);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Step 0b: Strip stray ```yaml metadata blocks that follow frontmatter.
|
|
37
|
+
// Some content generators produce a second YAML block (```yaml...---) after
|
|
38
|
+
// the frontmatter. This is a broken code fence (closed with --- instead of ```)
|
|
39
|
+
// that leaks metadata into the rendered page.
|
|
40
|
+
const yamlBlockMatch = processedContent.match(/^```yaml\n([\s\S]*?)\n---\n?/);
|
|
41
|
+
if (yamlBlockMatch) {
|
|
42
|
+
processedContent = processedContent.slice(yamlBlockMatch[0].length);
|
|
43
|
+
}
|
|
44
|
+
|
|
28
45
|
// Step 1: Apply preprocessor (handles MDX tags, code blocks, etc.)
|
|
29
46
|
const preprocessor = preprocessMdxTags();
|
|
30
|
-
let processedContent = content;
|
|
31
47
|
|
|
32
48
|
if (preprocessor.transform) {
|
|
33
|
-
const result = preprocessor.transform(
|
|
49
|
+
const result = preprocessor.transform(processedContent, fileName);
|
|
34
50
|
if (result && typeof result === 'object' && result.code) {
|
|
35
51
|
processedContent = result.code;
|
|
36
52
|
} else if (typeof result === 'string') {
|
|
@@ -3,14 +3,13 @@ import { readFileSync, existsSync } from 'fs';
|
|
|
3
3
|
import { resolve } from 'path';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Vite plugin that injects site metadata from dory.json into index.html
|
|
7
|
-
*
|
|
6
|
+
* Vite plugin that injects site metadata from dory.json into index.html.
|
|
7
|
+
* Provides proper defaults before JavaScript hydrates the page.
|
|
8
8
|
*/
|
|
9
9
|
export function htmlMetadataInjector(): Plugin {
|
|
10
10
|
return {
|
|
11
11
|
name: 'html-metadata-injector',
|
|
12
12
|
transformIndexHtml(html) {
|
|
13
|
-
// Read dory.json configuration
|
|
14
13
|
const doryJsonPath = resolve(process.cwd(), 'docs', 'dory.json');
|
|
15
14
|
|
|
16
15
|
if (!existsSync(doryJsonPath)) {
|
|
@@ -24,7 +23,13 @@ export function htmlMetadataInjector(): Plugin {
|
|
|
24
23
|
const defaultTitle = config.title || siteName;
|
|
25
24
|
const defaultDescription = config.description || `${siteName} - Technical Documentation`;
|
|
26
25
|
const siteUrl = config.url || '';
|
|
27
|
-
const
|
|
26
|
+
const twitterHandle = config.twitter || '';
|
|
27
|
+
|
|
28
|
+
// Resolve image to absolute URL
|
|
29
|
+
const rawImage = config.image || '';
|
|
30
|
+
const siteImage = rawImage && siteUrl && !rawImage.startsWith('http')
|
|
31
|
+
? `${siteUrl}${rawImage.startsWith('/') ? '' : '/'}${rawImage}`
|
|
32
|
+
: rawImage;
|
|
28
33
|
|
|
29
34
|
// Replace title
|
|
30
35
|
html = html.replace(
|
|
@@ -38,31 +43,25 @@ export function htmlMetadataInjector(): Plugin {
|
|
|
38
43
|
`<meta name="description" content="${defaultDescription}" />`
|
|
39
44
|
);
|
|
40
45
|
|
|
41
|
-
// Replace Open Graph
|
|
46
|
+
// Replace Open Graph tags
|
|
42
47
|
html = html.replace(
|
|
43
48
|
/<meta property="og:title" content=".*?" \/>/,
|
|
44
49
|
`<meta property="og:title" content="${defaultTitle}" />`
|
|
45
50
|
);
|
|
46
|
-
|
|
47
|
-
// Replace Open Graph description
|
|
48
51
|
html = html.replace(
|
|
49
52
|
/<meta property="og:description" content=".*?" \/>/,
|
|
50
53
|
`<meta property="og:description" content="${defaultDescription}" />`
|
|
51
54
|
);
|
|
52
|
-
|
|
53
|
-
// Replace Open Graph site name
|
|
54
55
|
html = html.replace(
|
|
55
56
|
/<meta property="og:site_name" content=".*?" \/>/,
|
|
56
57
|
`<meta property="og:site_name" content="${siteName}" />`
|
|
57
58
|
);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// Replace Open Graph URL
|
|
59
|
+
if (siteImage) {
|
|
60
|
+
html = html.replace(
|
|
61
|
+
/<meta property="og:image" content=".*?" \/>/,
|
|
62
|
+
`<meta property="og:image" content="${siteImage}" />`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
66
65
|
if (siteUrl) {
|
|
67
66
|
html = html.replace(
|
|
68
67
|
/<meta property="og:url" content=".*?" \/>/,
|
|
@@ -70,24 +69,38 @@ export function htmlMetadataInjector(): Plugin {
|
|
|
70
69
|
);
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
// Replace Twitter
|
|
72
|
+
// Replace Twitter tags
|
|
74
73
|
html = html.replace(
|
|
75
74
|
/<meta name="twitter:title" content=".*?" \/>/,
|
|
76
75
|
`<meta name="twitter:title" content="${defaultTitle}" />`
|
|
77
76
|
);
|
|
78
|
-
|
|
79
|
-
// Replace Twitter description
|
|
80
77
|
html = html.replace(
|
|
81
78
|
/<meta name="twitter:description" content=".*?" \/>/,
|
|
82
79
|
`<meta name="twitter:description" content="${defaultDescription}" />`
|
|
83
80
|
);
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
if (siteImage) {
|
|
82
|
+
html = html.replace(
|
|
83
|
+
/<meta name="twitter:image" content=".*?" \/>/,
|
|
84
|
+
`<meta name="twitter:image" content="${siteImage}" />`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
html = html.replace(
|
|
88
|
+
/<meta name="twitter:site" content=".*?" \/>/,
|
|
89
|
+
`<meta name="twitter:site" content="${twitterHandle}" />`
|
|
90
|
+
);
|
|
86
91
|
html = html.replace(
|
|
87
|
-
/<meta name="twitter:
|
|
88
|
-
`<meta name="twitter:
|
|
92
|
+
/<meta name="twitter:creator" content=".*?" \/>/,
|
|
93
|
+
`<meta name="twitter:creator" content="${twitterHandle}" />`
|
|
89
94
|
);
|
|
90
95
|
|
|
96
|
+
// Set canonical URL for the base page
|
|
97
|
+
if (siteUrl) {
|
|
98
|
+
html = html.replace(
|
|
99
|
+
/<link rel="canonical" href=".*?" \/>/,
|
|
100
|
+
`<link rel="canonical" href="${siteUrl}" />`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
91
104
|
return html;
|
|
92
105
|
} catch (error) {
|
|
93
106
|
console.error('❌ Failed to parse dory.json for metadata injection:', error);
|
package/src/plugins/prerender.ts
CHANGED
|
@@ -3,10 +3,9 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
|
3
3
|
import { resolve } from 'path';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Generates per-route HTML files with page-specific meta tags
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* so crawlers see unique, meaningful HTML for each documentation page.
|
|
6
|
+
* Generates per-route HTML files with page-specific meta tags and JSON-LD
|
|
7
|
+
* at build time. The SSR step (render-routes.mjs) later injects rendered
|
|
8
|
+
* content into these files.
|
|
10
9
|
*/
|
|
11
10
|
export function prerender(): Plugin {
|
|
12
11
|
let config: ResolvedConfig;
|
|
@@ -70,12 +69,10 @@ export function prerender(): Plugin {
|
|
|
70
69
|
/<meta property="og:description" content=".*?" \/>/,
|
|
71
70
|
`<meta property="og:description" content="${escapeAttr(description)}" />`
|
|
72
71
|
);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
);
|
|
78
|
-
}
|
|
72
|
+
html = html.replace(
|
|
73
|
+
/<meta property="og:url" content=".*?" \/>/,
|
|
74
|
+
`<meta property="og:url" content="${escapeAttr(fullUrl)}" />`
|
|
75
|
+
);
|
|
79
76
|
|
|
80
77
|
// Replace Twitter tags
|
|
81
78
|
html = html.replace(
|
|
@@ -87,14 +84,55 @@ export function prerender(): Plugin {
|
|
|
87
84
|
`<meta name="twitter:description" content="${escapeAttr(description)}" />`
|
|
88
85
|
);
|
|
89
86
|
|
|
90
|
-
//
|
|
91
|
-
|
|
87
|
+
// Update canonical link (always replace, never skip)
|
|
88
|
+
html = html.replace(
|
|
89
|
+
/<link rel="canonical" href=".*?" \/>/,
|
|
90
|
+
`<link rel="canonical" href="${escapeAttr(fullUrl)}" />`
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Inject per-page robots directive if specified in frontmatter
|
|
94
|
+
if (page.robots) {
|
|
92
95
|
html = html.replace(
|
|
93
96
|
'</head>',
|
|
94
|
-
` <
|
|
97
|
+
` <meta name="robots" content="${escapeAttr(page.robots)}" />\n </head>`
|
|
95
98
|
);
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
// Inject JSON-LD structured data
|
|
102
|
+
const jsonLd = JSON.stringify({
|
|
103
|
+
'@context': 'https://schema.org',
|
|
104
|
+
'@graph': [
|
|
105
|
+
{
|
|
106
|
+
'@type': 'Article',
|
|
107
|
+
'headline': title,
|
|
108
|
+
'description': description,
|
|
109
|
+
...(fullUrl ? { 'url': fullUrl } : {}),
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
'@type': 'BreadcrumbList',
|
|
113
|
+
'itemListElement': [
|
|
114
|
+
{
|
|
115
|
+
'@type': 'ListItem',
|
|
116
|
+
'position': 1,
|
|
117
|
+
'name': 'Home',
|
|
118
|
+
...(baseUrl ? { 'item': baseUrl } : {}),
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
'@type': 'ListItem',
|
|
122
|
+
'position': 2,
|
|
123
|
+
'name': title,
|
|
124
|
+
...(fullUrl ? { 'item': fullUrl } : {}),
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
html = html.replace(
|
|
132
|
+
'</head>',
|
|
133
|
+
` <script type="application/ld+json">${jsonLd}</script>\n </head>`
|
|
134
|
+
);
|
|
135
|
+
|
|
98
136
|
// Write to route directory
|
|
99
137
|
const routeDir = resolve(outDir, routePath.replace(/^\//, ''));
|
|
100
138
|
mkdirSync(routeDir, { recursive: true });
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
|
-
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { readFileSync, existsSync, statSync } from 'fs';
|
|
3
3
|
import { resolve } from 'path';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Generates sitemap.xml from dory.json navigation structure at build time.
|
|
7
|
-
* Uses
|
|
7
|
+
* Uses actual file modification times for lastmod and calculates priority
|
|
8
|
+
* based on route depth.
|
|
8
9
|
*/
|
|
9
10
|
export function sitemapGenerator(): Plugin {
|
|
10
11
|
return {
|
|
11
12
|
name: 'sitemap-generator',
|
|
12
13
|
generateBundle() {
|
|
13
|
-
const
|
|
14
|
+
const docsDir = resolve(process.cwd(), 'docs');
|
|
15
|
+
const doryJsonPath = resolve(docsDir, 'dory.json');
|
|
14
16
|
if (!existsSync(doryJsonPath)) {
|
|
15
17
|
console.warn('sitemap-generator: dory.json not found, skipping');
|
|
16
18
|
return;
|
|
@@ -25,7 +27,7 @@ export function sitemapGenerator(): Plugin {
|
|
|
25
27
|
function walkPages(pages: any[]) {
|
|
26
28
|
for (const page of pages) {
|
|
27
29
|
if (typeof page === 'string') {
|
|
28
|
-
routes.push(
|
|
30
|
+
routes.push(page);
|
|
29
31
|
} else if (typeof page === 'object' && page.pages) {
|
|
30
32
|
walkPages(page.pages);
|
|
31
33
|
}
|
|
@@ -44,14 +46,26 @@ export function sitemapGenerator(): Plugin {
|
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
const
|
|
49
|
+
const fallbackDate = new Date().toISOString().split('T')[0];
|
|
48
50
|
let sitemap = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
|
49
51
|
sitemap += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';
|
|
50
52
|
|
|
51
53
|
for (const route of routes) {
|
|
54
|
+
// Get actual file modification time
|
|
55
|
+
const mdxPath = resolve(docsDir, `${route}.mdx`);
|
|
56
|
+
let lastmod = fallbackDate;
|
|
57
|
+
if (existsSync(mdxPath)) {
|
|
58
|
+
lastmod = statSync(mdxPath).mtime.toISOString().split('T')[0];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Priority based on route depth: fewer segments = higher priority
|
|
62
|
+
const depth = route.split('/').length;
|
|
63
|
+
const priority = Math.max(0.3, 1.0 - (depth - 1) * 0.2).toFixed(1);
|
|
64
|
+
|
|
52
65
|
sitemap += ' <url>\n';
|
|
53
|
-
sitemap += ` <loc>${baseUrl}
|
|
54
|
-
sitemap += ` <lastmod>${
|
|
66
|
+
sitemap += ` <loc>${baseUrl}/${route}</loc>\n`;
|
|
67
|
+
sitemap += ` <lastmod>${lastmod}</lastmod>\n`;
|
|
68
|
+
sitemap += ` <priority>${priority}</priority>\n`;
|
|
55
69
|
sitemap += ' </url>\n';
|
|
56
70
|
}
|
|
57
71
|
|
|
@@ -27,7 +27,7 @@ export function unifiedMdxPreprocessor(): Plugin {
|
|
|
27
27
|
// Return the preprocessed content
|
|
28
28
|
// The actual MDX compilation will be handled by @mdx-js/rollup plugin
|
|
29
29
|
return {
|
|
30
|
-
code: result.processedContent
|
|
30
|
+
code: result.processedContent ?? code,
|
|
31
31
|
map: null
|
|
32
32
|
};
|
|
33
33
|
}
|
package/src/types/config.ts
CHANGED
package/src/ui/header.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import classNames from 'classnames';
|
|
2
2
|
import { useEffect, useMemo, useState } from 'preact/hooks';
|
|
3
3
|
import { Link } from 'wouter-preact';
|
|
4
|
-
import { usePathname } from '
|
|
4
|
+
import { usePathname } from '../components/hooks';
|
|
5
5
|
import docsConfig from '../../docs/dory.json' with { type: 'json' };
|
|
6
6
|
import type { DoryConfig } from '../types/config';
|
|
7
7
|
import { MobileSearch, Search } from '../components/search';
|
package/src/ui/layout.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ComponentChildren } from 'preact';
|
|
2
2
|
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks';
|
|
3
|
-
import { useLocation } from 'wouter-preact';
|
|
4
3
|
import { Navigation } from '../components/navigation';
|
|
5
4
|
import { ALL_NAVIGATION, ALL_OPENAPI, completeFrontMatter, loadMDXFrontMatterForPath, loadAllMDXFrontMatter, ALL_PAGES, pathFromFilename, preloadFrontmatter, ALL_ASYNCAPI } from '../components/store';
|
|
5
|
+
import { usePathname } from '../components/hooks';
|
|
6
6
|
import { OpenAPI } from '../mdx/open-api';
|
|
7
7
|
import { AsyncAPI } from '../mdx/async-api';
|
|
8
8
|
import { Header } from './header';
|
|
@@ -26,7 +26,7 @@ const prompt = (pathname: string) => `Please read and analyze the content from t
|
|
|
26
26
|
|
|
27
27
|
export default function Layout({ children }: LayoutProps) {
|
|
28
28
|
const [loading, setLoading] = useState(true);
|
|
29
|
-
const
|
|
29
|
+
const pathname = usePathname();
|
|
30
30
|
const { showNotification } = useNotification();
|
|
31
31
|
const isEmbedded = useIsEmbedded();
|
|
32
32
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import preact from '@preact/preset-vite';
|
|
3
|
+
import mdx from '@mdx-js/rollup';
|
|
4
|
+
import { unifiedMdxPreprocessor } from './src/plugins/unified-mdx-plugin';
|
|
5
|
+
import { getMdxConfig } from './src/config/mdx';
|
|
6
|
+
|
|
7
|
+
// Minimal Vite config for the SSR build.
|
|
8
|
+
// Only includes plugins needed for MDX compilation — no sitemap, prerender,
|
|
9
|
+
// analytics, or other build-time plugins that target the client output.
|
|
10
|
+
export default defineConfig({
|
|
11
|
+
plugins: [
|
|
12
|
+
unifiedMdxPreprocessor(),
|
|
13
|
+
preact(),
|
|
14
|
+
mdx(getMdxConfig(false)),
|
|
15
|
+
],
|
|
16
|
+
resolve: {
|
|
17
|
+
alias: {
|
|
18
|
+
react: 'preact/compat',
|
|
19
|
+
'react-dom': 'preact/compat'
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
build: {
|
|
23
|
+
ssr: 'src/entry-server.tsx',
|
|
24
|
+
outDir: 'dist-ssr',
|
|
25
|
+
},
|
|
26
|
+
ssr: {
|
|
27
|
+
// Bundle all dependencies so preact/compat aliases resolve correctly.
|
|
28
|
+
// Without this, externalized imports like @headlessui/react would
|
|
29
|
+
// import 'react' directly, bypassing the preact alias.
|
|
30
|
+
noExternal: true,
|
|
31
|
+
},
|
|
32
|
+
});
|
package/vite.config.ts
CHANGED
|
@@ -11,7 +11,6 @@ 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
13
|
import { analyticsInjector } from './src/plugins/analytics-injector';
|
|
14
|
-
import { cspGenerator } from './src/plugins/csp-generator';
|
|
15
14
|
import { sitemapGenerator } from './src/plugins/sitemap-generator';
|
|
16
15
|
import { robotsGenerator } from './src/plugins/robots-generator';
|
|
17
16
|
import { prerender } from './src/plugins/prerender';
|
|
@@ -32,7 +31,6 @@ export default defineConfig(({ command }) => ({
|
|
|
32
31
|
docsAssetsPlugin(),
|
|
33
32
|
htmlMetadataInjector(),
|
|
34
33
|
analyticsInjector(),
|
|
35
|
-
cspGenerator(),
|
|
36
34
|
sitemapGenerator(),
|
|
37
35
|
robotsGenerator(),
|
|
38
36
|
prerender(),
|
|
@@ -1,68 +0,0 @@
|
|
|
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
|
-
}
|