@clidey/dory 0.47.0 → 0.49.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 +15 -19
- package/package.json +35 -29
- package/scripts/render-routes.mjs +45 -14
- package/src/components/routes.tsx +19 -22
- package/src/components/store.ts +5 -5
- package/src/config/mdx.js +7 -3
- package/src/config/mdx.ts +6 -2
- package/src/entry-server.tsx +53 -20
- package/src/main.tsx +9 -1
- package/src/mdx/code.tsx +8 -5
- package/src/mdx/fence.tsx +10 -2
- package/src/mdx/icon.tsx +21 -14
- package/src/mdx/mermaid.tsx +10 -1
- package/src/mdx/processor.test.ts +47 -0
- package/src/mdx/step.tsx +3 -3
- package/src/plugins/analytics-injector.ts +7 -5
- package/src/plugins/docs-assets.ts +5 -4
- package/src/plugins/frontmatter-dev-server.ts +8 -159
- package/src/plugins/frontmatter-generator.ts +37 -129
- 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 +113 -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 +90 -59
- 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 +66 -47
- 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.ts +11 -7
- package/scripts/generate-frontmatter-json.js +0 -154
- package/scripts/generate-llm-txt.js +0 -115
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
|
-
import {
|
|
3
|
-
import { resolve } from 'path';
|
|
2
|
+
import { readDoryConfig, escapeHtml, applyTitleTemplate } from './shared';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Vite plugin that injects site metadata from dory.json into index.html.
|
|
@@ -10,100 +9,161 @@ export function htmlMetadataInjector(): Plugin {
|
|
|
10
9
|
return {
|
|
11
10
|
name: 'html-metadata-injector',
|
|
12
11
|
transformIndexHtml(html) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
if (!existsSync(doryJsonPath)) {
|
|
12
|
+
const config = readDoryConfig();
|
|
13
|
+
if (!config) {
|
|
16
14
|
console.warn('⚠️ dory.json not found, using default metadata');
|
|
17
15
|
return html;
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
try {
|
|
21
|
-
const
|
|
22
|
-
const siteName = config.name || 'Documentation';
|
|
23
|
-
const defaultTitle = config.title || siteName;
|
|
24
|
-
const defaultDescription = config.description || `${siteName} - Technical Documentation`;
|
|
25
|
-
const siteUrl = config.url || '';
|
|
26
|
-
const twitterHandle = config.twitter || '';
|
|
19
|
+
const seo = config.seo || {};
|
|
20
|
+
const siteName: string = config.name || 'Documentation';
|
|
21
|
+
const defaultTitle = applyTitleTemplate(config.title || siteName, seo.titleTemplate, siteName);
|
|
22
|
+
const defaultDescription: string = config.description || `${siteName} - Technical Documentation`;
|
|
23
|
+
const siteUrl: string = config.url || '';
|
|
24
|
+
const twitterHandle: string = config.twitter || '';
|
|
27
25
|
|
|
28
26
|
// Resolve image to absolute URL
|
|
29
|
-
const rawImage = config.image || '';
|
|
27
|
+
const rawImage: string = config.image || '';
|
|
30
28
|
const siteImage = rawImage && siteUrl && !rawImage.startsWith('http')
|
|
31
29
|
? `${siteUrl}${rawImage.startsWith('/') ? '' : '/'}${rawImage}`
|
|
32
30
|
: rawImage;
|
|
33
31
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
);
|
|
32
|
+
// Favicon: config.favicon (copied to dist root by docs-assets), else
|
|
33
|
+
// fall back to the logo so sites without a dedicated favicon still
|
|
34
|
+
// get an icon instead of a broken link.
|
|
35
|
+
const rawFavicon: string = config.favicon || config.logo?.light || config.logo?.dark || '';
|
|
36
|
+
const faviconHref = rawFavicon ? `/${rawFavicon.replace(/^\.?\/*/, '')}` : '';
|
|
37
|
+
|
|
38
|
+
// Function replacements everywhere so `$&` etc. in config values
|
|
39
|
+
// cannot corrupt the output.
|
|
40
|
+
const setTag = (re: RegExp, replacement: string) => {
|
|
41
|
+
html = html.replace(re, () => replacement);
|
|
42
|
+
};
|
|
43
|
+
const removeTag = (re: RegExp) => {
|
|
44
|
+
html = html.replace(re, '');
|
|
45
|
+
};
|
|
46
|
+
const injectHeadTag = (tag: string) => {
|
|
47
|
+
html = html.replace('</head>', () => ` ${tag}\n </head>`);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Title
|
|
51
|
+
setTag(/<title>.*?<\/title>/, `<title>${escapeHtml(defaultTitle)}</title>`);
|
|
52
|
+
|
|
53
|
+
// Favicon (icon + apple-touch-icon); left as empty attributes in
|
|
54
|
+
// index.html and removed entirely when there's nothing to point at.
|
|
55
|
+
if (faviconHref) {
|
|
56
|
+
setTag(/<link rel="icon" href=".*?" \/>/, `<link rel="icon" href="${escapeHtml(faviconHref)}" />`);
|
|
57
|
+
setTag(
|
|
58
|
+
/<link rel="apple-touch-icon" href=".*?" \/>/,
|
|
59
|
+
`<link rel="apple-touch-icon" href="${escapeHtml(faviconHref)}" />`
|
|
60
|
+
);
|
|
61
|
+
} else {
|
|
62
|
+
removeTag(/[ \t]*<link rel="icon" href=".*?" \/>\n?/);
|
|
63
|
+
removeTag(/[ \t]*<link rel="apple-touch-icon" href=".*?" \/>\n?/);
|
|
64
|
+
}
|
|
39
65
|
|
|
40
|
-
//
|
|
41
|
-
|
|
66
|
+
// Description
|
|
67
|
+
setTag(
|
|
42
68
|
/<meta name="description" content=".*?" \/>/,
|
|
43
|
-
`<meta name="description" content="${defaultDescription}" />`
|
|
69
|
+
`<meta name="description" content="${escapeHtml(defaultDescription)}" />`
|
|
44
70
|
);
|
|
45
71
|
|
|
46
|
-
//
|
|
47
|
-
|
|
72
|
+
// Keywords / author — injected only when configured
|
|
73
|
+
if (Array.isArray(seo.keywords) && seo.keywords.length > 0) {
|
|
74
|
+
injectHeadTag(`<meta name="keywords" content="${escapeHtml(seo.keywords.join(', '))}" />`);
|
|
75
|
+
}
|
|
76
|
+
if (seo.author) {
|
|
77
|
+
injectHeadTag(`<meta name="author" content="${escapeHtml(seo.author)}" />`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Site-wide noindex (staging)
|
|
81
|
+
if (seo.noindex) {
|
|
82
|
+
injectHeadTag('<meta name="robots" content="noindex" />');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Locale — html lang + og:locale
|
|
86
|
+
if (seo.locale) {
|
|
87
|
+
setTag(/<html lang=".*?">/, `<html lang="${escapeHtml(seo.locale.replace(/_/g, '-'))}">`);
|
|
88
|
+
setTag(
|
|
89
|
+
/<meta property="og:locale" content=".*?" \/>/,
|
|
90
|
+
`<meta property="og:locale" content="${escapeHtml(seo.locale)}" />`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Open Graph tags
|
|
95
|
+
setTag(
|
|
48
96
|
/<meta property="og:title" content=".*?" \/>/,
|
|
49
|
-
`<meta property="og:title" content="${defaultTitle}" />`
|
|
97
|
+
`<meta property="og:title" content="${escapeHtml(defaultTitle)}" />`
|
|
50
98
|
);
|
|
51
|
-
|
|
99
|
+
setTag(
|
|
52
100
|
/<meta property="og:description" content=".*?" \/>/,
|
|
53
|
-
`<meta property="og:description" content="${defaultDescription}" />`
|
|
101
|
+
`<meta property="og:description" content="${escapeHtml(defaultDescription)}" />`
|
|
54
102
|
);
|
|
55
|
-
|
|
103
|
+
setTag(
|
|
56
104
|
/<meta property="og:site_name" content=".*?" \/>/,
|
|
57
|
-
`<meta property="og:site_name" content="${siteName}" />`
|
|
105
|
+
`<meta property="og:site_name" content="${escapeHtml(siteName)}" />`
|
|
58
106
|
);
|
|
59
107
|
if (siteImage) {
|
|
60
|
-
|
|
108
|
+
setTag(
|
|
61
109
|
/<meta property="og:image" content=".*?" \/>/,
|
|
62
|
-
`<meta property="og:image" content="${siteImage}" />`
|
|
110
|
+
`<meta property="og:image" content="${escapeHtml(siteImage)}" />`
|
|
63
111
|
);
|
|
64
112
|
}
|
|
113
|
+
|
|
114
|
+
// og:url and canonical — absolute when config.url is set, removed otherwise
|
|
65
115
|
if (siteUrl) {
|
|
66
|
-
|
|
116
|
+
setTag(
|
|
67
117
|
/<meta property="og:url" content=".*?" \/>/,
|
|
68
|
-
`<meta property="og:url" content="${siteUrl}" />`
|
|
118
|
+
`<meta property="og:url" content="${escapeHtml(siteUrl)}" />`
|
|
69
119
|
);
|
|
120
|
+
setTag(
|
|
121
|
+
/<link rel="canonical" href=".*?" \/>/,
|
|
122
|
+
`<link rel="canonical" href="${escapeHtml(siteUrl)}" />`
|
|
123
|
+
);
|
|
124
|
+
} else {
|
|
125
|
+
removeTag(/[ \t]*<meta property="og:url" content=".*?" \/>\n?/);
|
|
126
|
+
removeTag(/[ \t]*<link rel="canonical" href=".*?" \/>\n?/);
|
|
70
127
|
}
|
|
71
128
|
|
|
72
|
-
//
|
|
73
|
-
|
|
129
|
+
// Twitter tags
|
|
130
|
+
if (seo.twitterCard) {
|
|
131
|
+
setTag(
|
|
132
|
+
/<meta name="twitter:card" content=".*?" \/>/,
|
|
133
|
+
`<meta name="twitter:card" content="${escapeHtml(seo.twitterCard)}" />`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
setTag(
|
|
74
137
|
/<meta name="twitter:title" content=".*?" \/>/,
|
|
75
|
-
`<meta name="twitter:title" content="${defaultTitle}" />`
|
|
138
|
+
`<meta name="twitter:title" content="${escapeHtml(defaultTitle)}" />`
|
|
76
139
|
);
|
|
77
|
-
|
|
140
|
+
setTag(
|
|
78
141
|
/<meta name="twitter:description" content=".*?" \/>/,
|
|
79
|
-
`<meta name="twitter:description" content="${defaultDescription}" />`
|
|
142
|
+
`<meta name="twitter:description" content="${escapeHtml(defaultDescription)}" />`
|
|
80
143
|
);
|
|
81
144
|
if (siteImage) {
|
|
82
|
-
|
|
145
|
+
setTag(
|
|
83
146
|
/<meta name="twitter:image" content=".*?" \/>/,
|
|
84
|
-
`<meta name="twitter:image" content="${siteImage}" />`
|
|
147
|
+
`<meta name="twitter:image" content="${escapeHtml(siteImage)}" />`
|
|
85
148
|
);
|
|
86
149
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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}" />`
|
|
150
|
+
if (twitterHandle) {
|
|
151
|
+
setTag(
|
|
152
|
+
/<meta name="twitter:site" content=".*?" \/>/,
|
|
153
|
+
`<meta name="twitter:site" content="${escapeHtml(twitterHandle)}" />`
|
|
154
|
+
);
|
|
155
|
+
setTag(
|
|
156
|
+
/<meta name="twitter:creator" content=".*?" \/>/,
|
|
157
|
+
`<meta name="twitter:creator" content="${escapeHtml(twitterHandle)}" />`
|
|
101
158
|
);
|
|
159
|
+
} else {
|
|
160
|
+
removeTag(/[ \t]*<meta name="twitter:site" content=".*?" \/>\n?/);
|
|
161
|
+
removeTag(/[ \t]*<meta name="twitter:creator" content=".*?" \/>\n?/);
|
|
102
162
|
}
|
|
103
163
|
|
|
104
164
|
return html;
|
|
105
165
|
} catch (error) {
|
|
106
|
-
console.error('❌ Failed to
|
|
166
|
+
console.error('❌ Failed to inject metadata from dory.json:', error);
|
|
107
167
|
return html;
|
|
108
168
|
}
|
|
109
169
|
},
|
|
@@ -1,190 +1,37 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
|
-
import fs from 'fs';
|
|
3
2
|
import path from 'path';
|
|
3
|
+
import { generateLlmsIndex, generateLlmContent } from './llm-txt-generator';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
console.error(`Failed to parse ${doryJsonPath}: ${detail}`);
|
|
15
|
-
return [];
|
|
16
|
-
}
|
|
17
|
-
const order: string[] = [];
|
|
18
|
-
|
|
19
|
-
function walkPages(pages: any[]) {
|
|
20
|
-
for (const page of pages) {
|
|
21
|
-
if (typeof page === 'string') {
|
|
22
|
-
order.push(page);
|
|
23
|
-
} else if (typeof page === 'object' && page.pages) {
|
|
24
|
-
walkPages(page.pages);
|
|
5
|
+
/**
|
|
6
|
+
* Dev server plugin that serves llms.txt and llms-full.txt on demand,
|
|
7
|
+
* generated fresh per request from the docs directory.
|
|
8
|
+
*/
|
|
9
|
+
export function llmTxtDevServer(): Plugin {
|
|
10
|
+
const serveText = (generate: (docsDir: string) => string) =>
|
|
11
|
+
(req: any, res: any, next: () => void) => {
|
|
12
|
+
if (req.method !== 'GET') {
|
|
13
|
+
return next();
|
|
25
14
|
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
15
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
16
|
+
try {
|
|
17
|
+
const docsDir = path.join(process.cwd(), 'docs');
|
|
18
|
+
const content = generate(docsDir);
|
|
19
|
+
|
|
20
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
21
|
+
res.setHeader('Content-Length', Buffer.byteLength(content));
|
|
22
|
+
res.end(content);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('Error generating LLM text:', error);
|
|
25
|
+
res.statusCode = 500;
|
|
26
|
+
res.end('Internal Server Error');
|
|
37
27
|
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return order;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Function to parse frontmatter from MDX content
|
|
44
|
-
function parseFrontmatter(content: string) {
|
|
45
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
|
|
46
|
-
if (!frontmatterMatch) return { data: {}, content };
|
|
47
|
-
|
|
48
|
-
const frontmatter = frontmatterMatch[1];
|
|
49
|
-
const data: Record<string, string> = {};
|
|
50
|
-
|
|
51
|
-
frontmatter.split('\n').forEach(line => {
|
|
52
|
-
const [key, ...valueParts] = line.split(':');
|
|
53
|
-
if (key && valueParts.length) {
|
|
54
|
-
const value = valueParts.join(':').trim();
|
|
55
|
-
data[key.trim()] = value.replace(/^["']|["']$/g, '');
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
return { data, content: content.slice(frontmatterMatch[0].length) };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Function to recursively find all MDX files and map by relative path (without .mdx)
|
|
63
|
-
function findMdxFilesMap(dir: string, baseDir: string): Record<string, string> {
|
|
64
|
-
const files: Record<string, string> = {};
|
|
65
|
-
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
66
|
-
|
|
67
|
-
for (const item of items) {
|
|
68
|
-
const fullPath = path.join(dir, item.name);
|
|
69
|
-
if (item.isDirectory()) {
|
|
70
|
-
Object.assign(files, findMdxFilesMap(fullPath, baseDir));
|
|
71
|
-
} else if (item.isFile() && item.name.endsWith('.mdx')) {
|
|
72
|
-
// Key: relative path without .mdx, with forward slashes
|
|
73
|
-
let rel = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
|
74
|
-
if (rel.endsWith('.mdx')) rel = rel.slice(0, -4);
|
|
75
|
-
files[rel] = fullPath;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return files;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Function to clean MDX content for LLM consumption
|
|
83
|
-
function cleanMdxContent(content: string): string {
|
|
84
|
-
// Remove JSX components but keep their content
|
|
85
|
-
let cleaned = content
|
|
86
|
-
// Remove JSX opening/closing tags but keep content
|
|
87
|
-
.replace(/<(\w+)([^>]*?)>/g, '')
|
|
88
|
-
.replace(/<\/\w+>/g, '')
|
|
89
|
-
// Remove code block language specifiers
|
|
90
|
-
.replace(/```(\w+)/g, '```')
|
|
91
|
-
// Remove HTML comments
|
|
92
|
-
.replace(/<!--[\s\S]*?-->/g, '')
|
|
93
|
-
// Remove excessive whitespace
|
|
94
|
-
.replace(/\n\s*\n\s*\n/g, '\n\n')
|
|
95
|
-
// Remove leading/trailing whitespace
|
|
96
|
-
.trim();
|
|
97
|
-
|
|
98
|
-
return cleaned;
|
|
99
|
-
}
|
|
28
|
+
};
|
|
100
29
|
|
|
101
|
-
// Function to generate LLM text content in dory.json order, then append any extra files
|
|
102
|
-
function generateLlmContent(docsDir: string): string {
|
|
103
|
-
const navOrder = getNavigationOrder(docsDir);
|
|
104
|
-
const mdxFilesMap = findMdxFilesMap(docsDir, docsDir);
|
|
105
|
-
const usedKeys = new Set<string>();
|
|
106
|
-
let aggregatedContent = '';
|
|
107
|
-
|
|
108
|
-
// Output files in navigation order, in order as in dory.json
|
|
109
|
-
for (const navKey of navOrder) {
|
|
110
|
-
// Try both with and without "index"
|
|
111
|
-
let fileKey = navKey;
|
|
112
|
-
if (mdxFilesMap[fileKey]) {
|
|
113
|
-
usedKeys.add(fileKey);
|
|
114
|
-
} else if (mdxFilesMap[`${fileKey}/index`]) {
|
|
115
|
-
fileKey = `${fileKey}/index`;
|
|
116
|
-
usedKeys.add(fileKey);
|
|
117
|
-
} else {
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
const filePath = mdxFilesMap[fileKey];
|
|
121
|
-
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
122
|
-
const { data, content } = parseFrontmatter(rawContent);
|
|
123
|
-
const cleanedContent = cleanMdxContent(content);
|
|
124
|
-
|
|
125
|
-
// Add file header
|
|
126
|
-
aggregatedContent += `\n# ${fileKey}.mdx\n`;
|
|
127
|
-
|
|
128
|
-
// Add title if available
|
|
129
|
-
if (data.title) {
|
|
130
|
-
aggregatedContent += `## ${data.title}\n\n`;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Add description if available
|
|
134
|
-
if (data.description) {
|
|
135
|
-
aggregatedContent += `${data.description}\n\n`;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Add cleaned content
|
|
139
|
-
aggregatedContent += cleanedContent + '\n\n';
|
|
140
|
-
aggregatedContent += '---\n\n';
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Output any extra files not in navigation order, sorted
|
|
144
|
-
const extraKeys = Object.keys(mdxFilesMap).filter(k => !usedKeys.has(k)).sort();
|
|
145
|
-
for (const fileKey of extraKeys) {
|
|
146
|
-
const filePath = mdxFilesMap[fileKey];
|
|
147
|
-
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
148
|
-
const { data, content } = parseFrontmatter(rawContent);
|
|
149
|
-
const cleanedContent = cleanMdxContent(content);
|
|
150
|
-
|
|
151
|
-
aggregatedContent += `\n# ${fileKey}.mdx\n`;
|
|
152
|
-
if (data.title) {
|
|
153
|
-
aggregatedContent += `## ${data.title}\n\n`;
|
|
154
|
-
}
|
|
155
|
-
if (data.description) {
|
|
156
|
-
aggregatedContent += `${data.description}\n\n`;
|
|
157
|
-
}
|
|
158
|
-
aggregatedContent += cleanedContent + '\n\n';
|
|
159
|
-
aggregatedContent += '---\n\n';
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Remove leading newlines for the very first file, so the first file is at the very top
|
|
163
|
-
return aggregatedContent.replace(/^\n+/, '').trim();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export function llmTxtDevServer(): Plugin {
|
|
167
30
|
return {
|
|
168
31
|
name: 'llm-txt-dev-server',
|
|
169
32
|
configureServer(server) {
|
|
170
|
-
server.middlewares.use('/llms.txt', (
|
|
171
|
-
|
|
172
|
-
return next();
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
try {
|
|
176
|
-
const docsDir = path.join(process.cwd(), 'docs');
|
|
177
|
-
const llmContent = generateLlmContent(docsDir);
|
|
178
|
-
|
|
179
|
-
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
180
|
-
res.setHeader('Content-Length', Buffer.byteLength(llmContent));
|
|
181
|
-
res.end(llmContent);
|
|
182
|
-
} catch (error) {
|
|
183
|
-
console.error('Error generating LLM text:', error);
|
|
184
|
-
res.statusCode = 500;
|
|
185
|
-
res.end('Internal Server Error');
|
|
186
|
-
}
|
|
187
|
-
});
|
|
33
|
+
server.middlewares.use('/llms.txt', serveText(generateLlmsIndex));
|
|
34
|
+
server.middlewares.use('/llms-full.txt', serveText(generateLlmContent));
|
|
188
35
|
}
|
|
189
36
|
};
|
|
190
|
-
}
|
|
37
|
+
}
|