boltdocs 2.2.0 → 2.3.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 (81) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/bin/boltdocs.js +2 -2
  3. package/dist/base-ui/index.d.mts +3 -3
  4. package/dist/base-ui/index.d.ts +3 -3
  5. package/dist/base-ui/index.js +1 -1
  6. package/dist/base-ui/index.mjs +1 -1
  7. package/dist/{cache-CRAZ55X7.mjs → cache-P6WK424C.mjs} +1 -1
  8. package/dist/chunk-22NXDNP4.mjs +74 -0
  9. package/dist/chunk-2HUVMMJU.mjs +1 -0
  10. package/dist/chunk-2Z5T6EAU.mjs +1 -0
  11. package/dist/chunk-CRZGOE32.mjs +1 -0
  12. package/dist/chunk-HA6543SL.mjs +1 -0
  13. package/dist/{chunk-ZK2266IZ.mjs → chunk-RPUERTVC.mjs} +1 -1
  14. package/dist/{chunk-MZBG4N4W.mjs → chunk-URTD6E6S.mjs} +1 -1
  15. package/dist/chunk-W2NB4T6V.mjs +1 -0
  16. package/dist/chunk-Y4RRHPXC.mjs +1 -0
  17. package/dist/client/index.d.mts +1 -1
  18. package/dist/client/index.d.ts +1 -1
  19. package/dist/client/index.js +1 -1
  20. package/dist/client/index.mjs +1 -1
  21. package/dist/client/ssr.js +1 -1
  22. package/dist/client/ssr.mjs +1 -1
  23. package/dist/hooks/index.d.mts +7 -15
  24. package/dist/hooks/index.d.ts +7 -15
  25. package/dist/hooks/index.js +1 -1
  26. package/dist/hooks/index.mjs +1 -1
  27. package/dist/{loading-chS3pm9W.d.ts → loading-B7X5Wchs.d.ts} +3 -5
  28. package/dist/{loading-BqGrFWO5.d.mts → loading-WuaQbsKb.d.mts} +3 -5
  29. package/dist/mdx/index.js +1 -1
  30. package/dist/mdx/index.mjs +1 -1
  31. package/dist/node/cli-entry.js +27 -23
  32. package/dist/node/cli-entry.mjs +5 -1
  33. package/dist/node/index.js +10 -10
  34. package/dist/node/index.mjs +1 -1
  35. package/dist/primitives/index.d.mts +2 -2
  36. package/dist/primitives/index.d.ts +2 -2
  37. package/dist/primitives/index.js +1 -1
  38. package/dist/primitives/index.mjs +1 -1
  39. package/dist/search-dialog-ZRXBAQJ5.mjs +1 -0
  40. package/package.json +2 -1
  41. package/src/client/app/theme-context.tsx +14 -7
  42. package/src/client/components/default-layout.tsx +6 -2
  43. package/src/client/components/primitives/navbar.tsx +3 -3
  44. package/src/client/components/primitives/search-dialog.tsx +4 -4
  45. package/src/client/components/primitives/sidebar.tsx +3 -2
  46. package/src/client/components/primitives/skeleton.tsx +26 -0
  47. package/src/client/components/ui-base/loading.tsx +43 -73
  48. package/src/client/components/ui-base/navbar.tsx +8 -6
  49. package/src/client/components/ui-base/page-nav.tsx +2 -1
  50. package/src/client/components/ui-base/powered-by.tsx +4 -1
  51. package/src/client/components/ui-base/search-dialog.tsx +16 -5
  52. package/src/client/components/ui-base/sidebar.tsx +4 -2
  53. package/src/client/hooks/use-i18n.ts +3 -2
  54. package/src/client/hooks/use-localized-to.ts +6 -5
  55. package/src/client/hooks/use-page-nav.ts +27 -6
  56. package/src/client/hooks/use-routes.ts +2 -1
  57. package/src/client/hooks/use-search.ts +81 -59
  58. package/src/client/hooks/use-sidebar.ts +2 -1
  59. package/src/client/store/use-boltdocs-store.ts +6 -5
  60. package/src/client/theme/neutral.css +29 -0
  61. package/src/node/{cli.ts → cli/build.ts} +17 -23
  62. package/src/node/cli/dev.ts +22 -0
  63. package/src/node/cli/doctor.ts +243 -0
  64. package/src/node/cli/index.ts +9 -0
  65. package/src/node/cli/ui.ts +54 -0
  66. package/src/node/cli-entry.ts +16 -16
  67. package/src/node/config.ts +1 -1
  68. package/src/node/plugin/entry.ts +1 -1
  69. package/src/node/plugin/index.ts +24 -10
  70. package/src/node/routes/parser.ts +9 -9
  71. package/src/node/search/index.ts +55 -0
  72. package/src/node/ssg/index.ts +14 -6
  73. package/src/node/ssg/robots.ts +7 -4
  74. package/dist/chunk-5D6XPYQ3.mjs +0 -74
  75. package/dist/chunk-6QXCKZAT.mjs +0 -1
  76. package/dist/chunk-H4M6P3DM.mjs +0 -1
  77. package/dist/chunk-JXHNX2WN.mjs +0 -1
  78. package/dist/chunk-Q3MLYTIQ.mjs +0 -1
  79. package/dist/chunk-RSII2UPE.mjs +0 -1
  80. package/dist/chunk-ZRJ55GGF.mjs +0 -1
  81. package/dist/search-dialog-MA5AISC7.mjs +0 -1
