boltdocs 1.11.0 → 2.1.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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/bin/boltdocs.js +12 -0
- package/dist/cache-Q4T6VAUL.mjs +1 -0
- package/dist/chunk-52MVMZWS.mjs +1 -0
- package/dist/chunk-BVWWKXJH.mjs +1 -0
- package/dist/chunk-DVY3RDXD.mjs +1 -0
- package/dist/chunk-FUVYCYWC.mjs +1 -0
- package/dist/chunk-GBLMDJ2B.mjs +1 -0
- package/dist/chunk-ISPX45DF.mjs +1 -0
- package/dist/chunk-PNXZMUCO.mjs +1 -0
- package/dist/chunk-V2ZHKQSP.mjs +74 -0
- package/dist/client/components/mdx/index.d.mts +208 -0
- package/dist/client/components/mdx/index.d.ts +208 -0
- package/dist/client/components/mdx/index.js +1 -0
- package/dist/client/components/mdx/index.mjs +1 -0
- package/dist/client/hooks/index.d.mts +132 -0
- package/dist/client/hooks/index.d.ts +132 -0
- package/dist/client/hooks/index.js +1 -0
- package/dist/client/hooks/index.mjs +1 -0
- package/dist/client/index.d.mts +210 -0
- package/dist/client/index.d.ts +210 -0
- package/dist/client/index.js +1 -0
- package/dist/client/index.mjs +1 -0
- package/dist/client/ssr.d.mts +78 -0
- package/dist/client/ssr.d.ts +78 -0
- package/dist/client/ssr.js +1 -0
- package/dist/client/ssr.mjs +1 -0
- package/dist/node/cli-entry.d.mts +1 -0
- package/dist/node/cli-entry.d.ts +1 -0
- package/dist/node/cli-entry.js +75 -0
- package/dist/node/cli-entry.mjs +2 -0
- package/dist/node/index.d.mts +298 -0
- package/dist/node/index.d.ts +298 -0
- package/dist/node/index.js +74 -0
- package/dist/node/index.mjs +1 -0
- package/dist/search-dialog-TWGYKF2D.mjs +1 -0
- package/dist/types-Cp21DHI6.d.mts +355 -0
- package/dist/types-Cp21DHI6.d.ts +355 -0
- package/dist/use-routes-8Iei6jTp.d.mts +29 -0
- package/dist/use-routes-xLhumjbV.d.ts +29 -0
- package/package.json +16 -10
- package/src/client/app/index.tsx +9 -6
- package/src/client/components/ui-base/breadcrumbs.tsx +2 -1
- package/src/client/components/ui-base/navbar.tsx +3 -3
- package/src/client/components/ui-base/sidebar.tsx +2 -1
- package/src/client/hooks/use-navbar.ts +1 -1
- package/src/client/types.ts +1 -1
- package/src/node/cli-entry.ts +24 -0
- package/src/node/cli.ts +59 -0
- package/src/node/config.ts +63 -11
- package/src/node/index.ts +39 -3
- package/src/node/plugin/entry.ts +7 -0
- package/src/node/plugin/html.ts +49 -9
- package/src/node/plugin/index.ts +42 -4
- package/src/node/routes/parser.ts +27 -32
- package/src/node/ssg/index.ts +35 -4
- package/src/node/ssg/robots.ts +50 -0
- package/src/node/utils.ts +23 -0
- package/tsup.config.ts +36 -10
package/src/node/config.ts
CHANGED
|
@@ -63,6 +63,15 @@ export interface BoltdocsThemeConfig {
|
|
|
63
63
|
version?: string
|
|
64
64
|
/** The GitHub repository in the format 'owner/repo' to fetch and display star count. */
|
|
65
65
|
githubRepo?: string
|
|
66
|
+
/**
|
|
67
|
+
* URL path to the site favicon.
|
|
68
|
+
* If not specified, the logo will be used if available.
|
|
69
|
+
*/
|
|
70
|
+
favicon?: string
|
|
71
|
+
/**
|
|
72
|
+
* The Open Graph image URL to display when the site is shared on social media.
|
|
73
|
+
*/
|
|
74
|
+
ogImage?: string
|
|
66
75
|
/** Whether to show the 'Powered by LiteDocs' badge in the sidebar (default: true) */
|
|
67
76
|
poweredBy?: boolean
|
|
68
77
|
/**
|
|
@@ -85,6 +94,24 @@ export interface BoltdocsThemeConfig {
|
|
|
85
94
|
copyMarkdown?: boolean | { text?: string; icon?: string }
|
|
86
95
|
}
|
|
87
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Configuration for the robots.txt file.
|
|
99
|
+
*/
|
|
100
|
+
export type BoltdocsRobotsConfig =
|
|
101
|
+
| string
|
|
102
|
+
| {
|
|
103
|
+
/** User-agent rules */
|
|
104
|
+
rules?: Array<{
|
|
105
|
+
userAgent: string
|
|
106
|
+
/** Paths allowed to be crawled */
|
|
107
|
+
allow?: string | string[]
|
|
108
|
+
/** Paths disallowed to be crawled */
|
|
109
|
+
disallow?: string | string[]
|
|
110
|
+
}>
|
|
111
|
+
/** Sitemaps to include in the robots.txt */
|
|
112
|
+
sitemaps?: string[]
|
|
113
|
+
}
|
|
114
|
+
|
|
88
115
|
/**
|
|
89
116
|
* Configuration for internationalization (i18n).
|
|
90
117
|
*/
|
|
@@ -142,10 +169,12 @@ export interface BoltdocsIntegrationsConfig {
|
|
|
142
169
|
export interface BoltdocsConfig {
|
|
143
170
|
/** The base URL of the site, used for generating the sitemap */
|
|
144
171
|
siteUrl?: string
|
|
145
|
-
/** Configuration pertaining to the UI and appearance */
|
|
146
|
-
themeConfig?: BoltdocsThemeConfig
|
|
147
172
|
/** The root directory containing markdown documentation files (default: 'docs') */
|
|
148
173
|
docsDir?: string
|
|
174
|
+
/** Path to a custom HomePage component */
|
|
175
|
+
homePage?: string
|
|
176
|
+
/** Configuration pertaining to the UI and appearance */
|
|
177
|
+
theme?: BoltdocsThemeConfig
|
|
149
178
|
/** Configuration for internationalization */
|
|
150
179
|
i18n?: BoltdocsI18nConfig
|
|
151
180
|
/** Configuration for documentation versioning */
|
|
@@ -156,6 +185,16 @@ export interface BoltdocsConfig {
|
|
|
156
185
|
external?: Record<string, string>
|
|
157
186
|
/** External integrations configuration */
|
|
158
187
|
integrations?: BoltdocsIntegrationsConfig
|
|
188
|
+
/** Configuration for the robots.txt file */
|
|
189
|
+
robots?: BoltdocsRobotsConfig
|
|
190
|
+
/** Low-level Vite configuration overrides */
|
|
191
|
+
vite?: import('vite').InlineConfig
|
|
192
|
+
/** @deprecated Use theme instead */
|
|
193
|
+
themeConfig?: BoltdocsThemeConfig
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function defineConfig(config: BoltdocsConfig): BoltdocsConfig {
|
|
197
|
+
return config
|
|
159
198
|
}
|
|
160
199
|
|
|
161
200
|
export const CONFIG_FILES = [
|
|
@@ -169,7 +208,10 @@ export const CONFIG_FILES = [
|
|
|
169
208
|
*/
|
|
170
209
|
interface RawUserConfig
|
|
171
210
|
extends Partial<BoltdocsConfig>,
|
|
172
|
-
Partial<BoltdocsThemeConfig> {
|
|
211
|
+
Partial<BoltdocsThemeConfig> {
|
|
212
|
+
favicon?: string
|
|
213
|
+
ogImage?: string
|
|
214
|
+
}
|
|
173
215
|
|
|
174
216
|
/**
|
|
175
217
|
* Loads user's configuration file (e.g., `boltdocs.config.js` or `boltdocs.config.ts`) if it exists,
|
|
@@ -187,7 +229,7 @@ export async function resolveConfig(
|
|
|
187
229
|
|
|
188
230
|
const defaults: BoltdocsConfig = {
|
|
189
231
|
docsDir: path.resolve(docsDir),
|
|
190
|
-
|
|
232
|
+
theme: {
|
|
191
233
|
title: 'Boltdocs',
|
|
192
234
|
description: 'A Vite documentation framework',
|
|
193
235
|
navbar: [
|
|
@@ -230,18 +272,28 @@ export async function resolveConfig(
|
|
|
230
272
|
title: userConfig.title,
|
|
231
273
|
description: userConfig.description,
|
|
232
274
|
logo: userConfig.logo,
|
|
275
|
+
favicon: userConfig.favicon,
|
|
276
|
+
ogImage: userConfig.ogImage,
|
|
233
277
|
navbar: userConfig.navbar,
|
|
234
278
|
sidebar: userConfig.sidebar,
|
|
235
279
|
socialLinks: userConfig.socialLinks,
|
|
236
280
|
footer: userConfig.footer,
|
|
237
281
|
githubRepo: userConfig.githubRepo,
|
|
238
282
|
tabs: userConfig.tabs,
|
|
283
|
+
codeTheme: userConfig.codeTheme,
|
|
284
|
+
copyMarkdown: userConfig.copyMarkdown,
|
|
285
|
+
breadcrumbs: userConfig.breadcrumbs,
|
|
286
|
+
poweredBy: userConfig.poweredBy,
|
|
287
|
+
communityHelp: userConfig.communityHelp,
|
|
288
|
+
version: userConfig.version,
|
|
289
|
+
editLink: userConfig.editLink
|
|
239
290
|
}
|
|
240
291
|
|
|
241
|
-
// User can define properties at top level or inside themeConfig
|
|
292
|
+
// User can define properties at top level or inside themeConfig/theme
|
|
242
293
|
const userThemeConfig: BoltdocsThemeConfig = {
|
|
243
294
|
...themeConfigFromTop,
|
|
244
295
|
...(userConfig.themeConfig || {}),
|
|
296
|
+
...(userConfig.theme || {}),
|
|
245
297
|
}
|
|
246
298
|
|
|
247
299
|
// Clean undefined properties
|
|
@@ -263,13 +315,10 @@ export async function resolveConfig(
|
|
|
263
315
|
|
|
264
316
|
return {
|
|
265
317
|
docsDir: path.resolve(docsDir),
|
|
266
|
-
|
|
267
|
-
|
|
318
|
+
homePage: userConfig.homePage,
|
|
319
|
+
theme: {
|
|
320
|
+
...defaults.theme,
|
|
268
321
|
...cleanThemeConfig,
|
|
269
|
-
codeTheme:
|
|
270
|
-
cleanThemeConfig.codeTheme ||
|
|
271
|
-
(userConfig.themeConfig || userConfig).codeTheme ||
|
|
272
|
-
defaults.themeConfig?.codeTheme,
|
|
273
322
|
},
|
|
274
323
|
i18n: userConfig.i18n,
|
|
275
324
|
versions: userConfig.versions,
|
|
@@ -277,5 +326,8 @@ export async function resolveConfig(
|
|
|
277
326
|
plugins: userConfig.plugins || [],
|
|
278
327
|
external: userConfig.external,
|
|
279
328
|
integrations: userConfig.integrations,
|
|
329
|
+
robots: userConfig.robots,
|
|
330
|
+
vite: userConfig.vite,
|
|
280
331
|
}
|
|
281
332
|
}
|
|
333
|
+
|
package/src/node/index.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type { Plugin } from 'vite'
|
|
1
|
+
import type { Plugin, InlineConfig } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
2
4
|
import { boltdocsPlugin } from './plugin/index'
|
|
3
5
|
import { boltdocsMdxPlugin } from './mdx'
|
|
4
6
|
import type { BoltdocsPluginOptions } from './plugin/index'
|
|
5
7
|
|
|
6
|
-
import { resolveConfig } from './config'
|
|
8
|
+
import { resolveConfig, type BoltdocsConfig } from './config'
|
|
7
9
|
|
|
8
10
|
export default async function boltdocs(
|
|
9
11
|
options?: BoltdocsPluginOptions,
|
|
@@ -11,7 +13,40 @@ export default async function boltdocs(
|
|
|
11
13
|
const docsDir = options?.docsDir || 'docs'
|
|
12
14
|
const config = await resolveConfig(docsDir)
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
// Merge options with config
|
|
17
|
+
const mergedOptions: BoltdocsPluginOptions = {
|
|
18
|
+
...options,
|
|
19
|
+
homePage: options?.homePage || config.homePage,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return [...boltdocsPlugin(mergedOptions, config), boltdocsMdxPlugin(config)]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generates the complete Vite configuration for a Boltdocs project.
|
|
27
|
+
* This is used by the Boltdocs CLI to run Vite without a user-defined vite.config.ts.
|
|
28
|
+
*/
|
|
29
|
+
export async function createViteConfig(
|
|
30
|
+
root: string,
|
|
31
|
+
mode: 'development' | 'production' = 'development',
|
|
32
|
+
): Promise<InlineConfig> {
|
|
33
|
+
const config = await resolveConfig('docs', root)
|
|
34
|
+
|
|
35
|
+
const viteConfig: InlineConfig = {
|
|
36
|
+
root,
|
|
37
|
+
mode,
|
|
38
|
+
plugins: [
|
|
39
|
+
react(),
|
|
40
|
+
tailwindcss(),
|
|
41
|
+
await boltdocs({
|
|
42
|
+
docsDir: config.docsDir,
|
|
43
|
+
homePage: config.homePage,
|
|
44
|
+
}),
|
|
45
|
+
],
|
|
46
|
+
...config.vite,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return viteConfig
|
|
15
50
|
}
|
|
16
51
|
|
|
17
52
|
export type { BoltdocsPluginOptions }
|
|
@@ -19,3 +54,4 @@ export { generateStaticPages } from './ssg'
|
|
|
19
54
|
export type { SSGOptions } from './ssg'
|
|
20
55
|
export type { RouteMeta } from './routes'
|
|
21
56
|
export type { BoltdocsConfig, BoltdocsThemeConfig } from './config'
|
|
57
|
+
export { resolveConfig, defineConfig } from './config'
|
package/src/node/plugin/entry.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { normalizePath } from '../utils'
|
|
|
2
2
|
import type { BoltdocsConfig } from '../config'
|
|
3
3
|
import type { BoltdocsPluginOptions } from './types'
|
|
4
4
|
import path from 'path'
|
|
5
|
+
import fs from 'fs'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Generates the raw source code for the virtual entry file (`\0virtual:boltdocs-entry`).
|
|
@@ -18,6 +19,11 @@ export function generateEntryCode(
|
|
|
18
19
|
const homeImport = options.homePage
|
|
19
20
|
? `import HomePage from '${normalizePath(options.homePage)}';`
|
|
20
21
|
: ''
|
|
22
|
+
|
|
23
|
+
// Auto-import index.css if it exists
|
|
24
|
+
const cssPath = path.resolve(process.cwd(), 'index.css')
|
|
25
|
+
const cssImport = fs.existsSync(cssPath) ? "import './index.css';" : ''
|
|
26
|
+
|
|
21
27
|
const homeOption = options.homePage ? 'homePage: HomePage,' : ''
|
|
22
28
|
const pluginComponents =
|
|
23
29
|
config?.plugins?.flatMap((p) => Object.entries(p.components || {})) || []
|
|
@@ -54,6 +60,7 @@ import { createBoltdocsApp as _createApp } from 'boltdocs/client';
|
|
|
54
60
|
import _routes from 'virtual:boltdocs-routes';
|
|
55
61
|
import _config from 'virtual:boltdocs-config';
|
|
56
62
|
import _user_mdx_components from 'virtual:boltdocs-mdx-components';
|
|
63
|
+
${cssImport}
|
|
57
64
|
${homeImport}
|
|
58
65
|
${componentImports}
|
|
59
66
|
${externalImports}
|
package/src/node/plugin/html.ts
CHANGED
|
@@ -1,27 +1,61 @@
|
|
|
1
1
|
import type { BoltdocsConfig } from '../config'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Provides a default HTML template if none is found in the project root.
|
|
5
|
+
*/
|
|
6
|
+
export function getHtmlTemplate(config: BoltdocsConfig): string {
|
|
7
|
+
const title = config.theme?.title || config.themeConfig?.title || 'Boltdocs'
|
|
8
|
+
return `<!doctype html>
|
|
9
|
+
<html lang="en">
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="UTF-8" />
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
13
|
+
<title>${title}</title>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<div id="root"></div>
|
|
17
|
+
</body>
|
|
18
|
+
</html>`
|
|
19
|
+
}
|
|
20
|
+
|
|
3
21
|
/**
|
|
4
22
|
* Injects OpenGraph, Twitter, and generic SEO meta tags into the final HTML output.
|
|
5
23
|
* Also ensures the virtual entry file is injected if it's missing (e.g., standard Vite index.html).
|
|
6
24
|
*
|
|
7
|
-
* @param html - The original HTML string
|
|
8
|
-
* @param config - The resolved Boltdocs configuration containing site metadata
|
|
9
|
-
* @returns The modified HTML string with injected tags
|
|
25
|
+
* @param html - {string} The original HTML string
|
|
26
|
+
* @param config - {BoltdocsConfig} The resolved Boltdocs configuration containing site metadata
|
|
27
|
+
* @returns {string} The modified HTML string with injected tags
|
|
10
28
|
*/
|
|
11
29
|
export function injectHtmlMeta(html: string, config: BoltdocsConfig): string {
|
|
12
|
-
const
|
|
13
|
-
const
|
|
30
|
+
const theme = config.theme || config.themeConfig
|
|
31
|
+
const title = theme?.title || 'Boltdocs'
|
|
32
|
+
const description = theme?.description || ''
|
|
33
|
+
|
|
34
|
+
// Determine favicon
|
|
35
|
+
let favicon = theme?.favicon
|
|
36
|
+
if (!favicon && theme?.logo) {
|
|
37
|
+
if (typeof theme.logo === 'string') {
|
|
38
|
+
favicon = theme.logo
|
|
39
|
+
} else {
|
|
40
|
+
favicon = theme.logo.light || theme.logo.dark
|
|
41
|
+
}
|
|
42
|
+
}
|
|
14
43
|
|
|
15
44
|
const seoTags = [
|
|
45
|
+
favicon ? `<link rel="icon" href="${favicon}">` : '',
|
|
16
46
|
`<meta name="description" content="${description}">`,
|
|
17
47
|
`<meta property="og:title" content="${title}">`,
|
|
18
48
|
`<meta property="og:description" content="${description}">`,
|
|
49
|
+
theme?.ogImage ? `<meta property="og:image" content="${theme.ogImage}">` : '',
|
|
19
50
|
`<meta property="og:type" content="website">`,
|
|
20
|
-
`<meta name="twitter:card" content="
|
|
51
|
+
`<meta name="twitter:card" content="summary_large_image">`,
|
|
21
52
|
`<meta name="twitter:title" content="${title}">`,
|
|
22
53
|
`<meta name="twitter:description" content="${description}">`,
|
|
54
|
+
theme?.ogImage ? `<meta name="twitter:image" content="${theme.ogImage}">` : '',
|
|
23
55
|
`<meta name="generator" content="Boltdocs">`,
|
|
24
|
-
]
|
|
56
|
+
]
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.join('\n ')
|
|
25
59
|
|
|
26
60
|
const themeScript = `
|
|
27
61
|
<script>
|
|
@@ -41,10 +75,16 @@ export function injectHtmlMeta(html: string, config: BoltdocsConfig): string {
|
|
|
41
75
|
</script>
|
|
42
76
|
`
|
|
43
77
|
|
|
44
|
-
|
|
78
|
+
// Use regex to replace title or inject it if missing
|
|
79
|
+
if (html.includes('<title>')) {
|
|
80
|
+
html = html.replace(/<title>.*?<\/title>/, `<title>${title}</title>`)
|
|
81
|
+
} else {
|
|
82
|
+
html = html.replace('</head>', ` <title>${title}</title>\n </head>`)
|
|
83
|
+
}
|
|
84
|
+
|
|
45
85
|
html = html.replace('</head>', ` ${seoTags}\n${themeScript} </head>`)
|
|
46
86
|
|
|
47
|
-
if (!html.includes('src/main')) {
|
|
87
|
+
if (!html.includes('src/main') && !html.includes('virtual:boltdocs-entry')) {
|
|
48
88
|
html = html.replace(
|
|
49
89
|
'</body>',
|
|
50
90
|
' <script type="module">import "virtual:boltdocs-entry";</script>\n </body>',
|
package/src/node/plugin/index.ts
CHANGED
|
@@ -5,12 +5,13 @@ import { resolveConfig, type BoltdocsConfig, CONFIG_FILES } from '../config'
|
|
|
5
5
|
import { generateStaticPages } from '../ssg'
|
|
6
6
|
import { normalizePath, isDocFile } from '../utils'
|
|
7
7
|
import path from 'path'
|
|
8
|
-
|
|
9
8
|
import type { BoltdocsPluginOptions } from './types'
|
|
10
9
|
import { generateEntryCode } from './entry'
|
|
11
|
-
import { injectHtmlMeta } from './html'
|
|
10
|
+
import { injectHtmlMeta, getHtmlTemplate } from './html'
|
|
11
|
+
import { generateRobotsTxt } from '../ssg/robots'
|
|
12
12
|
import fs from 'fs'
|
|
13
13
|
|
|
14
|
+
|
|
14
15
|
export * from './types'
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -63,7 +64,44 @@ export function boltdocsPlugin(
|
|
|
63
64
|
},
|
|
64
65
|
|
|
65
66
|
configureServer(server) {
|
|
66
|
-
//
|
|
67
|
+
// Serve robots.txt from config
|
|
68
|
+
server.middlewares.use((req, res, next) => {
|
|
69
|
+
if (req.url === '/robots.txt') {
|
|
70
|
+
const robots = generateRobotsTxt(config)
|
|
71
|
+
res.statusCode = 200
|
|
72
|
+
res.setHeader('Content-Type', 'text/plain')
|
|
73
|
+
res.end(robots)
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
next()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Serve default HTML if index.html is missing
|
|
80
|
+
server.middlewares.use(async (req, res, next) => {
|
|
81
|
+
const url = req.url?.split('?')[0] || '/'
|
|
82
|
+
const accept = req.headers.accept || ''
|
|
83
|
+
|
|
84
|
+
if (
|
|
85
|
+
accept.includes('text/html') &&
|
|
86
|
+
!url.includes('.') // Simple check for assets
|
|
87
|
+
) {
|
|
88
|
+
const indexPath = path.resolve(process.cwd(), 'index.html')
|
|
89
|
+
if (!fs.existsSync(indexPath)) {
|
|
90
|
+
let html = getHtmlTemplate(config)
|
|
91
|
+
html = injectHtmlMeta(html, config)
|
|
92
|
+
html = await server.transformIndexHtml(req.url || '/', html)
|
|
93
|
+
res.statusCode = 200
|
|
94
|
+
res.setHeader('Content-Type', 'text/html')
|
|
95
|
+
res.end(html)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
next()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// Explicitly watch config files...
|
|
104
|
+
|
|
67
105
|
const configPaths = CONFIG_FILES.map((c) =>
|
|
68
106
|
path.resolve(process.cwd(), c),
|
|
69
107
|
)
|
|
@@ -172,6 +210,7 @@ export function boltdocsPlugin(
|
|
|
172
210
|
}
|
|
173
211
|
if (id === '\0virtual:boltdocs-config') {
|
|
174
212
|
const clientConfig = {
|
|
213
|
+
theme: config?.theme,
|
|
175
214
|
themeConfig: config?.themeConfig,
|
|
176
215
|
integrations: config?.integrations,
|
|
177
216
|
i18n: config?.i18n,
|
|
@@ -262,7 +301,6 @@ export default DefaultLayout;`
|
|
|
262
301
|
plugins: [
|
|
263
302
|
{
|
|
264
303
|
name: 'preset-default',
|
|
265
|
-
params: { overrides: { removeViewBox: false } },
|
|
266
304
|
},
|
|
267
305
|
] as any,
|
|
268
306
|
},
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
capitalize,
|
|
10
10
|
stripNumberPrefix,
|
|
11
11
|
extractNumberPrefix,
|
|
12
|
+
sanitizeHtml,
|
|
13
|
+
stripHtmlTags,
|
|
12
14
|
} from '../utils'
|
|
13
15
|
|
|
14
16
|
/**
|
|
@@ -118,47 +120,47 @@ export function parseDocFile(
|
|
|
118
120
|
|
|
119
121
|
const isGroupIndex = parts.length >= 2 && /^index\.mdx?$/.test(cleanFileName)
|
|
120
122
|
|
|
121
|
-
const headings: { level: number; text: string; id: string }[] = []
|
|
122
123
|
const slugger = new GithubSlugger()
|
|
124
|
+
const headings: { level: number; text: string; id: string }[] = []
|
|
123
125
|
const headingsRegex = /^(#{2,4})\s+(.+)$/gm
|
|
126
|
+
|
|
124
127
|
let match
|
|
125
128
|
while ((match = headingsRegex.exec(content)) !== null) {
|
|
126
129
|
const level = match[1].length
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
.replace(
|
|
130
|
-
.replace(/[_*`]/g, '')
|
|
131
|
-
.trim()
|
|
132
|
-
const id = slugger.slug(text)
|
|
133
|
-
// Security: Sanitize heading text for XSS
|
|
134
|
-
const sanitizedText = text
|
|
135
|
-
.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gim, '')
|
|
136
|
-
.replace(/<[^>]+on\w+="[^"]*"/gim, '')
|
|
137
|
-
.replace(/<img[^>]+>/gim, '')
|
|
130
|
+
const rawText = match[2]
|
|
131
|
+
.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') // Strip markdown links
|
|
132
|
+
.replace(/[_*`]/g, '') // Strip markdown formatting
|
|
138
133
|
.trim()
|
|
134
|
+
|
|
135
|
+
const sanitizedText = sanitizeHtml(rawText).trim()
|
|
136
|
+
const id = slugger.slug(sanitizedText)
|
|
137
|
+
|
|
139
138
|
headings.push({ level, text: sanitizedText, id })
|
|
140
139
|
}
|
|
141
140
|
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
141
|
+
const sanitizedTitle = data.title
|
|
142
|
+
? sanitizeHtml(String(data.title))
|
|
143
|
+
: inferredTitle
|
|
144
|
+
let sanitizedDescription = data.description
|
|
145
|
+
? sanitizeHtml(String(data.description))
|
|
146
|
+
: ''
|
|
147
147
|
|
|
148
148
|
// If no description is provided, extract a summary from the content
|
|
149
149
|
if (!sanitizedDescription && content) {
|
|
150
|
-
|
|
150
|
+
const plainExcerpt = stripHtmlTags(
|
|
151
151
|
content
|
|
152
152
|
.replace(/^#+.*$/gm, '') // Remove headers
|
|
153
153
|
.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') // Simplify links
|
|
154
154
|
.replace(/[_*`]/g, '') // Remove formatting
|
|
155
|
-
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
156
|
-
.trim()
|
|
157
|
-
.slice(0, 160),
|
|
155
|
+
.replace(/\s+/g, ' '), // Normalize whitespace
|
|
158
156
|
)
|
|
157
|
+
.trim()
|
|
158
|
+
.slice(0, 160)
|
|
159
|
+
|
|
160
|
+
sanitizedDescription = plainExcerpt
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
const sanitizedBadge = data.badge ?
|
|
163
|
+
const sanitizedBadge = data.badge ? sanitizeHtml(String(data.badge)) : undefined
|
|
162
164
|
const icon = data.icon ? String(data.icon) : undefined
|
|
163
165
|
|
|
164
166
|
// Extract full content as plain text for search indexing
|
|
@@ -203,24 +205,17 @@ export function parseDocFile(
|
|
|
203
205
|
}
|
|
204
206
|
}
|
|
205
207
|
|
|
206
|
-
/**
|
|
207
|
-
* Sanitizes a string by removing script tags for basic XSS protection.
|
|
208
|
-
*/
|
|
209
|
-
function sanitize(str: string): string {
|
|
210
|
-
return str.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gim, '').trim()
|
|
211
|
-
}
|
|
212
|
-
|
|
213
208
|
/**
|
|
214
209
|
* Converts markdown content to plain text for search indexing.
|
|
215
210
|
* Strips headers, links, tags, and formatting.
|
|
216
211
|
*/
|
|
217
212
|
function parseContentToPlainText(content: string): string {
|
|
218
|
-
|
|
213
|
+
const plainText = content
|
|
219
214
|
.replace(/^#+.*$/gm, '') // Remove headers
|
|
220
215
|
.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') // Simplify links
|
|
221
|
-
.replace(/<[^>]+>/g, '') // Remove HTML/JSX tags
|
|
222
216
|
.replace(/\{[^\}]+\}/g, '') // Remove JS expressions/curly braces
|
|
223
217
|
.replace(/[_*`]/g, '') // Remove formatting
|
|
224
218
|
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
225
|
-
|
|
219
|
+
|
|
220
|
+
return stripHtmlTags(plainText).trim()
|
|
226
221
|
}
|
package/src/node/ssg/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { createRequire } from 'module'
|
|
|
8
8
|
import type { SSGOptions } from './options'
|
|
9
9
|
import { replaceMetaTags } from './meta'
|
|
10
10
|
import { generateSitemap } from './sitemap'
|
|
11
|
+
import { generateRobotsTxt } from './robots'
|
|
11
12
|
|
|
12
13
|
// Re-export options for consumers
|
|
13
14
|
export type { SSGOptions }
|
|
@@ -39,8 +40,34 @@ export async function generateStaticPages(options: SSGOptions): Promise<void> {
|
|
|
39
40
|
)
|
|
40
41
|
return
|
|
41
42
|
}
|
|
43
|
+
|
|
44
|
+
// Mock require so Node doesn't choke on virtual modules compiled externally
|
|
45
|
+
const Module = _require('module')
|
|
46
|
+
const originalRequire = Module.prototype.require
|
|
47
|
+
;(Module.prototype as any).require = function (id: string, ...args: any[]) {
|
|
48
|
+
if (id === 'virtual:boltdocs-layout') {
|
|
49
|
+
return {
|
|
50
|
+
__esModule: true,
|
|
51
|
+
default: function SSG_Virtual_Layout(props: any) {
|
|
52
|
+
try {
|
|
53
|
+
const client = originalRequire.apply(this, [path.resolve(_dirname, '../client/index.js')])
|
|
54
|
+
const Comp = client.DefaultLayout || (({ children }: any) => children)
|
|
55
|
+
const React = originalRequire.apply(this, ['react'])
|
|
56
|
+
return React.createElement(Comp, props)
|
|
57
|
+
} catch (e) {
|
|
58
|
+
return props.children
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return originalRequire.apply(this, [id, ...args])
|
|
64
|
+
}
|
|
65
|
+
|
|
42
66
|
const { render } = _require(ssrModulePath)
|
|
43
67
|
|
|
68
|
+
// Restore require after loading the module
|
|
69
|
+
;(Module.prototype as any).require = originalRequire
|
|
70
|
+
|
|
44
71
|
// Read the built index.html as template
|
|
45
72
|
const templatePath = path.join(outDir, 'index.html')
|
|
46
73
|
if (!fs.existsSync(templatePath)) {
|
|
@@ -57,7 +84,7 @@ export async function generateStaticPages(options: SSGOptions): Promise<void> {
|
|
|
57
84
|
|
|
58
85
|
// We mock the modules for SSR so it doesn't crash trying to dynamically import
|
|
59
86
|
const fakeModules: Record<string, any> = {}
|
|
60
|
-
fakeModules[route.
|
|
87
|
+
fakeModules[`/${docsDirName}/${route.filePath}`] = { default: () => null } // Mock MDX component
|
|
61
88
|
|
|
62
89
|
try {
|
|
63
90
|
const appHtml = await render({
|
|
@@ -83,8 +110,8 @@ export async function generateStaticPages(options: SSGOptions): Promise<void> {
|
|
|
83
110
|
html,
|
|
84
111
|
'utf-8',
|
|
85
112
|
)
|
|
86
|
-
} catch (e) {
|
|
87
|
-
console.error(`[boltdocs] Error SSR rendering route ${route.path}:`, e)
|
|
113
|
+
} catch (e: any) {
|
|
114
|
+
console.error(`[boltdocs] Error SSR rendering route ${route.path}:`, e ? e.stack || e : e)
|
|
88
115
|
}
|
|
89
116
|
}),
|
|
90
117
|
)
|
|
@@ -96,8 +123,12 @@ export async function generateStaticPages(options: SSGOptions): Promise<void> {
|
|
|
96
123
|
)
|
|
97
124
|
fs.writeFileSync(path.join(outDir, 'sitemap.xml'), sitemap, 'utf-8')
|
|
98
125
|
|
|
126
|
+
// Generate robots.txt
|
|
127
|
+
const robots = generateRobotsTxt(config!)
|
|
128
|
+
fs.writeFileSync(path.join(outDir, 'robots.txt'), robots, 'utf-8')
|
|
129
|
+
|
|
99
130
|
console.log(
|
|
100
|
-
`[boltdocs] Generated ${routes.length} static pages + sitemap.xml`,
|
|
131
|
+
`[boltdocs] Generated ${routes.length} static pages + sitemap.xml + robots.txt`,
|
|
101
132
|
)
|
|
102
133
|
|
|
103
134
|
// Ensure all cache operations (like index persistence) are finished
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { BoltdocsConfig } from '../config'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generates the content for a robots.txt file based on the Boltdocs configuration.
|
|
5
|
+
*
|
|
6
|
+
* @param config - The resolved Boltdocs configuration
|
|
7
|
+
* @returns The formatted robots.txt string
|
|
8
|
+
*/
|
|
9
|
+
export function generateRobotsTxt(config: BoltdocsConfig): string {
|
|
10
|
+
if (typeof config.robots === 'string') {
|
|
11
|
+
return config.robots
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const siteUrl = config.siteUrl?.replace(/\/$/, '') || ''
|
|
15
|
+
const robots = config.robots || {}
|
|
16
|
+
const rules = (robots as any).rules || [
|
|
17
|
+
{
|
|
18
|
+
userAgent: '*',
|
|
19
|
+
allow: '/',
|
|
20
|
+
},
|
|
21
|
+
]
|
|
22
|
+
const sitemaps = (robots as any).sitemaps || (siteUrl ? [`${siteUrl}/sitemap.xml`] : [])
|
|
23
|
+
|
|
24
|
+
let content = ''
|
|
25
|
+
|
|
26
|
+
for (const rule of rules) {
|
|
27
|
+
content += `User-agent: ${rule.userAgent}\n`
|
|
28
|
+
|
|
29
|
+
if (rule.disallow) {
|
|
30
|
+
const disallows = Array.isArray(rule.disallow) ? rule.disallow : [rule.disallow]
|
|
31
|
+
for (const d of disallows) {
|
|
32
|
+
content += `Disallow: ${d}\n`
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (rule.allow) {
|
|
37
|
+
const allows = Array.isArray(rule.allow) ? rule.allow : [rule.allow]
|
|
38
|
+
for (const a of allows) {
|
|
39
|
+
content += `Allow: ${a}\n`
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
content += '\n'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const sitemap of sitemaps) {
|
|
46
|
+
content += `Sitemap: ${sitemap}\n`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return content.trim()
|
|
50
|
+
}
|
package/src/node/utils.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import matter from 'gray-matter'
|
|
3
|
+
import DOMPurify from 'isomorphic-dompurify'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Normalizes a file path by replacing Windows backslashes with forward slashes.
|
|
@@ -136,6 +137,28 @@ export function fileToRoutePath(relativePath: string): string {
|
|
|
136
137
|
return routePath
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Sanitizes an HTML string using DOMPurify to prevent XSS.
|
|
142
|
+
* By default, it allows a safe subset of HTML tags.
|
|
143
|
+
*
|
|
144
|
+
* @param html - The raw HTML string
|
|
145
|
+
* @returns The sanitized HTML
|
|
146
|
+
*/
|
|
147
|
+
export function sanitizeHtml(html: string): string {
|
|
148
|
+
return DOMPurify.sanitize(html)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Strips all HTML tags from a string, returning only the text content.
|
|
153
|
+
* Uses DOMPurify for secure and complete tag removal.
|
|
154
|
+
*
|
|
155
|
+
* @param html - The string containing HTML tags
|
|
156
|
+
* @returns The plain text content
|
|
157
|
+
*/
|
|
158
|
+
export function stripHtmlTags(html: string): string {
|
|
159
|
+
return DOMPurify.sanitize(html, { ALLOWED_TAGS: [], KEEP_CONTENT: true })
|
|
160
|
+
}
|
|
161
|
+
|
|
139
162
|
/**
|
|
140
163
|
* Capitalizes the first letter of a given string.
|
|
141
164
|
* Used primarily for generating default group titles.
|