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.
Files changed (60) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/bin/boltdocs.js +12 -0
  4. package/dist/cache-Q4T6VAUL.mjs +1 -0
  5. package/dist/chunk-52MVMZWS.mjs +1 -0
  6. package/dist/chunk-BVWWKXJH.mjs +1 -0
  7. package/dist/chunk-DVY3RDXD.mjs +1 -0
  8. package/dist/chunk-FUVYCYWC.mjs +1 -0
  9. package/dist/chunk-GBLMDJ2B.mjs +1 -0
  10. package/dist/chunk-ISPX45DF.mjs +1 -0
  11. package/dist/chunk-PNXZMUCO.mjs +1 -0
  12. package/dist/chunk-V2ZHKQSP.mjs +74 -0
  13. package/dist/client/components/mdx/index.d.mts +208 -0
  14. package/dist/client/components/mdx/index.d.ts +208 -0
  15. package/dist/client/components/mdx/index.js +1 -0
  16. package/dist/client/components/mdx/index.mjs +1 -0
  17. package/dist/client/hooks/index.d.mts +132 -0
  18. package/dist/client/hooks/index.d.ts +132 -0
  19. package/dist/client/hooks/index.js +1 -0
  20. package/dist/client/hooks/index.mjs +1 -0
  21. package/dist/client/index.d.mts +210 -0
  22. package/dist/client/index.d.ts +210 -0
  23. package/dist/client/index.js +1 -0
  24. package/dist/client/index.mjs +1 -0
  25. package/dist/client/ssr.d.mts +78 -0
  26. package/dist/client/ssr.d.ts +78 -0
  27. package/dist/client/ssr.js +1 -0
  28. package/dist/client/ssr.mjs +1 -0
  29. package/dist/node/cli-entry.d.mts +1 -0
  30. package/dist/node/cli-entry.d.ts +1 -0
  31. package/dist/node/cli-entry.js +75 -0
  32. package/dist/node/cli-entry.mjs +2 -0
  33. package/dist/node/index.d.mts +298 -0
  34. package/dist/node/index.d.ts +298 -0
  35. package/dist/node/index.js +74 -0
  36. package/dist/node/index.mjs +1 -0
  37. package/dist/search-dialog-TWGYKF2D.mjs +1 -0
  38. package/dist/types-Cp21DHI6.d.mts +355 -0
  39. package/dist/types-Cp21DHI6.d.ts +355 -0
  40. package/dist/use-routes-8Iei6jTp.d.mts +29 -0
  41. package/dist/use-routes-xLhumjbV.d.ts +29 -0
  42. package/package.json +16 -10
  43. package/src/client/app/index.tsx +9 -6
  44. package/src/client/components/ui-base/breadcrumbs.tsx +2 -1
  45. package/src/client/components/ui-base/navbar.tsx +3 -3
  46. package/src/client/components/ui-base/sidebar.tsx +2 -1
  47. package/src/client/hooks/use-navbar.ts +1 -1
  48. package/src/client/types.ts +1 -1
  49. package/src/node/cli-entry.ts +24 -0
  50. package/src/node/cli.ts +59 -0
  51. package/src/node/config.ts +63 -11
  52. package/src/node/index.ts +39 -3
  53. package/src/node/plugin/entry.ts +7 -0
  54. package/src/node/plugin/html.ts +49 -9
  55. package/src/node/plugin/index.ts +42 -4
  56. package/src/node/routes/parser.ts +27 -32
  57. package/src/node/ssg/index.ts +35 -4
  58. package/src/node/ssg/robots.ts +50 -0
  59. package/src/node/utils.ts +23 -0
  60. package/tsup.config.ts +36 -10
@@ -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
- themeConfig: {
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
- themeConfig: {
267
- ...defaults.themeConfig,
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
- return [...boltdocsPlugin(options, config), boltdocsMdxPlugin(config)]
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'
@@ -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}
@@ -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 title = config.themeConfig?.title || 'Boltdocs'
13
- const description = config.themeConfig?.description || ''
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="summary">`,
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
- ].join('\n ')
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
- html = html.replace(/<title>.*?<\/title>/, `<title>${title}</title>`)
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>',
@@ -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
- // Explicitly watch config files and mdx-components to trigger server restarts or module invalidations
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
- // Strip simple markdown formatting specifically for the plain-text search index
128
- const text = match[2]
129
- .replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1')
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 sanitize = (str: string) =>
143
- str.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gim, '').trim()
144
-
145
- const sanitizedTitle = data.title ? sanitize(data.title) : inferredTitle
146
- let sanitizedDescription = data.description ? sanitize(data.description) : ''
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
- sanitizedDescription = sanitize(
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 ? sanitize(data.badge) : undefined
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
- return content
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
- .trim()
219
+
220
+ return stripHtmlTags(plainText).trim()
226
221
  }
@@ -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.componentPath] = { default: () => {} } // Mock MDX component
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.