@@ -1,27 +1,16 @@
1
- import { createServer, build, preview } from 'vite'
2
- import { createViteConfig, resolveConfig } from './index'
3
- import { getHtmlTemplate } from './plugin/html'
1
+ import { build, preview } from 'vite'
2
+ import { createViteConfig, resolveConfig } from '../index'
3
+ import { getHtmlTemplate } from '../plugin/html'
4
4
  import path from 'path'
5
5
  import fs from 'fs'
6
+ import * as ui from './ui'
6
7
 
7
8
  /**
8
- * Core logic for the Boltdocs CLI commands.
9
- * These functions wrap Vite's JS API to provide a seamless experience.
9
+ * Logic for the `boltdocs build` command.
10
+ * Prepares the production bundle and handles dynamic index.html generation.
11
+ *
12
+ * @param root - The project root directory
10
13
  */
11
-
12
- export async function devAction(root: string = process.cwd()) {
13
- try {
14
- const viteConfig = await createViteConfig(root, 'development')
15
- const server = await createServer(viteConfig)
16
- await server.listen()
17
- server.printUrls()
18
- server.bindCLIShortcuts({ print: true })
19
- } catch (e) {
20
- console.error('[boltdocs] Failed to start dev server:', e)
21
- process.exit(1)
22
- }
23
- }
24
-
25
14
  export async function buildAction(root: string = process.cwd()) {
26
15
  let createdIndexHtml = false
27
16
  const indexPath = path.resolve(root, 'index.html')
@@ -35,9 +24,9 @@ export async function buildAction(root: string = process.cwd()) {
35
24
 
36
25
  const viteConfig = await createViteConfig(root, 'production')
37
26
  await build(viteConfig)
38
- console.log('[boltdocs] Build completed successfully.')
27
+ ui.success('Build completed successfully.')
39
28
  } catch (e) {
40
- console.error('[boltdocs] Build failed:', e)
29
+ ui.error('Build failed:', e)
41
30
  process.exit(1)
42
31
  } finally {
43
32
  if (createdIndexHtml && fs.existsSync(indexPath)) {
@@ -46,14 +35,19 @@ export async function buildAction(root: string = process.cwd()) {
46
35
  }
47
36
  }
48
37
 
38
+ /**
39
+ * Logic for the `boltdocs preview` command.
40
+ * Serves the production build from the disk.
41
+ *
42
+ * @param root - The project root directory
43
+ */
49
44
  export async function previewAction(root: string = process.cwd()) {
50
45
  try {
51
46
  const viteConfig = await createViteConfig(root, 'production')
52
47
  const previewServer = await preview(viteConfig)
53
48
  previewServer.printUrls()
54
49
  } catch (e) {
55
- console.error('[boltdocs] Failed to start preview server:', e)
50
+ ui.error('Failed to start preview server:', e)
56
51
  process.exit(1)
57
52
  }
58
53
  }
59
-
@@ -0,0 +1,22 @@
1
+ import { createServer } from 'vite'
2
+ import { createViteConfig } from '../index'
3
+ import * as ui from './ui'
4
+
5
+ /**
6
+ * Logic for the `boltdocs dev` command.
7
+ * Starts a Vite development server and sets up HMR.
8
+ *
9
+ * @param root - The project root directory
10
+ */
11
+ export async function devAction(root: string = process.cwd()) {
12
+ try {
13
+ const viteConfig = await createViteConfig(root, 'development')
14
+ const server = await createServer(viteConfig)
15
+ await server.listen()
16
+ server.printUrls()
17
+ server.bindCLIShortcuts({ print: true })
18
+ } catch (e) {
19
+ ui.error('Failed to start dev server:', e)
20
+ process.exit(1)
21
+ }
22
+ }
@@ -0,0 +1,243 @@
1
+ import path from 'path'
2
+ import fs from 'fs'
3
+ import fastGlob from 'fast-glob'
4
+ import { resolveConfig } from '../config'
5
+ import { parseFrontmatter, normalizePath } from '../utils'
6
+ import * as ui from './ui'
7
+
8
+ /**
9
+ * Interface representing a documentation hygiene issue.
10
+ */
11
+ interface Issue {
12
+ level: 'high' | 'warning' | 'low'
13
+ message: string
14
+ suggestion?: string
15
+ }
16
+
17
+ /**
18
+ * Logic for the `boltdocs doctor` command.
19
+ * Scans the documentation directory for broken links, missing frontmatter,
20
+ * and orphaned translations.
21
+ *
22
+ * @param root - The project root directory
23
+ */
24
+ export async function doctorAction(root: string = process.cwd()) {
25
+ const { colors } = ui
26
+ ui.info(
27
+ `${colors.bold}Running documentation health check...${colors.reset}\n`,
28
+ )
29
+ const start = performance.now()
30
+
31
+ try {
32
+ const config = await resolveConfig('docs', root)
33
+ const docsDir = path.resolve(root, 'docs')
34
+
35
+ if (!fs.existsSync(docsDir)) {
36
+ ui.error(`Documentation directory not found at ${docsDir}`)
37
+ process.exit(1)
38
+ }
39
+
40
+ const files = await fastGlob(['**/*.md', '**/*.mdx'], {
41
+ cwd: docsDir,
42
+ absolute: true,
43
+ suppressErrors: true,
44
+ })
45
+
46
+ let highCount = 0
47
+ let warningCount = 0
48
+ let lowCount = 0
49
+ const issuesMap = new Map<string, Issue[]>()
50
+
51
+ const addIssue = (file: string, issue: Issue) => {
52
+ const relPath = path.relative(docsDir, file)
53
+ let issues = issuesMap.get(relPath)
54
+ if (!issues) {
55
+ issues = []
56
+ issuesMap.set(relPath, issues)
57
+ }
58
+ issues.push(issue)
59
+ if (issue.level === 'high') highCount++
60
+ else if (issue.level === 'warning') warningCount++
61
+ else if (issue.level === 'low') lowCount++
62
+ }
63
+
64
+ const basePath = '/docs'
65
+
66
+ // 1. Scan for Frontmatter, Links, and Content Issues
67
+ for (const file of files) {
68
+ const { data, content } = parseFrontmatter(file)
69
+
70
+ // Frontmatter Validation
71
+ if (!data.title) {
72
+ addIssue(file, {
73
+ level: 'warning',
74
+ message: 'Missing "title" in frontmatter.',
75
+ suggestion:
76
+ 'Add `title: Your Title` to the YAML frontmatter at the top of the file.',
77
+ })
78
+ }
79
+
80
+ if (!data.description) {
81
+ addIssue(file, {
82
+ level: 'low',
83
+ message: 'Missing "description" in frontmatter.',
84
+ suggestion:
85
+ 'Adding a description helps with SEO and search previews.',
86
+ })
87
+ }
88
+
89
+ // Link Validation
90
+ const linkRegex = /\[.*?\]\((.*?)\)/g
91
+ const htmlLinkRegex = /<a\s+[^>]*href=["']([^"']+)["'][^>]*>/g
92
+ const links = [
93
+ ...content.matchAll(linkRegex),
94
+ ...content.matchAll(htmlLinkRegex),
95
+ ]
96
+
97
+ for (const match of links) {
98
+ let link = match[1]
99
+ if (
100
+ !link ||
101
+ link.startsWith('http') ||
102
+ link.startsWith('https') ||
103
+ link.startsWith('#') ||
104
+ link.startsWith('mailto:') ||
105
+ link.startsWith('tel:')
106
+ ) {
107
+ continue
108
+ }
109
+
110
+ link = link.split('#')[0]
111
+ if (!link) continue
112
+
113
+ let targetPath: string
114
+ if (link.startsWith('/')) {
115
+ let pathAfterBase = link
116
+ if (link.startsWith(basePath + '/') || link === basePath) {
117
+ pathAfterBase = link.substring(basePath.length)
118
+ }
119
+ targetPath = path.join(docsDir, pathAfterBase)
120
+ } else {
121
+ targetPath = path.resolve(path.dirname(file), link)
122
+ }
123
+
124
+ const extensions = ['', '.md', '.mdx', '/index.md', '/index.mdx']
125
+ let exists = false
126
+ for (const ext of extensions) {
127
+ const finalPath = targetPath + ext
128
+ if (fs.existsSync(finalPath) && fs.statSync(finalPath).isFile()) {
129
+ exists = true
130
+ break
131
+ }
132
+ }
133
+
134
+ if (!exists) {
135
+ addIssue(file, {
136
+ level: 'high',
137
+ message: `Broken internal link: "${link}"`,
138
+ suggestion: `Ensure the file exists at "${targetPath}". If it's a directory, ensure it has an "index.md" or "index.mdx".`,
139
+ })
140
+ }
141
+ }
142
+ }
143
+
144
+ // 2. Scan for Orphaned Translations
145
+ if (config.i18n) {
146
+ const { defaultLocale, locales } = config.i18n
147
+ const otherLocales = Object.keys(locales).filter(
148
+ (l) => l !== defaultLocale,
149
+ )
150
+
151
+ for (const file of files) {
152
+ const relPath = normalizePath(path.relative(docsDir, file))
153
+ const parts = relPath.split('/')
154
+
155
+ if (parts[0] === defaultLocale) {
156
+ const pathAfterLocale = parts.slice(1).join('/')
157
+ for (const locale of otherLocales) {
158
+ const localeParts = [locale, ...parts.slice(1)]
159
+ const targetLocaleFile = path.join(docsDir, ...localeParts)
160
+
161
+ if (!fs.existsSync(targetLocaleFile)) {
162
+ addIssue(file, {
163
+ level: 'warning',
164
+ message: `Missing translation for locale "${locale}"`,
165
+ suggestion: `Create a translated version of this file at "${locale}/${pathAfterLocale}".`,
166
+ })
167
+ }
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ // Final Reporting
174
+ if (issuesMap.size === 0) {
175
+ ui.success('All documentation files are healthy!\n')
176
+ } else {
177
+ for (const [file, issues] of issuesMap.entries()) {
178
+ console.log(`📄 ${colors.bold}${file}${colors.reset}`)
179
+
180
+ const sortedIssues = issues.sort((a, b) => {
181
+ const order = { high: 1, warning: 2, low: 3 }
182
+ return order[a.level] - order[b.level]
183
+ })
184
+
185
+ for (const issue of sortedIssues) {
186
+ let prefix = ''
187
+ let color = ''
188
+ if (issue.level === 'high') {
189
+ prefix = '❌'
190
+ color = colors.red
191
+ } else if (issue.level === 'warning') {
192
+ prefix = '⚠️'
193
+ color = colors.yellow
194
+ } else {
195
+ prefix = 'ℹ️'
196
+ color = colors.cyan
197
+ }
198
+
199
+ console.log(
200
+ ` ${color}${prefix} ${issue.level.toUpperCase()}:${colors.reset} ${issue.message}`,
201
+ )
202
+ if (issue.suggestion) {
203
+ console.log(
204
+ ` ${colors.gray}💡 Suggestion: ${issue.suggestion}${colors.reset}`,
205
+ )
206
+ }
207
+ }
208
+ console.log('')
209
+ }
210
+
211
+ console.log(`${colors.bold}Summary:${colors.reset}`)
212
+ console.log(
213
+ ` ${colors.red}${highCount} high-level errors${colors.reset}`,
214
+ )
215
+ console.log(` ${colors.yellow}${warningCount} warnings${colors.reset}`)
216
+ console.log(
217
+ ` ${colors.cyan}${lowCount} minor improvements${colors.reset}\n`,
218
+ )
219
+
220
+ if (highCount > 0) {
221
+ ui.error(
222
+ 'HIGH ERROR: Fix these to ensure your documentation builds correctly.',
223
+ )
224
+ }
225
+ if (warningCount > 0 || lowCount > 0) {
226
+ ui.info(
227
+ 'TIP: Address warnings and suggestions for premium quality docs.',
228
+ )
229
+ }
230
+ console.log('')
231
+ }
232
+
233
+ const duration = performance.now() - start
234
+ ui.info(`Finished in ${duration.toFixed(2)}ms\n`)
235
+
236
+ if (highCount > 0) {
237
+ process.exit(1)
238
+ }
239
+ } catch (e) {
240
+ ui.error('Failed to run doctor check:', e)
241
+ process.exit(1)
242
+ }
243
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Boltdocs CLI Actions Module.
3
+ * This module exports all command-line actions used by the Boltdocs tool.
4
+ */
5
+
6
+ export * from './dev'
7
+ export * from './build'
8
+ export * from './doctor'
9
+ export * from './ui'
@@ -0,0 +1,54 @@
1
+ /**
2
+ * ANSI Escape sequences for terminal coloring and styling.
3
+ * Used to provide a premium and consistent CLI experience.
4
+ */
5
+ export const colors = {
6
+ reset: '\x1b[0m',
7
+ bold: '\x1b[1m',
8
+ red: '\x1b[31m',
9
+ green: '\x1b[32m',
10
+ yellow: '\x1b[33m',
11
+ blue: '\x1b[34m',
12
+ cyan: '\x1b[36m',
13
+ gray: '\x1b[90m',
14
+ }
15
+
16
+ /**
17
+ * Formats a message with the boltdocs prefix and provided styling.
18
+ *
19
+ * @param message - The content to log
20
+ * @param style - Optional ANSI style prefix
21
+ * @returns The formatted string
22
+ */
23
+ export function formatLog(message: string, style: string = ''): string {
24
+ return `${style}${colors.bold}[boltdocs]${colors.reset} ${message}${colors.reset}`
25
+ }
26
+
27
+ /**
28
+ * Logs a standard informational message to the console.
29
+ *
30
+ * @param message - The message to display
31
+ */
32
+ export function info(message: string) {
33
+ console.log(formatLog(message))
34
+ }
35
+
36
+ /**
37
+ * Logs an error message to the console with red styling.
38
+ *
39
+ * @param message - The error description
40
+ * @param error - Optional error object for stack tracing
41
+ */
42
+ export function error(message: string, error?: any) {
43
+ console.error(formatLog(message, colors.red))
44
+ if (error) console.error(error)
45
+ }
46
+
47
+ /**
48
+ * Logs a success message to the console with green styling.
49
+ *
50
+ * @param message - The success description
51
+ */
52
+ export function success(message: string) {
53
+ console.log(formatLog(message, colors.green))
54
+ }
@@ -1,24 +1,24 @@
1
1
  #!/usr/bin/env node
2
- import cac from "cac";
3
- import { devAction, buildAction, previewAction } from "./cli";
2
+ import cac from 'cac'
3
+ import {
4
+ devAction,
5
+ buildAction,
6
+ previewAction,
7
+ doctorAction,
8
+ } from './cli/index'
4
9
 
5
- const cli = cac("boltdocs");
10
+ const cli = cac('boltdocs')
6
11
 
7
- cli
8
- .command("[root]", "Start development server")
9
- .alias("dev")
10
- .action(devAction);
12
+ cli.command('[root]', 'Start development server').alias('dev').action(devAction)
11
13
 
12
- cli
13
- .command("build [root]", "Build for production")
14
- .action(buildAction);
14
+ cli.command('build [root]', 'Build for production').action(buildAction)
15
15
 
16
- cli
17
- .command("preview [root]", "Preview production build")
18
- .action(previewAction);
16
+ cli.command('preview [root]', 'Preview production build').action(previewAction)
19
17
 
20
- cli.help();
18
+ cli.command('doctor [root]', 'Check documentation health').action(doctorAction)
19
+
20
+ cli.help()
21
21
  // This will be replaced at build time or package publishing, but hardcoded to 2.0.0 for now
22
- cli.version("2.0.0");
22
+ cli.version('2.0.0')
23
23
 
24
- cli.parse();
24
+ cli.parse()
@@ -164,7 +164,7 @@ export interface BoltdocsVersionConfig {
164
164
  export interface BoltdocsVersionsConfig {
165
165
  /** The default version path (e.g., 'v2') */
166
166
  defaultVersion: string
167
- /**
167
+ /**
168
168
  * Optional prefix for all version paths (e.g., 'v').
169
169
  * If set to 'v', version '1.1' will be available at '/docs/v1.1'.
170
170
  */
@@ -19,7 +19,7 @@ export function generateEntryCode(
19
19
  const homeImport = options.homePage
20
20
  ? `import HomePage from '${normalizePath(options.homePage)}';`
21
21
  : ''
22
-
22
+
23
23
  // Auto-import index.css if it exists
24
24
  const cssPath = path.resolve(process.cwd(), 'index.css')
25
25
  const cssImport = fs.existsSync(cssPath) ? "import './index.css';" : ''
@@ -9,6 +9,7 @@ import type { BoltdocsPluginOptions } from './types'
9
9
  import { generateEntryCode } from './entry'
10
10
  import { injectHtmlMeta, getHtmlTemplate } from './html'
11
11
  import { generateRobotsTxt } from '../ssg/robots'
12
+ import { generateSearchData } from '../search'
12
13
  import fs from 'fs'
13
14
 
14
15
  export * from './types'
@@ -108,13 +109,12 @@ export function boltdocsPlugin(
108
109
 
109
110
  // Improved check: If it's a doc route, serve HTML even if it has a dot (e.g. version 1.1)
110
111
  // We only skip if it has a known asset extension to prevent serving HTML for images/js/etc.
111
- const isAsset = /\.(js|css|png|jpe?g|gif|svg|ico|webp|woff2?|ttf|otf|mp4|webm|ogg|mp3|wav|flac|aac|pdf|zip|gz|map|json)$/i.test(url)
112
+ const isAsset =
113
+ /\.(js|css|png|jpe?g|gif|svg|ico|webp|woff2?|ttf|otf|mp4|webm|ogg|mp3|wav|flac|aac|pdf|zip|gz|map|json)$/i.test(
114
+ url,
115
+ )
112
116
 
113
- if (
114
- accept.includes('text/html') &&
115
- !isAsset &&
116
- isDocRoute
117
- ) {
117
+ if (accept.includes('text/html') && !isAsset && isDocRoute) {
118
118
  let html = getHtmlTemplate(config)
119
119
  html = injectHtmlMeta(html, config)
120
120
  html = await server.transformIndexHtml(req.url || '/', html)
@@ -197,8 +197,10 @@ export function boltdocsPlugin(
197
197
  invalidateRouteCache()
198
198
  // Re-resolve config as it might affect versions/routes
199
199
  config = await resolveConfig(docsDir)
200
-
201
- const configMod = server.moduleGraph.getModuleById('\0virtual:boltdocs-config')
200
+
201
+ const configMod = server.moduleGraph.getModuleById(
202
+ '\0virtual:boltdocs-config',
203
+ )
202
204
  if (configMod) server.moduleGraph.invalidateModule(configMod)
203
205
 
204
206
  server.ws.send({
@@ -219,7 +221,12 @@ export function boltdocsPlugin(
219
221
  // Regenerate and push to client
220
222
  // Optimization: generateRoutes is mostly incremental thanks to docCache
221
223
  // We only force a full disk scan on add/unlink events
222
- const newRoutes = await generateRoutes(docsDir, config, '/docs', type !== 'change')
224
+ const newRoutes = await generateRoutes(
225
+ docsDir,
226
+ config,
227
+ '/docs',
228
+ type !== 'change',
229
+ )
223
230
 
224
231
  const routesMod = server.moduleGraph.getModuleById(
225
232
  '\0virtual:boltdocs-routes',
@@ -247,7 +254,8 @@ export function boltdocsPlugin(
247
254
  id === 'virtual:boltdocs-config' ||
248
255
  id === 'virtual:boltdocs-entry' ||
249
256
  id === 'virtual:boltdocs-mdx-components' ||
250
- id === 'virtual:boltdocs-layout'
257
+ id === 'virtual:boltdocs-layout' ||
258
+ id === 'virtual:boltdocs-search'
251
259
  ) {
252
260
  return '\0' + id
253
261
  }
@@ -316,6 +324,12 @@ export default UserLayout;`
316
324
  return `import { DefaultLayout } from 'boltdocs/client';
317
325
  export default DefaultLayout;`
318
326
  }
327
+
328
+ if (id === '\0virtual:boltdocs-search') {
329
+ const routes = await generateRoutes(docsDir, config)
330
+ const searchData = generateSearchData(routes)
331
+ return `export default ${JSON.stringify(searchData, null, 2)};`
332
+ }
319
333
  },
320
334
 
321
335
  transformIndexHtml: {
@@ -60,14 +60,12 @@ export function parseDocFile(
60
60
  if (config?.versions && parts.length > 0) {
61
61
  const potentialVersion = parts[0]
62
62
  const prefix = config.versions.prefix || ''
63
-
64
- const versionMatch = config.versions.versions.find(
65
- (v) => {
66
- const fullPath = prefix + v.path
67
- return potentialVersion === fullPath || potentialVersion === v.path
68
- }
69
- )
70
-
63
+
64
+ const versionMatch = config.versions.versions.find((v) => {
65
+ const fullPath = prefix + v.path
66
+ return potentialVersion === fullPath || potentialVersion === v.path
67
+ })
68
+
71
69
  if (versionMatch) {
72
70
  version = versionMatch.path
73
71
  parts = parts.slice(1)
@@ -168,7 +166,9 @@ export function parseDocFile(
168
166
  sanitizedDescription = plainExcerpt
169
167
  }
170
168
 
171
- const sanitizedBadge = data.badge ? sanitizeHtml(String(data.badge)) : undefined
169
+ const sanitizedBadge = data.badge
170
+ ? sanitizeHtml(String(data.badge))
171
+ : undefined
172
172
  const icon = data.icon ? String(data.icon) : undefined
173
173
 
174
174
  // Extract full content as plain text for search indexing
@@ -0,0 +1,55 @@
1
+ import type { RouteMeta } from '../routes/types'
2
+
3
+ export interface SearchDocument {
4
+ id: string
5
+ title: string
6
+ content: string
7
+ url: string
8
+ display: string
9
+ locale?: string
10
+ version?: string
11
+ }
12
+
13
+ /**
14
+ * Generates a flat list of searchable documents from the route metadata.
15
+ * Each page is indexed as a primary document, and its sections (headings)
16
+ * are indexed as secondary documents to provide granular search results.
17
+ */
18
+ export function generateSearchData(routes: RouteMeta[]): SearchDocument[] {
19
+ const documents: SearchDocument[] = []
20
+
21
+ for (const route of routes) {
22
+ // 1. Index the main page
23
+ documents.push({
24
+ id: route.path,
25
+ title: route.title,
26
+ content: route._content || '',
27
+ url: route.path,
28
+ display: route.groupTitle
29
+ ? `${route.groupTitle} > ${route.title}`
30
+ : route.title,
31
+ locale: route.locale,
32
+ version: route.version,
33
+ })
34
+
35
+ // 2. Index headings as sub-documents for deep linking
36
+ if (route.headings) {
37
+ for (const heading of route.headings) {
38
+ // We find the content belonging to this heading?
39
+ // For now, indexing just the heading text and a bit of context is standard.
40
+ // Deep full-text mapping to specific headings is more complex.
41
+ documents.push({
42
+ id: `${route.path}#${heading.id}`,
43
+ title: heading.text,
44
+ content: `${heading.text} in ${route.title}`,
45
+ url: `${route.path}#${heading.id}`,
46
+ display: `${route.title} > ${heading.text}`,
47
+ locale: route.locale,
48
+ version: route.version,
49
+ })
50
+ }
51
+ }
52
+ }
53
+
54
+ return documents
55
+ }
@@ -48,14 +48,17 @@ export async function generateStaticPages(options: SSGOptions): Promise<void> {
48
48
  __esModule: true,
49
49
  default: function SSG_Virtual_Layout(props: any) {
50
50
  try {
51
- const client = originalRequire.apply(this, [path.resolve(_dirname, '../client/index.js')])
52
- const Comp = client.DefaultLayout || (({ children }: any) => children)
51
+ const client = originalRequire.apply(this, [
52
+ path.resolve(_dirname, '../client/index.js'),
53
+ ])
54
+ const Comp =
55
+ client.DefaultLayout || (({ children }: any) => children)
53
56
  const React = originalRequire.apply(this, ['react'])
54
57
  return React.createElement(Comp, props)
55
58
  } catch (e) {
56
59
  return props.children
57
60
  }
58
- }
61
+ },
59
62
  }
60
63
  }
61
64
  return originalRequire.apply(this, [id, ...args])
@@ -77,8 +80,10 @@ export async function generateStaticPages(options: SSGOptions): Promise<void> {
77
80
  // Generate an HTML file for each route concurrently
78
81
  await Promise.all(
79
82
  routes.map(async (route) => {
80
- const siteTitle = getTranslated(config?.theme?.title, route.locale) || 'Boltdocs'
81
- const siteDescription = getTranslated(config?.theme?.description, route.locale) || ''
83
+ const siteTitle =
84
+ getTranslated(config?.theme?.title, route.locale) || 'Boltdocs'
85
+ const siteDescription =
86
+ getTranslated(config?.theme?.description, route.locale) || ''
82
87
  const pageTitle = `${route.title} | ${siteTitle}`
83
88
  const pageDescription = route.description || siteDescription
84
89
 
@@ -111,7 +116,10 @@ export async function generateStaticPages(options: SSGOptions): Promise<void> {
111
116
  'utf-8',
112
117
  )
113
118
  } catch (e: any) {
114
- console.error(`[boltdocs] Error SSR rendering route ${route.path}:`, e ? e.stack || e : e)
119
+ console.error(
120
+ `[boltdocs] Error SSR rendering route ${route.path}:`,
121
+ e ? e.stack || e : e,
122
+ )
115
123
  }
116
124
  }),
117
125
  )